From b9492170d31012932c13fd4d630a70bb4e764d8b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 07:39:55 +0000 Subject: [PATCH] chore: version packages (rc) --- .changeset/pre.json | 138 + examples/app-crm/CHANGELOG.md | 69 + examples/app-crm/package.json | 2 +- examples/app-showcase/CHANGELOG.md | 86 + examples/app-showcase/package.json | 2 +- examples/app-todo/CHANGELOG.md | 102 + examples/app-todo/package.json | 2 +- examples/embed-objectql/CHANGELOG.md | 85 + examples/embed-objectql/package.json | 2 +- packages/adapters/hono/CHANGELOG.md | 18 + packages/adapters/hono/package.json | 2 +- packages/apps/account/CHANGELOG.md | 62 + packages/apps/account/package.json | 2 +- packages/apps/setup/CHANGELOG.md | 62 + packages/apps/setup/package.json | 2 +- packages/apps/studio/CHANGELOG.md | 62 + packages/apps/studio/package.json | 2 +- packages/cli/CHANGELOG.md | 285 ++ packages/cli/package.json | 2 +- packages/client-react/CHANGELOG.md | 65 + packages/client-react/package.json | 2 +- packages/client/CHANGELOG.md | 213 ++ packages/client/package.json | 2 +- packages/cloud-connection/CHANGELOG.md | 72 + packages/cloud-connection/package.json | 2 +- .../connectors/connector-mcp/CHANGELOG.md | 62 + .../connectors/connector-mcp/package.json | 2 +- .../connectors/connector-openapi/CHANGELOG.md | 62 + .../connectors/connector-openapi/package.json | 2 +- .../connectors/connector-rest/CHANGELOG.md | 62 + .../connectors/connector-rest/package.json | 2 +- .../connectors/connector-slack/CHANGELOG.md | 62 + .../connectors/connector-slack/package.json | 2 +- packages/console/CHANGELOG.md | 84 + packages/console/package.json | 2 +- packages/core/CHANGELOG.md | 61 + packages/core/package.json | 2 +- packages/create-objectstack/CHANGELOG.md | 2 + packages/create-objectstack/package.json | 2 +- packages/drivers/driver-memory/CHANGELOG.md | 214 ++ packages/drivers/driver-memory/package.json | 2 +- packages/drivers/driver-mongodb/CHANGELOG.md | 216 ++ packages/drivers/driver-mongodb/package.json | 2 +- packages/drivers/driver-sql/CHANGELOG.md | 415 +++ packages/drivers/driver-sql/package.json | 2 +- .../drivers/driver-sqlite-wasm/CHANGELOG.md | 185 ++ .../drivers/driver-sqlite-wasm/package.json | 2 +- packages/drivers/driver-turso/CHANGELOG.md | 248 ++ packages/drivers/driver-turso/package.json | 2 +- packages/formula/CHANGELOG.md | 181 ++ packages/formula/package.json | 2 +- packages/lint/CHANGELOG.md | 532 ++++ packages/lint/package.json | 2 +- packages/mcp/CHANGELOG.md | 99 + packages/mcp/package.json | 2 +- packages/metadata-core/CHANGELOG.md | 134 + packages/metadata-core/package.json | 2 +- packages/metadata-fs/CHANGELOG.md | 8 + packages/metadata-fs/package.json | 2 +- packages/metadata-protocol/CHANGELOG.md | 458 +++ packages/metadata-protocol/package.json | 2 +- packages/metadata/CHANGELOG.md | 108 + packages/metadata/package.json | 2 +- packages/objectql/CHANGELOG.md | 800 +++++ packages/objectql/package.json | 2 +- packages/observability/CHANGELOG.md | 61 + packages/observability/package.json | 2 +- packages/platform-objects/CHANGELOG.md | 64 + packages/platform-objects/package.json | 2 +- packages/plugins/embedder-openai/CHANGELOG.md | 61 + packages/plugins/embedder-openai/package.json | 2 +- .../plugins/knowledge-memory/CHANGELOG.md | 63 + .../plugins/knowledge-memory/package.json | 2 +- .../plugins/knowledge-ragflow/CHANGELOG.md | 63 + .../plugins/knowledge-ragflow/package.json | 2 +- .../plugins/plugin-approvals/CHANGELOG.md | 72 + .../plugins/plugin-approvals/package.json | 2 +- packages/plugins/plugin-audit/CHANGELOG.md | 89 + packages/plugins/plugin-audit/package.json | 2 +- packages/plugins/plugin-auth/CHANGELOG.md | 169 ++ packages/plugins/plugin-auth/package.json | 2 +- packages/plugins/plugin-dev/CHANGELOG.md | 110 + packages/plugins/plugin-dev/package.json | 2 +- packages/plugins/plugin-email/CHANGELOG.md | 67 + packages/plugins/plugin-email/package.json | 2 +- .../plugins/plugin-hono-server/CHANGELOG.md | 103 + .../plugins/plugin-hono-server/package.json | 2 +- .../plugins/plugin-pinyin-search/CHANGELOG.md | 33 + .../plugins/plugin-pinyin-search/package.json | 2 +- packages/plugins/plugin-reports/CHANGELOG.md | 63 + packages/plugins/plugin-reports/package.json | 2 +- packages/plugins/plugin-security/CHANGELOG.md | 67 + packages/plugins/plugin-security/package.json | 2 +- packages/plugins/plugin-sharing/CHANGELOG.md | 177 ++ packages/plugins/plugin-sharing/package.json | 2 +- packages/plugins/plugin-webhooks/CHANGELOG.md | 65 + packages/plugins/plugin-webhooks/package.json | 2 +- packages/qa/dogfood/CHANGELOG.md | 120 + packages/qa/dogfood/package.json | 2 +- packages/qa/downstream-contract/CHANGELOG.md | 61 + packages/qa/downstream-contract/package.json | 2 +- packages/qa/http-conformance/CHANGELOG.md | 6 + packages/qa/http-conformance/package.json | 2 +- packages/rest/CHANGELOG.md | 390 +++ packages/rest/package.json | 2 +- packages/runtime/CHANGELOG.md | 363 +++ packages/runtime/package.json | 2 +- packages/sdui-parser/CHANGELOG.md | 2 + packages/sdui-parser/package.json | 2 +- .../services/service-analytics/CHANGELOG.md | 505 ++++ .../services/service-analytics/package.json | 2 +- .../services/service-automation/CHANGELOG.md | 277 ++ .../services/service-automation/package.json | 2 +- packages/services/service-cache/CHANGELOG.md | 63 + packages/services/service-cache/package.json | 2 +- .../service-cluster-redis/CHANGELOG.md | 62 + .../service-cluster-redis/package.json | 2 +- .../services/service-cluster/CHANGELOG.md | 62 + .../services/service-cluster/package.json | 2 +- .../services/service-datasource/CHANGELOG.md | 158 + .../services/service-datasource/package.json | 2 +- packages/services/service-i18n/CHANGELOG.md | 110 + packages/services/service-i18n/package.json | 2 +- packages/services/service-job/CHANGELOG.md | 63 + packages/services/service-job/package.json | 2 +- .../services/service-knowledge/CHANGELOG.md | 62 + .../services/service-knowledge/package.json | 2 +- .../services/service-messaging/CHANGELOG.md | 154 + .../services/service-messaging/package.json | 2 +- .../services/service-package/CHANGELOG.md | 65 + .../services/service-package/package.json | 2 +- packages/services/service-queue/CHANGELOG.md | 63 + packages/services/service-queue/package.json | 2 +- .../services/service-realtime/CHANGELOG.md | 63 + .../services/service-realtime/package.json | 2 +- .../services/service-settings/CHANGELOG.md | 200 ++ .../services/service-settings/package.json | 2 +- packages/services/service-sms/CHANGELOG.md | 114 + packages/services/service-sms/package.json | 2 +- .../services/service-storage/CHANGELOG.md | 132 + .../services/service-storage/package.json | 2 +- packages/spec/CHANGELOG.md | 2620 +++++++++++++++++ packages/spec/package.json | 2 +- packages/triggers/trigger-api/CHANGELOG.md | 62 + packages/triggers/trigger-api/package.json | 2 +- .../trigger-record-change/CHANGELOG.md | 62 + .../trigger-record-change/package.json | 2 +- .../triggers/trigger-schedule/CHANGELOG.md | 62 + .../triggers/trigger-schedule/package.json | 2 +- packages/types/CHANGELOG.md | 115 + packages/types/package.json | 2 +- packages/verify/CHANGELOG.md | 163 + packages/verify/package.json | 2 +- 153 files changed, 12894 insertions(+), 76 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index a787163dab..c830a4ac75 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -93,7 +93,9 @@ "action-governance-engine-owned", "action-location-dual-source-c17", "action-no-placement-lint", + "action-param-builtin-near-miss-hint", "action-param-inline-lookup-reference", + "action-param-option-visible-when", "action-param-strict-unknown-keys", "action-record-write-discarded-lint", "action-record-writes-runtime-report", @@ -102,6 +104,7 @@ "action-session-positions-runtime-dual-emit", "action-strict-envelope-zero", "action-undoable-liveness-corrected", + "action-visible-disabled-unified-condition", "actions-empty-object-segment-route", "actions-failures-speak-http", "actions-global-key-and-failure-status", @@ -170,6 +173,7 @@ "adr-0119-d2-migration-journal", "adr-0119-plugin-reachable-transactions", "adr-0121-endpoint-routing-namespace", + "adr-0122-phase-2-bare-name-flip", "adr-anchors-authz-sweep", "adr-anchors-guard", "adr0104-attest-after-boot-writes", @@ -182,7 +186,9 @@ "agents-releases-freeze-merge-queue", "agents-spec-generated-artifacts-map", "aggregate-bulk-dispatch-selected-ids", + "aggregate-driver-query-and-alias-retirement", "aggregate-temporal-output", + "aggregation-array-string-agg-removed", "aggregation-vocabulary-lockstep", "ai-agent-authoring-and-tools-removal", "ai-agents-envelope-declaration", @@ -207,6 +213,7 @@ "analytics-dataset-degradation-envelope-first", "analytics-dataset-refusal-envelope", "analytics-dimension-source-field-gate", + "analytics-dotted-measure-refusal", "analytics-echo-startswith-endswith", "analytics-effective-granularity", "analytics-empty-combinator-identity", @@ -220,6 +227,7 @@ "analytics-filter-value-type-fidelity", "analytics-label-read-scope", "analytics-like-escape", + "analytics-missing-column-hard-failure", "analytics-notcontains-inert-predicate", "analytics-null-comparand-predicate", "analytics-objectql-read-scope", @@ -276,11 +284,13 @@ "approver-live-record-3447", "approver-value-sources-and-dead-slot-warning", "apps-mdx-retired-version-mobile-navigation", + "archive-keep-prune-abort-leg", "area-prescription-both-trees", "array-form-triggertype-not-silent", "attachment-read-visibility-real-filter-semantics", "attachment-unscoped-multi-delete", "audit-anchor-and-lookup-integrity", + "audit-hook-exclude-objects-registration", "audit-provenance-and-import-vocabulary", "audit-provisioning-datasource-audible", "audit-skip-sys-job-queue", @@ -310,7 +320,9 @@ "automation-client-resume-screen-flow", "automation-resume-authority-gate", "autonumber-runtime-owned-write-path", + "autonumber-seed-suffix-parse", "banner-dsn-connection-display", + "bare-path-parent-prefix-inside-link", "batch-create-readonly-ingress", "batch-dropped-fields-observability", "batch-row-error-codes-registered", @@ -329,8 +341,10 @@ "break-glass-standing-revocation-guard", "builtin-node-config-reconciliation", "bulk-action-option-passthrough", + "bulk-action-required-permissions", "bulk-batch-size-cap", "bulk-data-event-contract", + "bulk-write-before-hook-per-row-contract", "bulk-write-per-row-hook-semantics", "bulk-write-previous-hook-condition-diagnostic", "bulk-write-record-not-found", @@ -338,9 +352,12 @@ "calendar-day-primitive-to-spec", "calendar-day-upper-bound-memory-mongodb", "calendar-day-upper-bound", + "calldata-batch-arm-retired", "calldata-query-fallback-serves-query", "calldata-record-not-found-unify", "capabilities-registry-provenance-seam", + "capability-metadata-kind-registry-entry", + "cel-parse-fault-kind", "changelog-ships-in-tarball", "chart-drilldown-declared", "chart-drilldown-navigate-target", @@ -368,6 +385,8 @@ "client-actions-surface", "client-delete-result-success", "client-error-envelope-normalisation", + "client-find-canonical-only-key-predicate", + "client-find-pagination-presence", "client-keys-sharelinks-security", "client-meta-automation-descriptors", "client-meta-getitem-saveitem-return-types", @@ -387,16 +406,22 @@ "cluster-peer-write-invalidates-caches", "code-only-metadata-types-refused-everywhere", "cold-boot-flow-bind-read-decorations", + "collection-operator-scalar-comparand-400", + "compare-to-bucket-alignment", + "comparison-operator-string-comparand", + "compose-stacks-i18n-single-valued", "compose-stacks-no-silent-key-loss", "config-resolution-email-example", "connector-action-declared-effect", "connector-authoring-guide", "connector-degrade-cause", "connector-descriptors-meet-their-contract", + "connector-jsdoc-retirement-drift", "connector-rate-limit-config-removed", "connector-reconcile-fail-cause", "connector-template-cluster-removed", "console-09c6a177bb4a", + "console-0cf8f0f70d10", "console-1bb77aa24514", "console-2cb8d78e24ad", "console-4a4829d0ef39", @@ -424,9 +449,11 @@ "cross-object-batch-501-code", "cross-repo-issue-closer", "current-user-endpoints-kernel-resolver", + "current-user-scope-root", "d12-fake-inventory-gate", "dangling-audit-provenance-bucket", "dangling-audit-unscanned-bucket", + "dashboard-service-gate-default-path", "dashboard-strict", "dashboard-widget-action-aria-removed", "dashboard-widget-compareto-converged", @@ -452,12 +479,15 @@ "datasource-external-inert-keys-retired", "datasource-health-check-retired", "datasource-mapping-is-routing", + "datasource-memory-pool-loud-reject", "datasource-read-replicas-removed", "datasource-retry-policy-retired", "datasource-routes-catch-service-throws", "datasource-sqlite-pool-loud-reject", "datasource-teardown-ownership", + "datasource-unbuilt-workspace-remedy", "datasync-conflict-dual-source-c13-c15", + "date-bucket-parity-call-sites-drop-as-never", "date-bucket-parity-gate", "date-now-default-utc", "datetime-canonical-utc-storage", @@ -477,6 +507,7 @@ "degraded-boot-stderr-premise", "degraded-list-cache-policy", "degraded-register-cause", + "degraded-suspended-run-load-log-cause-meta", "delegable-scope-read-surface", "delete-fallback-success-shape", "delete-many-id-predicate", @@ -491,6 +522,8 @@ "dev-watcher-restart-honesty", "discovery-cache-queue-job-no-route", "discovery-data-slot-computed", + "discovery-environment-fold-exhaustive", + "discovery-environment-single-default", "discovery-metadata-slot-computed", "discovery-node-env-unset-production", "discovery-remedy-names-a-real-package", @@ -519,10 +552,14 @@ "docs-drift-skip-test-files", "docs-extend-faq-reversed-rot", "docs-fieldschema-extend-rot", + "docs-gen-bare-path-in-parens", + "docs-gen-description-line-layout-and-nested-links", + "docs-gen-inline-enum-width", "docs-gen-module-description-selection", "docs-gen-nested-brace-escape", "docs-gen-numeric-literal-quoting", "docs-gen-retired-key-never", + "docs-gen-same-dir-bare-path", "docs-index-category-keyed", "docs-plugin-spec-onupgrade-example", "docs-v17-plugin-retirement-notes", @@ -543,6 +580,8 @@ "driver-connect-bound-and-reconnect-correction", "driver-double-registration-log", "driver-options-bypass-tenant-audit", + "driver-owned-query-methods-driver-query", + "driver-query-signatures-follow-through", "driver-sql-logicalop-retention-note", "driver-sql-or-branch-and-semantics", "drivers-dir-and-turso-migration", @@ -562,6 +601,7 @@ "durability-gate-savemetaitem", "durable-suspended-screen-refetch", "eighty-donkeys-repeat", + "eighty-jars-tickle", "eighty-months-obey", "email-config-runtime-read-keys", "email-durable-queue-delivery", @@ -593,12 +633,18 @@ "engine-filter-fold-every-method", "engine-rejects-unknown-option-keys", "engine-rejects-wire-only-aliases", + "engine-residual-log-splice-sweep", + "engine-suspended-run-store-log-cause", + "engine-update-by-id-payload-id-strip", "engine-update-dispatch-predicate", + "engine-update-multi-payload-id-strip", "engines-node-22", "env-settings-option-table-gate", "envelope-violations-predicate", "environment-artifact-dual-source-c10", + "error-code-ledger-overlay-persistence-retired", "etl-author-shape-aliases", + "etl-pipeline-layer-retired", "event-schema-dual-source-c6", "executor-contract-surface-e1", "export-axis-opt-in", @@ -609,6 +655,7 @@ "external-datasource-400s-answer-their-own-code", "fault-edge-guard-containment", "fault-edge-label-lint", + "fault-edge-tombstone-type", "field-conditional-required-fold", "field-expression-check-activated", "field-file-collection-dogfood-proof", @@ -617,6 +664,7 @@ "field-readonly-doc-preserveaudit", "field-strict-guidance", "field-time-canonical-storage", + "fieldrule-no-current-user", "file-access-delegate", "filewatch-degraded-not-announced", "filter-app-areas-nav-gate", @@ -646,6 +694,7 @@ "flow-executors-parse-config", "flow-filter-collapse-and-write-path-tokens", "flow-function-declared-effect", + "flow-function-lowered-declaration", "flow-lint-loop-body-descent", "flow-lookup-expand", "flow-max-retries-single-default", @@ -654,6 +703,7 @@ "flow-node-expression-ledger", "flow-node-type-audit-at-boot-close", "flow-node-write-set-lint", + "flow-org-override-rolled-back", "flow-run-summaries", "flow-runas-unscoped-region-descent", "flow-runas-unscoped-two-way-true-wording", @@ -664,7 +714,9 @@ "flow-time-relative-descriptor-lint", "flow-trigger-unknown-event-lint", "flow-unbounded-bulk-write-lint", + "flownode-parses-its-regions", "form-layout-lint-wired", + "form-layout-view-container-ladder", "form-section-pane", "format-type-union-array-brackets", "format-zod-error-union-branches", @@ -676,6 +728,7 @@ "gentle-buttons-shave", "gentle-pumas-repeat", "getreadfilter-controlled-by-parent", + "getuiview-slim-body", "govern-remaining-nine-metadata-types", "govern-report-dashboard-liveness", "govern-sys-member-writes", @@ -684,15 +737,20 @@ "group-key-read-shape", "group-union-driver-scope", "guard-refusal-chokepoint", + "handwritten-errmap-fix-before-history", "has-is-not-a-null-guard-lint", + "having-filter-null-safe-negative-operators", "health-monitor-timeout-guard-cleared", "hierarchy-scope-organization-authority", + "hierarchy-scope-posture-signal", + "hip-otters-smash", "historical-import-audit-docs", "homepageid-tombstone-premise-corrected", "honest-service-self-description", "hono-adapter-handler-throw-logging", "hono-current-user-endpoints-exported", "hono-current-user-endpoints-ungated", + "hono-current-user-position-grants", "hono-retire-standard-endpoints", "hono-standalone-discovery-computed", "hono-standard-endpoints-default-off", @@ -702,10 +760,15 @@ "hook-condition-fail-loud", "hook-condition-merged-record", "hook-condition-previous-binding", + "hook-context-api-scoped-context", "hook-context-session-positions-preserve-audit", "hook-context-session-roles-retired", + "hook-ctx-doc-alias-reads-removed", "hook-empty-target-not-wildcard", + "hook-exclude-objects-registration-face", + "hook-input-options-before-phase", "hook-logger-contract-shape", + "hook-positions-reachable-example", "hook-vs-flow-path-guidance", "host-app-resolver-shared", "host-declared-package-resolution", @@ -716,6 +779,7 @@ "http-request-dual-source-c11", "http-server-config-retired", "http-server-exemption-revoked", + "http-server-runtime-vocabulary-retired", "hungry-donkeys-repeat", "i18n-bundle-drift-sweep", "i18n-consolidate-success-builder", @@ -725,6 +789,7 @@ "i18n-field-labels-emit-declared-shape", "i18n-field-labels-shared-nested-derivation", "i18n-gate-declared-labels", + "i18n-label-union-and-tab-translations", "i18n-sso-scim-userposition-importjob-coverage", "i18n-success-envelope-conformance", "i18n-translate-platform-bundles", @@ -733,11 +798,13 @@ "i18n-view-coverage", "i18n-walker-object-sections", "ihttpserver-contract-codify", + "import-dryrun-asks-for-the-verdict", "import-historical-audit", "import-historical-fsm", "import-sanitize-row-errors", "import-undo-preserveaudit", "inbound-rate-limit-seam", + "inbox-list-result-unread-count-jsdoc", "index-drift-migrate-plan", "index-type-partial-removed", "inert-rule-warn-dedupe", @@ -747,6 +814,7 @@ "init-service-declared-enforced", "injected-system-columns-author-time", "inline-action-schema", + "inline-shape-depth-budget", "invitation-accepted-host-seam", "io-node-config-reconciliation", "isLikelyEmail-no-control-char", @@ -769,18 +837,23 @@ "last-admin-delete-break-glass", "last-admin-guard-permission-set-row", "layered-read-outage-is-not-no-overlay", + "lazy-buttons-invite", "lazy-deps-dist-probe-timeout", "ledger-readability-leaves-the-posture-gate", "lifecycle-archive-abort-check", "lifecycle-event-registry-enforced", + "lifecycle-reclaim-rotation-abort-legs", "lifecycle-retention-floor", "lifecycle-unguarded-reap-batching", + "light-berries-tickle", "lint-action-dedup-composite-key", "lint-expressions-security-alias-reads", "lint-fieldless-object-skip", "lint-flag-record-change-trap", + "lint-missing-name-field-reads-name-field", "lint-never-fire-family-gates", "lint-null-guards-canonical-parse", + "lint-primaryfield-phantom-key-removed", "lint-reference-integrity-suite", "lint-rule-compilability-alias-read", "lint-searchable-fields-type-validation", @@ -789,6 +862,9 @@ "lint-translatable-sections", "lint-translation-reference-integrity", "lint-unique-double-declaration", + "lint-views-key-runtime-identity", + "lint-visibility-bare-identifier-gate", + "lint-visibility-predicate-syntax-gate", "list-column-prefix-summary-object", "list-single-flight", "listcommits-outage-503", @@ -802,11 +878,15 @@ "liveness-verified-at-clock", "liveness-widget-drill-and-container-coverage", "load-meta-hydration-benign-predicate", + "localization-value-domain-enforced", "localized-field-validation-messages", "lock-gate-fail-closed", "logger-redact-word-boundary", "lost-audit-row-is-an-error", "loud-pausing-resume-authority", + "lucky-moons-smoke", + "lucky-pandas-repeat", + "lucky-pears-arrive", "mail-provider-options-honest", "manifest-bridge-arm-on-project-kernels", "manifest-read-says-which-null", @@ -818,6 +898,7 @@ "marketplace-rehydrate-seed-heal", "marketplace-seed-test-budget", "mcp-discovery-service-aware", + "mcp-metadata-outage-vs-miss", "measure-emits-what-it-declares", "membership-grade-not-capability-channel", "memory-datasource-ephemeral-per-pool", @@ -827,9 +908,12 @@ "merge-driver-worktree-independent", "messaging-declares-its-event-object", "messaging-outbox-no-updated-at-on-update", + "meta-bind-unregistered-kinds", "meta-canonical-type-segment", "meta-migrate-stored-route", "meta-overlay-write-through-dispatch", + "meta-plural-audience-gate-bypass", + "meta-read-audit-field-governance", "meta-type-gate-plural", "metadata-event-contract", "metadata-event-dual-source-kernel-side", @@ -839,16 +923,19 @@ "metadata-loader-save-contract", "metadata-remove-artifact-api-source", "metadata-store-outage-is-not-a-miss", + "metadata-stored-envelope-body-split", "metadata-type-registration-names-a-real-hook", "metadata-unresolvable-posture-fail-closed", "metadata-watch-event-canonical-enum", "metadataformat-cachestrategy-single-declaration", + "migrate-json-exit-code", "migrate-meta-stored-rewrite", "migrate-occupancy-and-deferred-ddl", "migrate-occupancy-file-descriptor-signal", "migrate-plan-lists-datetime-convergence", "migrate-search-companion-parity", "migration-journal-boot-recovery", + "mixed-wrapper-refusal", "modal-actions-are-client-only", "mongodb-boolean-identity-reduction", "mongodb-single-tenant-boot-guard", @@ -860,7 +947,10 @@ "node-and-shutdown-timeout-guards-cleared", "notification-action-embed-config-retired", "notification-dual-source-c3", + "notification-mark-all-read-full-sweep", "notification-orphan-template-schemas", + "notification-retirement-evidence-corrected", + "notification-unread-count-true-total", "notifications-redos-fix", "notify-source-shape-conversion", "now-default-token-engine-resolved", @@ -868,6 +958,7 @@ "null-guard-surface-coverage", "null-safe-org-unique-driver", "object-enable-trash-mru-migration-surface", + "object-form-repeater-data-binding", "object-parse-path-strict", "object-subblock-strictness-batch20", "objectchart-aggregate-result-columns", @@ -883,6 +974,8 @@ "objectui-pin-freshness-gate", "objectui-range-from-declarations", "olive-donkeys-repeat", + "olive-hounds-repeat", + "olive-moons-repeat", "olive-pugs-repeat", "olive-pugs-sniff", "openapi-builtin-routes-rest-owned", @@ -893,10 +986,12 @@ "org-axis-dead-alias-branches", "org-axis-red-lines-read-spec-keys", "org-create-effective-posture-gate", + "org-scoped-cold-boot-audit", "osv-batch-2026-07-dep-bumps", "osv-batch-2026-08-fixable-bumps", "osv-exemption-conventions", "otp-sms-quota-429", + "owning-business-unit-injected-jsdoc", "owning-business-unit-injection", "package-dependency-dual-source-c7", "packages-envelope-suite-comment", @@ -956,6 +1051,7 @@ "published-pm-dispatch-skill", "published-pm-dispatch-three-axis-decision-frame", "purge-webhook-delivery-i18n-and-bundle-ownership-guards", + "qa-protocol-category-title", "query-ast-inert-request-surface", "query-cursor-removed", "query-distinct-removed", @@ -965,6 +1061,7 @@ "quiet-ledgers-speak-up", "rare-jars-shave", "rate-limit-config-dual-source-c9", + "rate-limit-storage-subpath-export", "rbac-objects-bulk-primitive", "react-block-field-props-lint", "react-blocks-declaration-parity-not-conformance", @@ -974,13 +1071,16 @@ "react-page-props-joins-the-suite", "react-tier-record-blocks-withdrawn", "read-diagnostics-union-branches", + "read-scope-boolean-flag-comparand-refusal", "read-scope-not-null-safe-and-empty", "read-scope-null-safe-negative-operators", + "read-scope-undefined-comparand-refusal", "readme-fde-audience", "readonly-flow-write-json-warning", "readonly-flow-write-lint", "readonly-flow-writes-joins-the-suite", "readonly-strip-actionable-warning", + "readonly-when-total-record", "ready-probe-driver-health", "reap-guard-intersection-composition", "reconcile-ledger-in-ci", @@ -993,12 +1093,14 @@ "recorded-by-nullable-lookup", "recursive-schema-input-pins", "recursive-schema-input-types", + "reference-cell-width-enum-relocation-variant-cap", "reference-id-embedded-record", "reference-integrity-object-and-action-names", "reference-integrity-wiring-guard", "refuse-out-of-contract-filter-input", "refused-capability-declaration-hole", "regenerate-ui-action-reference-doc", + "regex-retirement-icontains-drivers", "region-metadata-parity", "region-slots-single-declaration", "region-validator-coverage", @@ -1018,11 +1120,13 @@ "report-ordering-and-time-axis-default", "request-body-schema-validation", "required-decision-outputs", + "required-when-parent-scope", "resolve-service-returns-its-contract", "rest-4xx-message-truncate-not-replace", "rest-5xx-message-withheld", "rest-actions-type-dispatch", "rest-env-resolution-kernel-resolver-seam", + "rest-exec-ctx-principal-kind", "rest-expected-4xx-not-logged-as-unhandled", "rest-list-implicit-filter-and-merge", "rest-list-malformed-filter-rejected", @@ -1034,8 +1138,10 @@ "rest-server-openapi31-block-removed", "rest-unclassified-error-server-fault-status", "rest-union-branch-field-errors", + "restore-version-package-scope", "resume-gate-map-chain-and-reserved-vars", "resume-signal-chokepoint", + "resume-store-unreachable-log-cause-meta", "retire-activation-events", "retire-batch-validate-only", "retire-data-engine-batch", @@ -1065,6 +1171,7 @@ "route-envelope-four-more-modules", "route-envelope-guard-dispatcher-domains", "route-ledger-audit-guard", + "route-ledger-response-schema", "rpc-alias-precedence-one-fold", "rule-compilability-publish-gate", "rule-id-barrel-export-gap", @@ -1099,10 +1206,12 @@ "schemaless-node-config-contracts", "schemaless-node-expression-ledger", "scim-provider-key-and-sso-scim-parity", + "scoped-context-transaction-ambient-join", "scoped-invitation-placement", "screen-field-visible-when-on-the-wire", "screen-resume-declared-field-contract", "script-branch-keys-retired", + "sdui-component-props-enforce-or-remove", "sdui-component-props-gate", "searchable-fields-stale-declaration", "security-get-readable-fields", @@ -1138,11 +1247,16 @@ "service-storage-success-envelope", "session-dual-source-c4", "session-of-record-is-sys-session", + "settings-declared-step-grid-enforced", + "settings-declared-value-window-enforced", "settings-error-details-declared-slot", "settings-select-options-enforced", + "share-link-enforcement-full-envelope", "share-link-record-existence", + "share-link-route-full-envelope", "share-link-routes-envelope", "share-links-dispatcher-dual-key", + "shared-filter-verdict-reduction", "shared-response-envelope-builders", "sharing-access-level-full-removed", "sharing-hierarchy-org-authority", @@ -1152,6 +1266,7 @@ "sharing-rule-unknown-sort-and-stale-help", "sharing-rule-unlowerable-condition-gate", "sharing-rule-withdrawal-and-delete", + "sharing-write-verdict-tristate", "showcase-action-disabled-specimen", "showcase-approver-and-picker-specimens", "showcase-bulk-actions-example", @@ -1163,6 +1278,7 @@ "showcase-legacy-rowactions-specimen", "showcase-nav-affordance-specimen", "showcase-register-contact-views", + "silent-empty-predicate-comparands", "single-delete-binds-previous", "sink-engine-dispatch-to-metadata-core", "skill-formula-condition-abort-scope", @@ -1190,10 +1306,13 @@ "spec-property-retirement-skill", "spec-type-alias-parsed-convention", "spec-vitest-testtimeout", + "specifier-value-domain", + "sql-distinct-bare-filter-condition", "sql-driver-boolean-identity", "sql-driver-dialect-connect-timeout", "sql-driver-not-null-safe", "sql-driver-null-safe-negative-operators", + "sql-driver-own-query-doors", "sqlite-datetime-date-bucket", "sqlite-wal-journal-mode", "sso-register-gate-one-admin-grade-ruler", @@ -1201,6 +1320,8 @@ "stall-forensics-and-kernel-test-hygiene", "stall-guard-rollout", "stall-guard-self-test", + "standalone-stack-libsql-dispatch", + "standalone-stack-mysql-and-driver-validation", "standard-endpoints-parity-correction", "standard-endpoints-precedence-pin", "startup-log-noise-cleanup", @@ -1218,6 +1339,7 @@ "strict-automation-control-flow-state-machine", "strict-object-registered-types", "strict-readonly-writes", + "strict-unknown-key-history-last", "strictness-ledger-covered-verdict", "strictness-ledger-gate", "strictness-ledger-numbers-prose-split", @@ -1233,12 +1355,14 @@ "sys-comment-record-level-authorization", "sys-comment-retire-visibility-reply-count", "sys-email-headers-attachments", + "sys-file-hydrate-fault-warn", "sys-metadata-repo-history-counters-loud", "sys-migration-ledger-platform-infra", "sys-secret-store-platform-infra", "sys-view-definition-default-open", "system-data-import-opt-in", "system-field-name-injected-columns", + "systemfields-owner-guidance-org-skips-owner-id", "temporal-conformance-driver-axis", "temporal-conformance-matrix", "temporal-conformance-stall-guard", @@ -1259,7 +1383,10 @@ "theme-inert-token-scales-removed", "thick-pumas-judge", "tidy-donkeys-yawn", + "tidy-drivers-aggregate-refusal-envelope", "tidy-eyes-shine", + "tidy-pugs-tap", + "tombstone-age-exact-key", "tombstone-agent-tools", "tool-inert-keys-removed", "tool-requires-confirmation-not-enforced", @@ -1268,8 +1395,10 @@ "transaction-degrade-and-cross-datasource-observability", "transaction-same-origin-audit-carve-out", "translation-groups-strict", + "translation-pages-components-facet", "translation-refs-container-default-form-sections", "trigger-registry-connector-cluster-removed", + "turso-aggregate-case-normalization", "turso-remote-canonical-temporal-backfill", "turso-remote-node-operator-refusal", "turso-remote-null-safe-negation", @@ -1284,11 +1413,13 @@ "ui-discovery-reads-the-protocol-service", "ui-interaction-config-family-retired", "ui-vocabularies-derive-not-restate", + "ui-widget-i18n-family-retired", "undefined-comparand-refusal", "unified-capability-vocabulary", "unique-scope-lint-rules", "unique-scope-organization-vocabulary", "unique-tenant-scoped-materialization", + "unique-violation-shared-predicate", "unknown-authoring-key-lint", "unknown-key-lint-nested-descent", "unknown-key-strictness-automation-batch11", @@ -1329,6 +1460,7 @@ "v17-verification-defects-docs", "v17-watch-event-raw-values-note", "vacuous-filter-carveouts", + "validate-only-data-protocol", "validate-runs-build-authoring-lints", "validation-kind-retired", "validation-variants-strict", @@ -1338,11 +1470,15 @@ "verify-harness-durable-suspended-runs", "verify-multitenant-requests-isolated-posture", "view-ast-operator-parity", + "view-definition-active-row-unique", "view-filter-rule-console-id-hotfix", "view-label-resolve-served-shape", + "view-management-protocol-retired", "view-strict-final", "view-subblock-strictness-batch18", + "view-union-identity-precondition", "views-translation-key-runtime-identity", + "visibility-alias-deprecated-retired", "wait-loose-config-graduation", "wait-node-log-cause-meta", "wait-timeout-keys-retired", @@ -1354,8 +1490,10 @@ "webhook-dual-source-api-side", "webhook-liveness-ledger-flip", "webhooks-drop-dead-delivery-i18n", + "wide-donkeys-repeat", "widget-contract-performanceconfig", "widget-contract-theme-token-vocabulary", + "wild-pugs-clap", "wildcard-fallthrough-guard", "withdraw-adr-0107-drop-writes-proposal", "workflow-slot-retired", diff --git a/examples/app-crm/CHANGELOG.md b/examples/app-crm/CHANGELOG.md index 9f22465496..8c734dc80f 100644 --- a/examples/app-crm/CHANGELOG.md +++ b/examples/app-crm/CHANGELOG.md @@ -1,5 +1,74 @@ # @objectstack/example-crm +## 4.0.92-rc.5 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [f7d80f4] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [db59e9c] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [c51ffa5] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [1fa224a] +- Updated dependencies [35f7fb4] +- Updated dependencies [f8fe47e] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [cca11e9] +- Updated dependencies [cfb549d] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/runtime@17.0.0-rc.6 + ## 4.0.92-rc.4 ### Patch Changes diff --git a/examples/app-crm/package.json b/examples/app-crm/package.json index bb3870cd9e..c3b4de47e2 100644 --- a/examples/app-crm/package.json +++ b/examples/app-crm/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-crm", - "version": "4.0.92-rc.4", + "version": "4.0.92-rc.5", "description": "Minimal CRM example — a smoke-test workspace that exercises the metadata loading pipeline (objects → views → app → dashboard → hook → flow → 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-showcase/CHANGELOG.md b/examples/app-showcase/CHANGELOG.md index b78f01e96c..ffe3970121 100644 --- a/examples/app-showcase/CHANGELOG.md +++ b/examples/app-showcase/CHANGELOG.md @@ -1,5 +1,91 @@ # @objectstack/example-showcase +## 0.3.14-rc.5 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [29e28a3] +- Updated dependencies [259459d] +- Updated dependencies [6f6fec7] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [f7d80f4] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [01faeb1] +- Updated dependencies [d92ed03] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d367f03] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [db59e9c] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [c51ffa5] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [1fa224a] +- Updated dependencies [35f7fb4] +- Updated dependencies [82397b6] +- Updated dependencies [f8fe47e] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [3264516] +- Updated dependencies [2fdb36e] +- Updated dependencies [62159bd] +- Updated dependencies [d48aad5] +- Updated dependencies [cca11e9] +- Updated dependencies [cfb549d] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [939f579] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/driver-sql@17.0.0-rc.6 + - @objectstack/runtime@17.0.0-rc.6 + - @objectstack/service-datasource@17.0.0-rc.6 + - @objectstack/cloud-connection@17.0.0-rc.6 + - @objectstack/connector-mcp@17.0.0-rc.6 + - @objectstack/connector-openapi@17.0.0-rc.6 + - @objectstack/connector-rest@17.0.0-rc.6 + - @objectstack/connector-slack@17.0.0-rc.6 + ## 0.3.14-rc.4 ### Patch Changes diff --git a/examples/app-showcase/package.json b/examples/app-showcase/package.json index 44b145cfe0..f47e99981c 100644 --- a/examples/app-showcase/package.json +++ b/examples/app-showcase/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-showcase", - "version": "0.3.14-rc.4", + "version": "0.3.14-rc.5", "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 1cef736502..d0f2bcc4bd 100644 --- a/examples/app-todo/CHANGELOG.md +++ b/examples/app-todo/CHANGELOG.md @@ -1,5 +1,107 @@ # @objectstack/example-todo +## 4.0.92-rc.5 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [ad878e7] +- Updated dependencies [6f6fec7] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [f7d80f4] +- Updated dependencies [ae31a19] +- Updated dependencies [ec3dfd7] +- Updated dependencies [466c503] +- Updated dependencies [10c4ea9] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d367f03] +- Updated dependencies [9c82b89] +- Updated dependencies [b7d3be4] +- Updated dependencies [2a0d65e] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [f09a2e7] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [db59e9c] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [53ef057] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [c804f19] +- Updated dependencies [c51ffa5] +- Updated dependencies [dbe92a7] +- Updated dependencies [5c2716b] +- Updated dependencies [2f59da0] +- Updated dependencies [1a53a02] +- Updated dependencies [7c6261a] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [bfe689b] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [1fa224a] +- Updated dependencies [3fb42d2] +- Updated dependencies [35f7fb4] +- Updated dependencies [82397b6] +- Updated dependencies [9bc846b] +- Updated dependencies [f8fe47e] +- Updated dependencies [a841151] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [62159bd] +- Updated dependencies [cca11e9] +- Updated dependencies [cfb549d] +- Updated dependencies [761a0ba] +- Updated dependencies [d86815e] +- Updated dependencies [be87153] +- Updated dependencies [2bd4e5e] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] +- Updated dependencies [c9bf940] +- Updated dependencies [a682670] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/objectql@17.0.0-rc.6 + - @objectstack/runtime@17.0.0-rc.6 + - @objectstack/client@17.0.0-rc.6 + - @objectstack/driver-sqlite-wasm@17.0.0-rc.6 + - @objectstack/mcp@17.0.0-rc.6 + - @objectstack/metadata@17.0.0-rc.6 + - @objectstack/knowledge-memory@17.0.0-rc.6 + - @objectstack/service-knowledge@17.0.0-rc.6 + ## 4.0.92-rc.4 ### Patch Changes diff --git a/examples/app-todo/package.json b/examples/app-todo/package.json index aa24f5cae5..5ec693454c 100644 --- a/examples/app-todo/package.json +++ b/examples/app-todo/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-todo", - "version": "4.0.92-rc.4", + "version": "4.0.92-rc.5", "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 dc1a73e407..3f77928b85 100644 --- a/examples/embed-objectql/CHANGELOG.md +++ b/examples/embed-objectql/CHANGELOG.md @@ -1,5 +1,90 @@ # @objectstack/example-embed-objectql +## 0.0.32-rc.5 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [ad878e7] +- Updated dependencies [6f6fec7] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [10c4ea9] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [262e40d] +- Updated dependencies [d367f03] +- Updated dependencies [9c82b89] +- Updated dependencies [b7d3be4] +- Updated dependencies [2a0d65e] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [f09a2e7] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [53ef057] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [c804f19] +- Updated dependencies [dbe92a7] +- Updated dependencies [2f59da0] +- Updated dependencies [1a53a02] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [bfe689b] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [3fb42d2] +- Updated dependencies [35f7fb4] +- Updated dependencies [82397b6] +- Updated dependencies [9bc846b] +- Updated dependencies [a841151] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [d86815e] +- Updated dependencies [be87153] +- Updated dependencies [2bd4e5e] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] +- Updated dependencies [c9bf940] +- Updated dependencies [a682670] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/objectql@17.0.0-rc.6 + - @objectstack/driver-memory@17.0.0-rc.6 + ## 0.0.32-rc.4 ### Patch Changes diff --git a/examples/embed-objectql/package.json b/examples/embed-objectql/package.json index 2456c4b0bf..08a1fb65fe 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.32-rc.4", + "version": "0.0.32-rc.5", "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 61f4fa4af8..e6289d4637 100644 --- a/packages/adapters/hono/CHANGELOG.md +++ b/packages/adapters/hono/CHANGELOG.md @@ -1,5 +1,23 @@ # @objectstack/hono +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [f7d80f4] +- Updated dependencies [2a2a9fb] +- Updated dependencies [1fe436d] +- Updated dependencies [db59e9c] +- Updated dependencies [c51ffa5] +- Updated dependencies [1fa224a] +- Updated dependencies [f8fe47e] +- Updated dependencies [cca11e9] +- Updated dependencies [cfb549d] +- Updated dependencies [88f9d94] + - @objectstack/runtime@17.0.0-rc.6 + - @objectstack/plugin-hono-server@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/adapters/hono/package.json b/packages/adapters/hono/package.json index a136753e39..7d0a0961a9 100644 --- a/packages/adapters/hono/package.json +++ b/packages/adapters/hono/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/hono", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apps/account/CHANGELOG.md b/packages/apps/account/CHANGELOG.md index 37f80b891f..a684776cb6 100644 --- a/packages/apps/account/CHANGELOG.md +++ b/packages/apps/account/CHANGELOG.md @@ -1,5 +1,67 @@ # @objectstack/account +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/apps/account/package.json b/packages/apps/account/package.json index b17b4ee310..74c83de8b3 100644 --- a/packages/apps/account/package.json +++ b/packages/apps/account/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/account", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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).", "main": "dist/index.js", diff --git a/packages/apps/setup/CHANGELOG.md b/packages/apps/setup/CHANGELOG.md index ff5587556c..85e95a936c 100644 --- a/packages/apps/setup/CHANGELOG.md +++ b/packages/apps/setup/CHANGELOG.md @@ -1,5 +1,67 @@ # @objectstack/setup +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/apps/setup/package.json b/packages/apps/setup/package.json index d616507846..82593ba476 100644 --- a/packages/apps/setup/package.json +++ b/packages/apps/setup/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/setup", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "ObjectStack Setup — the platform administration app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", "main": "dist/index.js", diff --git a/packages/apps/studio/CHANGELOG.md b/packages/apps/studio/CHANGELOG.md index f8c9b0242b..a20a95049f 100644 --- a/packages/apps/studio/CHANGELOG.md +++ b/packages/apps/studio/CHANGELOG.md @@ -1,5 +1,67 @@ # @objectstack/studio +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/apps/studio/package.json b/packages/apps/studio/package.json index 888071fdc4..90be6f2db2 100644 --- a/packages/apps/studio/package.json +++ b/packages/apps/studio/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/studio", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "ObjectStack Studio — the metadata builder app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", "main": "dist/index.js", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 25c94d1bb9..050419c22b 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,290 @@ # @objectstack/cli +## 17.0.0-rc.6 + +### Minor Changes + +- a36db28: i18n label contract: inline locale maps are authorized, and filter-preset tabs get a translation slot + + **`I18nLabelSchema` accepts two forms, not one (#5728).** It declared a plain + `z.string()` while three published platform pages authored 31 inline + `{ en, 'zh-CN', 'ja-JP', 'es-ES' }` maps that objectui resolves through + `pickLocalized` — so the authoritative document was the wrong one, and the + #5068 component-props gate reported 42 findings against the platform's own + pages. The schema is now a union of the default-language string and an inline + locale map. `ElementTextPropsSchema.content` was declared a bare `z.string()` + and therefore out of that union's reach; it moves onto `I18nLabelSchema` in the + same change, which is the other 8 of the 42. The gate now reports **0**. + + This does not reverse #4667 / #5055. What those retired was the _key-reference_ + dialect (`{ key, defaultValue }`) — a shape with **no resolver**, whose label + reached the screen as a raw key or not at all. What is authorized here is the + inline locale map, which has a live resolver and which the CLI's `i18n-extract` + already understands. Same "declared = enforced" principle, applied in both + directions: the map's keys are constrained to BCP-47 tags (plus `default`), so + `{ key, defaultValue }` stays a parse error rather than becoming "a locale map + whose locales are named `key` and `defaultValue`". + + Zero breaking: every previously-valid label is still valid. The + translation-bundle channel remains the direction that scales and is unchanged. + + **Filter-preset tab labels are translatable (#5377).** `ObjectTranslationData` + gains `_tabs`, addressed by `ViewTabSchema.name`, and `resolveTabLabel` reads + it — explicit `_tabs` translation, then the referenced view's `_views.*.label` + for a tab that carries `view` (the path that already worked, preserved), then + the authored literal. A tab carrying only a `filter` referenced nothing to + inherit from and had no key of its own, so its label rendered in the source + language above a fully localized grid with no authoring workaround. `os i18n +extract` scaffolds the new keys, so the slot, the resolver and the extractor + land together. + + `I18nLabelSchema`'s description no longer claims "i18n keys are auto-generated + by the framework" — none are. `AriaPropsSchema.ariaLabel` now states that no + translation-bundle slot addresses it. + +### Patch Changes + +- 2b641dd: fix(cli): `--json` now owns stdout — kernel boot logs move to stderr (#6217) + + Every `os migrate` / `os meta` subcommand that boots a kernel wrote its + machine-readable payload into a stream it shared with ~60 INFO lines. The + kernel logger routes `debug`/`info`/`warn` to stdout and only `error`/`fatal` + to stderr, so `os migrate recorded-by --json | jq .` failed with `parse error: +Invalid numeric literal` while stderr sat completely empty — a `--json` flag + whose only audience is a program, handing that program something it cannot + parse. + + With this change, a `--json` run reserves stdout for its payload: everything + the kernel and its plugins write goes to **stderr** instead, including the + `[StandaloneStack] no compiled artifact …` notice that never went through the + logger at all. `JSON.parse()` now succeeds with no heuristic + extraction, and no diagnostic is lost — every line an operator used to see is + still printed, on the stream diagnostics belong on. + + Covers the whole family that shares the boot seam: `os migrate plan` / `apply` + / `resume` / `recorded-by` / `summary-nulls` / `value-shapes` / + `files-to-references`, `os migrate meta --stored`, and `os meta resync`. + Human-mode runs are unchanged. + +- 83df2fd: fix(cli): `os migrate --json` no longer exits with its own runtime as the status code (#4873) + + A **successful** `os migrate recorded-by --json` returned a different non-zero + exit code on every invocation — 208, 171, 176, 163, 62, 19, 48, 57 — while + printing correct JSON, printing `✅ Graceful shutdown complete`, and leaving + stderr completely empty. `os migrate resume --json` had it too. Nothing that an + author reads was wrong; the only thing that was wrong is the only thing a CI + step, a `set -e` script, a Makefile, or a container entrypoint reads. `--json` + exists for programs, and the first thing a program consumes is the exit status. + + **Root cause.** `emitJson(payload, exitCode, opts)` takes its exit code as the + second positional argument, and both commands were passing `timer.elapsed()` + there — a duration in milliseconds. So a run that took 531 ms set + `process.exitCode = 531`, and the shell saw `531 & 0xFF` = 19. The codes looked + random because they _were_ the run's duration, and no two runs take the same + number of milliseconds. + + It was not what it looked like from the outside: no native `abort` during + teardown, no libsql/sqlite handle, no `safeExit`, and not a leftover of #4813 + (whose 120-second hang is fixed and unrelated — the random codes predate and + survive it). + + **What changed.** + + - Both commands now report their duration where every other `--json` command in + this CLI already reports it — inside the payload, as `duration`. A successful + run exits `0`; a failing one still exits `1`, unchanged. + - `emitJson` / `emitText` narrow that parameter from `number` to + `CliExitCode = 0 | 1`, so handing a duration (or any other stray number) to + the exit-code slot is now a compile error instead of a silent false failure. + + **Payload change.** `os migrate recorded-by --json` and `os migrate resume +--json` gained a `duration` key (milliseconds). Consumers that were reading the + exit status of these two commands should note that a zero now means what it + says. + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [29e28a3] +- Updated dependencies [259459d] +- Updated dependencies [2bc1876] +- Updated dependencies [1d0faa7] +- Updated dependencies [ad878e7] +- Updated dependencies [0f8d16a] +- Updated dependencies [6f6fec7] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [f7d80f4] +- Updated dependencies [ae31a19] +- Updated dependencies [07c68b0] +- Updated dependencies [ec3dfd7] +- Updated dependencies [466c503] +- Updated dependencies [10c4ea9] +- Updated dependencies [8e2bbba] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [379b749] +- Updated dependencies [e9b5265] +- Updated dependencies [de6b7f1] +- Updated dependencies [01faeb1] +- Updated dependencies [d92ed03] +- Updated dependencies [6517448] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [262e40d] +- Updated dependencies [d367f03] +- Updated dependencies [9c82b89] +- Updated dependencies [24122a9] +- Updated dependencies [b0d54bf] +- Updated dependencies [b7d3be4] +- Updated dependencies [2a0d65e] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [01fd9e1] +- Updated dependencies [dba7747] +- Updated dependencies [bf0ae99] +- Updated dependencies [f09a2e7] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [1fe436d] +- Updated dependencies [59b794f] +- Updated dependencies [db59e9c] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [53ef057] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [465c5fc] +- Updated dependencies [c804f19] +- Updated dependencies [c51ffa5] +- Updated dependencies [a5ca08d] +- Updated dependencies [6ce10bd] +- Updated dependencies [7618ee8] +- Updated dependencies [6965160] +- Updated dependencies [ecff951] +- Updated dependencies [babddf6] +- Updated dependencies [dbe92a7] +- Updated dependencies [49f208b] +- Updated dependencies [5c2716b] +- Updated dependencies [2f59da0] +- Updated dependencies [83a3b1f] +- Updated dependencies [1a53a02] +- Updated dependencies [7c6261a] +- Updated dependencies [2604d34] +- Updated dependencies [f1850d8] +- Updated dependencies [eb91eba] +- Updated dependencies [17d0954] +- Updated dependencies [643b7c7] +- Updated dependencies [7e1b480] +- Updated dependencies [bfe689b] +- Updated dependencies [e39dd66] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [1fa224a] +- Updated dependencies [3cc8676] +- Updated dependencies [e15bf7e] +- Updated dependencies [3fb42d2] +- Updated dependencies [35f7fb4] +- Updated dependencies [82397b6] +- Updated dependencies [9bc846b] +- Updated dependencies [773f80a] +- Updated dependencies [2873eb9] +- Updated dependencies [72847c5] +- Updated dependencies [f8fe47e] +- Updated dependencies [a841151] +- Updated dependencies [85ec26d] +- Updated dependencies [4afdd3e] +- Updated dependencies [9566c38] +- Updated dependencies [d7e0b42] +- Updated dependencies [8e13ca8] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [3264516] +- Updated dependencies [2fdb36e] +- Updated dependencies [62159bd] +- Updated dependencies [d48aad5] +- Updated dependencies [cca11e9] +- Updated dependencies [cfb549d] +- Updated dependencies [761a0ba] +- Updated dependencies [d86815e] +- Updated dependencies [be87153] +- Updated dependencies [939f579] +- Updated dependencies [2bd4e5e] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [3d4c545] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] +- Updated dependencies [92e13a0] +- Updated dependencies [c9bf940] +- Updated dependencies [a682670] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/driver-sql@17.0.0-rc.6 + - @objectstack/objectql@17.0.0-rc.6 + - @objectstack/service-analytics@17.0.0-rc.6 + - @objectstack/plugin-audit@17.0.0-rc.6 + - @objectstack/runtime@17.0.0-rc.6 + - @objectstack/metadata-protocol@17.0.0-rc.6 + - @objectstack/formula@17.0.0-rc.6 + - @objectstack/client@17.0.0-rc.6 + - @objectstack/console@17.0.0-rc.6 + - @objectstack/lint@17.0.0-rc.6 + - @objectstack/rest@17.0.0-rc.6 + - @objectstack/service-datasource@17.0.0-rc.6 + - @objectstack/service-automation@17.0.0-rc.6 + - @objectstack/driver-memory@17.0.0-rc.6 + - @objectstack/driver-mongodb@17.0.0-rc.6 + - @objectstack/driver-sqlite-wasm@17.0.0-rc.6 + - @objectstack/plugin-hono-server@17.0.0-rc.6 + - @objectstack/service-storage@17.0.0-rc.6 + - @objectstack/plugin-sharing@17.0.0-rc.6 + - @objectstack/service-settings@17.0.0-rc.6 + - @objectstack/mcp@17.0.0-rc.6 + - @objectstack/metadata@17.0.0-rc.6 + - @objectstack/service-messaging@17.0.0-rc.6 + - @objectstack/plugin-auth@17.0.0-rc.6 + - @objectstack/service-sms@17.0.0-rc.6 + - @objectstack/verify@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/account@17.0.0-rc.6 + - @objectstack/setup@17.0.0-rc.6 + - @objectstack/cloud-connection@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/observability@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + - @objectstack/plugin-approvals@17.0.0-rc.6 + - @objectstack/plugin-email@17.0.0-rc.6 + - @objectstack/plugin-reports@17.0.0-rc.6 + - @objectstack/plugin-security@17.0.0-rc.6 + - @objectstack/plugin-webhooks@17.0.0-rc.6 + - @objectstack/service-cache@17.0.0-rc.6 + - @objectstack/service-job@17.0.0-rc.6 + - @objectstack/service-package@17.0.0-rc.6 + - @objectstack/service-queue@17.0.0-rc.6 + - @objectstack/service-realtime@17.0.0-rc.6 + - @objectstack/trigger-api@17.0.0-rc.6 + - @objectstack/trigger-record-change@17.0.0-rc.6 + - @objectstack/trigger-schedule@17.0.0-rc.6 + - @objectstack/plugin-pinyin-search@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 92fec90797..238b23c3f5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cli", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 9d5367d08b..3d22681f01 100644 --- a/packages/client-react/CHANGELOG.md +++ b/packages/client-react/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/client-react +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [ec3dfd7] +- Updated dependencies [466c503] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/client@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Major Changes diff --git a/packages/client-react/package.json b/packages/client-react/package.json index b484b2d31d..e2b6a1d410 100644 --- a/packages/client-react/package.json +++ b/packages/client-react/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client-react", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "React hooks for ObjectStack Client SDK", "main": "dist/index.js", diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md index c547a17fe1..bcd5165826 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -1,5 +1,218 @@ # @objectstack/client +## 17.0.0-rc.6 + +### Major Changes + +- f549a0d: refactor(spec,client)!: retire `ViewProtocol`'s five viewId-addressed methods and their ten schemas (#6239) + + `listViews`, `getView`, `createView`, `updateView` and `deleteView` — the + `ViewProtocol` interface and `ListViews`/`GetView`/`CreateView`/`UpdateView`/`DeleteView` + Request+Response schemas in `api/protocol.zod.ts` — are REMOVED under ADR-0049 + enforce-or-remove (maintainer ruling 2026-08-07). `@objectstack/client` drops the + five response types it re-exported. + + Measured on `origin/main` immediately before the removal, the surface had none of + the three things a protocol method needs: + + - **no implementation** — `packages/metadata-protocol/src/protocol.ts` declares no + `listViews`/`getView`/`createView`/`updateView`/`deleteView`; its only view + resolver is `getUiView`; + - **no route** — `packages/rest/src/rest-server.ts` never mentions `viewId`, so + nothing viewId-addressed was reachable over HTTP at all; + - **no caller** — the only `ViewProtocol` mention outside its own file was + `content/docs/kernel/services-checklist.mdx`, which already recorded the five as + declared-and-unrouted. + + FROM → TO — both replacements are surfaces that were always the live ones: + + | removed | use instead | + | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | + | `listViews` / `getView` / `createView` / `updateView` / `deleteView` (+ their 10 schemas) | the generic metadata methods with `type: 'view'` — `getMetaItem` / `getMetaItems` / `saveMetaItem` / `deleteMetaItem`, served at `/api/v1/meta/view/:name` | + | `GetViewResponse` as "the shape of the resolved view" | `GetUiViewResponse` — `getUiView`, served at `GET /api/v1/ui/view/:object/:type` | + + **The fix:** delete the import and address views by NAME through the metadata API + (`view` is a metadata type), or by object+type through `getUiView`. Nothing + addressed a view by `viewId` before this change either; that is the finding. + + **Why a removal rather than a note.** The declared surface is name-identical and + semantics-adjacent to a real one, which makes it an attractive nuisance in every + grep — and it has already mis-directed a decision: **#5948's issue body AND its + 2026-08-07 maintainer ruling both read `GetViewResponseSchema` (zero + implementations) as the contract of `GET /ui/view/:object/:type`**, whose declared + response is `GetUiViewResponseSchema`, 250 lines up and one word different. That + ruling's reasoning happened to survive the mix-up; this removal stops relying on + that luck. + + The retirement kit — route 3: **no tombstone and no D2 conversion** (none of the ten + was a key on an authorable shape, and nothing parsed them, so there is no source or + `sys_metadata` row to rewrite). `RETIRED_DEFS_BY_MAJOR[17]` (10 defs) plus the D3 + `SemanticMigration` `view-management-protocol-retired` are the declaration; the + generated baselines and reference docs lose their entries in the same change. + + If "read and write ONE view by id" becomes a real requirement, it returns + implementation-first. + + + +### Patch Changes + +- ec3dfd7: fix(client): `data.find({ limit })` reached the server as an empty query, and `QueryOptionsV2.expand` reached it as nothing at all (#6322) + + `data.find()` accepts two vocabularies — the canonical `QueryOptionsV2` + (`where` / `fields` / `orderBy` / `limit` / `offset` / `expand`) and the legacy + `QueryOptions` (`filter` / `select` / `sort` / `top` / `skip`) — and picked the + branch with a hand-written condition that named four keys: + `'where' in options || 'fields' in options || 'orderBy' in options || 'offset' in options`. + That condition was a second, independent statement of what `QueryOptionsV2` + declares, and it had fallen behind the interface twice. + + **`limit` was missing from it.** `client.data.find('task', { limit: 20 })` — a + canonical key as the only key, and the most natural spelling of "first 20" — + was not recognised as canonical, fell to the legacy branch, and that branch + reads only `top` / `skip` / `sort` / `select` / `filter` / `filters` / + `aggregations` / `groupBy`. Nothing there reads `limit`, so the value was + dropped between the call and the wire: the request went out with an **empty + query string**, the caller got the server's default page size, HTTP 200, no + warning. Its pagination twin `{ offset: 5 }` worked correctly, because `offset` + happened to be one of the four listed keys — one interface, two pagination + keys, opposite behaviour. + + **`expand` was missing too, and had no mapping either.** It is declared on + `QueryOptionsV2`, documented as the replacement for a legacy `populate` that + `QueryOptions` never had, and was carried by neither branch — not one character + of it reached the wire, on either of the two `find` implementations. + + **What changed.** The branch predicate is now derived from the interface rather + than restated beside it: the canonical-only key set is + `Exclude`, held as a + `Record<…, true>` that TypeScript rejects when a key is missing or extra. A key + added to `QueryOptionsV2` from now on is a compile error until it is listed, so + the next canonical key is covered on the day it is declared. Appending `limit` + to the old list would have been the third round of the same mistake. + + `expand` now maps onto the spelling the server actually accepts: + `?expand=`, which + `HttpFindQueryParamsSchema` declares for the GET list route and the protocol + normalizer splits on commas before folding each name into the engine's expand + map. The `Record` form contributes its keys — the same relation names the + server derives from the comma list. A **nested** per-relation query inside + `expand` has no spelling on a GET, so it is now refused with an error naming + the relation and the keys it could not carry, rather than trimmed away + silently; `data.query()` carries a QueryAST body and is where nested expand + detail belongs. + + Both `find` implementations — `ObjectStackClient.data.find` and + `ScopedProjectClient.data.find`, which were byte-identical copies of the same + defect — read the one shared predicate and the one shared `expand` mapping. + + No change to the five paired keys: canonical and legacy spellings of the same + query still produce byte-identical transport parameters, and that parity is now + pinned by a test table both implementations are driven through. + +- 466c503: fix(client): `data.find()` emits `top`/`skip` on presence, so `limit: 0` reaches the server (#6485) + + Both `find` implementations — `ObjectStackClient.data.find` and its + byte-identical `ScopedProjectClient.data.find` copy — emitted the two pagination + transport params on **truthiness**: + + ```ts + if (normalizedOptions.top) + queryParams.set("top", normalizedOptions.top.toString()); + if (normalizedOptions.skip) + queryParams.set("skip", normalizedOptions.skip.toString()); + ``` + + while the canonical normalizer ten lines above already tested **presence** + (`if (v2.limit != null) normalizedOptions.top = v2.limit`). So `0` survived the + normalizer and was then discarded by the emitter. Both now test presence, in + both copies. + + **What changes on the wire, and why that is the fix rather than a preference.** + `find('task', { limit: 0 })` — and equally `{ top: 0 }` — used to reach the + server with **no `top` param at all**. The GET list route has no default page + size, so an absent `top` returns the _entire_ match set: the caller who asked + for no records received every record, under HTTP 200 with no warning. + + The direction was measured before the change rather than assumed, because a + client fix is only worth having if the server honours what it sends: + + | layer | `top=0` | + | :------------------------------------------------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | REST list route → `ObjectStackProtocolImplementation.findData` | not rejected, not ignored — folds `top` into `limit`, coerces `Number('0')`, forwards `{ limit: 0 }` to the engine; envelope reports `total: 0, hasMore: false` | + | `SqlDriver.find` (the driver behind the default file-backed SQLite datasource, and every Postgres/MySQL deployment) | paginates on presence — `LIMIT 0`, **zero rows** | + | `TursoRemoteTransport` | presence — `LIMIT ?` bound to `0`, zero rows | + + So `limit: 0` now means "return no records" end to end, which is what the + canonical branch already implied. + + **`offset: 0` / `skip: 0` were dropped too, and that half is a consistency + change with no behavioural consequence** — `skip=0` is already the server's + default, so the request means the same thing whether the param is sent or not. + They are aligned because one emitter must not hold two rules for one pair, not + because a wrong answer was being returned. + + Callers passing a non-zero `limit`/`top`/`offset`/`skip`, or omitting them + entirely, are unaffected — the emitted query string is byte-identical. + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Major Changes diff --git a/packages/client/package.json b/packages/client/package.json index e006d0387e..e25dbb171e 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "Official Client SDK for ObjectStack Protocol", "main": "dist/index.js", diff --git a/packages/cloud-connection/CHANGELOG.md b/packages/cloud-connection/CHANGELOG.md index 686f66f9a0..9bbf7aca9b 100644 --- a/packages/cloud-connection/CHANGELOG.md +++ b/packages/cloud-connection/CHANGELOG.md @@ -1,5 +1,77 @@ # @objectstack/cloud-connection +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [f7d80f4] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [db59e9c] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [c51ffa5] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [1fa224a] +- Updated dependencies [35f7fb4] +- Updated dependencies [f8fe47e] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [cca11e9] +- Updated dependencies [cfb549d] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/runtime@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/cloud-connection/package.json b/packages/cloud-connection/package.json index db1f20c9bf..c12a979680 100644 --- a/packages/cloud-connection/package.json +++ b/packages/cloud-connection/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cloud-connection", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 (ADR-0008): the hub service, plan policy, and entitlements stay server-side.", "type": "module", diff --git a/packages/connectors/connector-mcp/CHANGELOG.md b/packages/connectors/connector-mcp/CHANGELOG.md index bfbc1c48f2..610ed4c6e1 100644 --- a/packages/connectors/connector-mcp/CHANGELOG.md +++ b/packages/connectors/connector-mcp/CHANGELOG.md @@ -1,5 +1,67 @@ # @objectstack/connector-mcp +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/connectors/connector-mcp/package.json b/packages/connectors/connector-mcp/package.json index 5d799fcace..aabf7707b9 100644 --- a/packages/connectors/connector-mcp/package.json +++ b/packages/connectors/connector-mcp/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-mcp", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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).", "main": "dist/index.js", diff --git a/packages/connectors/connector-openapi/CHANGELOG.md b/packages/connectors/connector-openapi/CHANGELOG.md index 12c7904ac9..876d327ceb 100644 --- a/packages/connectors/connector-openapi/CHANGELOG.md +++ b/packages/connectors/connector-openapi/CHANGELOG.md @@ -1,5 +1,67 @@ # @objectstack/connector-openapi +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/connectors/connector-openapi/package.json b/packages/connectors/connector-openapi/package.json index 873739022a..c5862d9f37 100644 --- a/packages/connectors/connector-openapi/package.json +++ b/packages/connectors/connector-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-openapi", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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).", "main": "dist/index.js", diff --git a/packages/connectors/connector-rest/CHANGELOG.md b/packages/connectors/connector-rest/CHANGELOG.md index 406af2b249..01e9971d8f 100644 --- a/packages/connectors/connector-rest/CHANGELOG.md +++ b/packages/connectors/connector-rest/CHANGELOG.md @@ -1,5 +1,67 @@ # @objectstack/connector-rest +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/connectors/connector-rest/package.json b/packages/connectors/connector-rest/package.json index 05849e2a8e..b7af1527d4 100644 --- a/packages/connectors/connector-rest/package.json +++ b/packages/connectors/connector-rest/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-rest", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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).", "main": "dist/index.js", diff --git a/packages/connectors/connector-slack/CHANGELOG.md b/packages/connectors/connector-slack/CHANGELOG.md index ca53b14b57..9c4a6e1b18 100644 --- a/packages/connectors/connector-slack/CHANGELOG.md +++ b/packages/connectors/connector-slack/CHANGELOG.md @@ -1,5 +1,67 @@ # @objectstack/connector-slack +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/connectors/connector-slack/package.json b/packages/connectors/connector-slack/package.json index cb76b9a6ed..df434f8209 100644 --- a/packages/connectors/connector-slack/package.json +++ b/packages/connectors/connector-slack/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-slack", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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).", "main": "dist/index.js", diff --git a/packages/console/CHANGELOG.md b/packages/console/CHANGELOG.md index 4c7a06af20..73be94ee21 100644 --- a/packages/console/CHANGELOG.md +++ b/packages/console/CHANGELOG.md @@ -1,5 +1,89 @@ # @objectstack/console +## 17.0.0-rc.6 + +### Minor Changes + +- 379b749: Console (objectui) refreshed to `0cf8f0f70d10`. Frontend changes in this range: + + Derived from the changesets objectui declared over the range — 24 releasing of 24 changesets added across 66 non-merge commits; omitted: 42 commits carrying no changeset (they ship no package code). + + - **minor** — **BREAKING** — Reclaim the natural names `GestureType` and `GestureConfig` (objectui#3363). (objectui `e6fdbdcc4`) + - **minor** — **BREAKING** — Track the `@objectstack` family at `17.0.0-rc.5` (objectui#3560). (objectui `48132f7e6`) + - **patch** — metadata-admin: name the offending key when only one union member ever read the value (objectui `be9cd38ac`) + - **patch** — System Hub: a card count that failed to load no longer renders as `0` (objectui `c1a18ed99`) + - **patch** — Count System Hub's Organizations card through `sys_organization`, the object the framework actually registers — it asked for `sys_org`, which does not exist, so the card read `0`… (objectui `278f57c36`) + - **patch** — metadata-admin: name the offending column when `config.columns` is rejected (objectui `949b2f147`) + - **patch** — Declare the retired `system/{users,organizations,roles,positions}` console URLs as redirects onto the framework-owned system objects (objectui#3655). (objectui `9961df297`) + - **patch** — Point the last four navigation producers at the canonical metadata-admin routes instead of the deprecated `component/metadata` alias, removing a redirect hop from each (objectui#3… (objectui `d2fd044b7`) + - **patch** — `view.readonlyTooltip` — the tooltip on a view tab's read-only lock — is retranslated in the eight packs (ja/ko/de/fr/es/pt/ru/ar) that still described the retired "duplicate to c… (objectui `33526fd51`) + - **patch** — Send the console host's legacy URL redirects straight to the canonical metadata-admin routes instead of routing them through the deprecated `component/metadata/resource` alias (ob… (objectui `7883c0250`) + - **patch** — Match the built-in pseudo-routes on whole path segments, so a mistyped app name can no longer render a different app (objectui#3638). (objectui `5f752a089`) + - **patch** — Make the zero-app console's "Object Manager" / "Datasources" entries resolve, and give that branch a not-found screen instead of a blank one (objectui#3610). (objectui `fa3ba5bf1`) + - **patch** — Point the four remaining "Settings" senders at the system hub `/apps/setup/system` instead of the bare `/apps/setup` (objectui#3611). (objectui `6b3d47b34`) + - **patch** — Render the `/home` Administration group as a real group, so its nine system-administration entries are reachable (objectui#3609). (objectui `13b72c740`) + - **patch** — `console.objectView.systemViewReadonly` and `console.objectView.expandToPage` are translated in the eight packs that stored English for them, so a Japanese, Korean, German, French… (objectui `4dcd52abe`) + - **patch** — metadata-admin: restore per-field diagnostics when editing an invalid stored `view` (objectui `c993ff26a`) + - **patch** — Point the "System Settings" entries at the system hub `/apps/setup/system` instead of the bare `/apps/setup` (objectui#3590). (objectui `d1be43673`) + - **patch** — Converge dashboard widget `compareTo` on the executor's `{ kind, dimension? }` contract, and make the dataset path actually render a comparison (objectui `4bc6c2340`) + - **patch** — metadata-admin no longer false-rejects a stored `view` that has been pinned or reordered. The editor's live client-side validation judged BOTH the create and the edit draft with t… (objectui `4cf76ce45`) + - **patch** — The organization-management console is translatable. The 90 keys under `organization.*` — the org layout and its tabs, the members list, the whole invitation flow, organization se… (objectui `42ae5c62a`) + - **patch** — Complete `packages/runner/vite.config.ts`'s workspace alias table to the full transitive import closure, so `@object-ui/runner` boots and builds from the monorepo sources without… (objectui `03f25f7a3`) + - **patch** — Runner in-app navigation now carries the current query string across to the pushed URL instead of `pushState`-ing a bare path. Opening the Runner with `?api=` and clicking a… (objectui `04fb8b8ab`) + - **patch** — The no-apps empty state's "Create Your First App" CTA now opens the app-creation flow instead of silently bouncing the user back to the landing page. It called `navigate('/create-… (objectui `9089d8503`) + - **patch** — The five locale keys behind #3546's eight no-fallback `t()` call sites are now defined in all ten packs, so the built-in-view toasts, the activity-timeline source link, the wizard… (objectui `6d762da7a`) + + ⚠️ 2 of these carry a breaking change: 2 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record. + + **In this console build, declared nowhere** — objectui merged 42 commits in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared them, so they appear in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387. + + - _(no changeset)_ docs(ROADMAP): P1.12 Routes/Tests 两条 PermissionManagementPage 记录按现实改写 (#3704) (#3714) (objectui `0cf8f0f70`) + - _(no changeset)_ test(scripts): gate version literals written into docs prose (#3711) (objectui `36bf20235`) + - _(no changeset)_ test(app-shell): pin what `invalid_value` at a union node means, and decline the relaxation (#3706) (objectui `f1310e40f`) + - _(no changeset)_ fix(react-runtime,sdui-parser,console): 补上三包声明了 MIT 却从未随包发布的许可证文本,并加门禁封死该类 (#3696, #3702) (#3703) (objectui `2267d6399`) + - _(no changeset)_ docs(ROADMAP): rewrite P1.12.2 + Permission Management to the post-#3673/#3699 reality (#3700) (#3705) (objectui `35e84b65f`) + - _(no changeset)_ test(scripts): 棘轮陈旧消息按实际成因分句,覆盖全部三条退休路径 (#3674) (#3701) (objectui `45cdd4cb4`) + - _(no changeset)_ chore(console): remove the orphaned SystemObjectViewPage + systemObjects dead code (#3672) (#3699) (objectui `b19f54f39`) + - _(no changeset)_ docs(cli): point the Node row at root engines, drop the ghost spec compatibility row (#3698) (objectui `d46b40324`) + - _(no changeset)_ docs(plugin-tree): add the `## License` section the other 36 published READMEs carry (#3664) (#3695) (objectui `3e601773e`) + - _(no changeset)_ docs(packages): retire the dead release-metadata §Compatibility block from 36 package READMEs (#3688) (objectui `4747344da`) + - _(no changeset)_ fix(cli,create-plugin): drop the `templates` files entry neither package has ever had (#3665) (#3687) (objectui `dcff16e06`) + - _(no changeset)_ docs(ci): type-check 作业行补两道 i18n 门禁,并把该表的钉粒度降到步骤级 (#3653) (#3683) (objectui `9cd84de2e`) + - _(no changeset)_ docs(agents): 补一条 prettier 假红护栏(#3682) (#3684) (objectui `074ec53d6`) + - _(no changeset)_ chore(deps): remove the unwired prettier devDependency (#3657) (#3681) (objectui `f953b5884`) + - _(no changeset)_ docs(runner): 按实测闭合 §Features 插件断言,三处 main.tsx 订正为 App.tsx (#3619) (#3652) (#3676) (objectui `0fcd57199`) + - _(no changeset)_ chore(deps): Bump mermaid from 11.16.0 to 11.16.1 (#3675) (objectui `2ce5c31f0`) + - _(no changeset)_ feat(scripts): en 文案改动必须由九个译文包同批跟改的门禁 (#3650) (#3659) (objectui `880e06905`) + - _(no changeset)_ docs(scripts): 按真实机制改写两处 Lychee 门禁描述,并删掉 judgeHref 重复注释 (#3587) (#3648) (#3656) (objectui `f4b828857`) + - _(no changeset)_ test(app-shell): 把 MetadataRedirectStub 同步回宿主实现,并用整链断言钉住转录真实性 (#3661) (#3671) (objectui `e98702190`) + - _(no changeset)_ test(scripts): gate that package.json `files` entries exist on disk (#3663) (#3667) (objectui `fe4d4da37`) + - _(no changeset)_ docs(runner): 把 README 两处开放集合的插件措辞按实测闭合 (#3632) (#3644) (objectui `c6acd7b8f`) + - _(no changeset)_ test(filter-parity): 给两处 spec 词表减法加排除项存活棘轮 (#3628) (#3640) (objectui `1e635d654`) + - _(no changeset)_ fix(plugin-tree): ship the MIT LICENSE the package.json files field already declares (#3647) (#3662) (objectui `dae1ac41e`) + - _(no changeset)_ docs(runner): §vite.config.ts 改写为指路真实文件 + 点名两个承重不变量 (#3643) (#3651) (objectui `93c261992`) + - _(no changeset)_ feat(scripts): check-doc-links 扫描面第四扩 packages/\*/README.md,并付清入场价的 11 条死链 (#3622) (#3649) (objectui `0d5da5394`) + - _(no changeset)_ docs(runner): §Add Custom Routes 改写为指向 Add Custom Schemas 的元数据路由说明 (#3618) (#3646) (objectui `54dd7ec1f`) + - _(no changeset)_ docs(runner): 删掉 Best Practices 里复活的环境变量配置面 (#3617) (#3633) (objectui `d9a03fe9a`) + - _(no changeset)_ docs(runner): README §Features 的 Hot Reload 按两个 loader 分路限定 (#3620) (#3634) (objectui `8098c8585`) + - _(no changeset)_ fix(docs,scripts): 清掉 9 条包 README 死链,并让链接门禁认站内绝对 URL (#3603) (#3629) (objectui `0e4ea07b2`) + - _(no changeset)_ test(types): drop 37 spec-retired DROPPED_SCHEMA_EXPORTS rows, add liveness ratchet (#3601) (#3623) (objectui `2904a7cd3`) + - _(no changeset)_ docs(runner): README §Development Workflow 按两个 loader 的真相改写第 1、3 步 (#3604) (#3621) (objectui `8d5418e59`) + - _(no changeset)_ docs(runner): 删掉 runner.mdx 的幽灵目录与「内置示例 schema」断言,重写 Package Information (#3577) (#3616) (objectui `616353ad1`) + - _(no changeset)_ docs(contributing): 按真实 root scripts 重写三条死的开发服务器命令 (#3596) (#3615) (objectui `ee3b42021`) + - _(no changeset)_ feat(scripts): check-doc-links 扫描面第三扩 CONTRIBUTING/ROADMAP/docs (#3572) (#3589) (objectui `6632114bc`) + - _(no changeset)_ docs(runner): 删掉 README 两处虚构能力面,修正 404 文档链接 (#3576) (#3602) (objectui `622c23082`) + - _(no changeset)_ docs(contributing): 按现状改写文档目录说明,站点源是 content/docs/ (#3584) (#3597) (objectui `74387e314`) + - _(no changeset)_ chore(scripts): remove dead start-app.mjs, fix stale MetadataLoader comment (#3591) (objectui `39477b03b`) + - _(no changeset)_ docs(contributing): 按现状改写链接门禁分工,换掉三条死的"正确示例"路由 (#3570) (#3585) (objectui `7a1a449c8`) + - _(no changeset)_ docs(runner): 记录 `api` 查询参数这一真实的元数据加载配置面 (#3537) (#3581) (objectui `632c07c5b`) + - _(no changeset)_ fix(tsconfig): 根 tsconfig.node.json 加 noEmit,堵住全仓排放 (#3574) (objectui `c35fed098`) + - _(no changeset)_ docs: 修正 CONTRIBUTING.md / ROADMAP.md 的 3 条死链 (#3545) (#3571) (objectui `d126607dc`) + - _(no changeset)_ fix(fields): 编辑弹窗 datetime/date 字段回显存量值 (#3565) (objectui `b785a77b3`) + + objectui range: `7dfbeb704e1e...0cf8f0f70d10` + + + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/console/package.json b/packages/console/package.json index 1100dfd3cd..9adc5350a6 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/console", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "description": "Prebuilt Console SPA pinned to this @objectstack/framework release. Source of truth: @object-ui/console (https://github.com/objectstack-ai/objectui).", "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 811afc209e..00e8047f81 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,66 @@ # @objectstack/core +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index aeee6e813c..da2d4eaca2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/core", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 97c93bb0d6..4c7c04bff4 100644 --- a/packages/create-objectstack/CHANGELOG.md +++ b/packages/create-objectstack/CHANGELOG.md @@ -1,5 +1,7 @@ # create-objectstack +## 17.0.0-rc.6 + ## 17.0.0-rc.5 ## 17.0.0-rc.4 diff --git a/packages/create-objectstack/package.json b/packages/create-objectstack/package.json index 52576ac130..85a66a3297 100644 --- a/packages/create-objectstack/package.json +++ b/packages/create-objectstack/package.json @@ -1,6 +1,6 @@ { "name": "create-objectstack", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "description": "Create a new ObjectStack project — npx create-objectstack", "bin": { "create-objectstack": "./bin/create-objectstack.js" diff --git a/packages/drivers/driver-memory/CHANGELOG.md b/packages/drivers/driver-memory/CHANGELOG.md index 125bfc5650..58412fd194 100644 --- a/packages/drivers/driver-memory/CHANGELOG.md +++ b/packages/drivers/driver-memory/CHANGELOG.md @@ -1,5 +1,219 @@ # @objectstack/driver-memory +## 17.0.0-rc.6 + +### Major Changes + +- 262e40d: refactor(drivers)!: memory / mongodb 的 `aggregate` / `distinct` 也收进 `DriverQuery`,契约没覆盖的方法不再要求把对象名写两遍 (#6212 批 C) + + #6210 的 changeset 结尾专门留了一句:`aggregate` / `distinct` **不在**那次范围内,因为它们不是 `IDataDriver` 收窄的那六个方法。#6212 记下了这笔账,本次结清 memory 与 mongodb 这两个包的部分。 + + 这批方法的第一个实参**已经是对象名**,query 里却仍旧要求再写一遍: + + | 位置 | 收窄前 | 收窄后 | + | :------------------------------------------ | :---------------------------------- | :------------------------------------- | + | `MongoDBDriver.aggregate` | `query: QueryAST` | `query: DriverQuery` | + | `InMemoryDriver.distinct` | `query?: QueryInput` | `query?: DriverQuery` | + | `InMemoryDriver.aggregate` | `Record[] \| QueryAST` | `Record[] \| DriverQuery` | + | `InMemoryDriver.performAggregation`(私有) | `Omit` | `DriverQuery` | + + 因为 `QueryAST` / `QueryInput` 都把 `object` 声明成**必填**,一个手上只有 `where` 的调用方根本叫不出这个类型的名字,于是伸手去拿 `as any` —— 连 `where` / `orderBy` / `limit` 的检查一起关掉。这正是 #5181 记过账的那笔代价(cloud#1053 实测 20 处,cloud#1030 的 `$like` 就是从这个口子活到运行时的)。收窄之后调用方可以直接写字面量: + + ```ts + // 收窄前:object 是必填,这句编译不过,于是 ... as any + // 收窄后:直接过,且 where / orderBy / aggregations 逐个受检 + await driver.aggregate("order", { + groupBy: ["region"], + aggregations: [{ function: "sum", field: "amount", alias: "total" }], + }); + ``` + + 同一次改动收回了 4 处已经多余的 `as any`(memory 2、mongodb 2),`check:query-options-erasure` 的测试面因此从 267 降到 263,baseline 已按门禁要求同 PR `--update`。 + + **`InMemoryDriver.aggregate` 的联合刻意保留。** 两条分支都有活体生产者:mongo 管线数组那支由 `memory-analytics.ts` 喂,AST 那支由 objectql 引擎与 `@objectstack/verify` 的日期分桶探针喂。退役任何一支都会打断其中一条。 + + **顺带把 `#6212` 正文的一处归因证伪了**:正文说 `performAggregation` 当初选 `Omit` 是被 `groupBy` 的元素类型差异逼的。实测 `QueryInput` 与 `QueryAST` 在 `groupBy` 上**逐字相同**,差异只在 `search` / `orderBy` / `expand`;直接换 `DriverQuery` 零报错。所以那不是被迫的选择,契约优先取 `DriverQuery`,不再引入第二个查询类型家族。 + + **零运行时改动。** 非测试改动 100% 是类型注解,无逻辑、无行为、无 emit 差异(`as` 断言在编译期即被抹除)。测试全绿:memory 532、mongodb 206(另 137 条需真实 mongod,按既有 opt-in 规则跳过)。这也是 #5499 冻结面上被允许的处置口径 —— 与 #6210 在同一批驱动上走的是同一条。 + + **迁移面:删掉调用字面量里的 `object:` 键**,与 #5181 / #6210 同一句话,现在覆盖到 `aggregate` / `distinct`。编译器会逐处指出来: + + ``` + error TS2353: Object literal may only specify known properties, + and 'object' does not exist in type 'DriverQuery'. + ``` + + 本仓实测只有一处需要改(`memory-driver.test.ts` 的 `distinct` 用例),且它写的值与第一实参逐字相等,纯冗余。 + + 标 major 的依据与 #5181 / #6210 一致:**源码级破坏性**(调用点内联字面量),运行时行为零变化。`check:api-surface` 只记录导出的存在与否、不记录签名,因此这条说明同样是该变更唯一的下游载体。 + +- d367f03: refactor(drivers)!: 五个驱动的 query 参数跟进 `DriverQuery`,休眠的类型谎言就此没有藏身处 (#6075) + + #5181(PR #6076)把 `IDataDriver.find/findOne/count/updateMany/deleteMany/explain` 的 query 参数收窄为 `DriverQuery`(`Omit`),并在同一条 changeset 里写明:「把驱动签名一并迁到 `DriverQuery` 是后续的机械收尾」。这就是那次收尾。 + + 在此之前,五个驱动的实现仍旧声明 `query: QueryAST`(turso 侧是 `query: any`)。**它不红,也不会红** —— 方法参数按双变比较,实现声明得比契约宽照样满足契约。但调用方现在**有权**省略 `object`,于是这些实现的类型说 `query.object` 是 `string`,运行期却可能是 `undefined`:一句休眠的谎言,没有任何门拦得住下一个照着它写代码的人。 + + 收尾之后,「驱动读 `query.object`」直接变成编译错误: + + ```ts + // 收窄前:编译通过,运行期可能是 undefined —— 谎言 + // 收窄后:error TS2339: Property 'object' does not exist on type 'DriverQuery'. + const name = query.object; + ``` + + **零运行时改动。** 本次改的全部是类型注解:五个驱动的六个契约方法签名,以及为让类型自洽而必须跟进的少量私有辅助方法参数(mongodb 的 `buildFindOptions` / `buildSortSpec`,sql 的 `findRows` / `orderKeysFor`,turso 的 `toRemoteQuery` / `toRemoteReadQuery`,memory 的 `performAggregation`)—— 它们都只转发或读取 `where` / `orderBy` / `groupBy` 这些字段,本来就不读 `object`。turso 的几处 `query: any` 一并收紧,多拿回一批本已放弃的检查。emit 无差异,测试全绿(memory 524、mongodb 206、sql 906、sqlite-wasm 254、turso 788)。 + + **迁移面:删掉驱动调用字面量里的 `object:` 键**,与 #5181 是同一句话,只是现在也覆盖了直接按具体驱动类(`SqlDriver` / `MemoryDriver` / …)而非按 `IDataDriver` 取类型的调用方。编译器会逐处指出来(TS2353 `'object' does not exist in type 'DriverQuery'`)。本仓下游 25 个包实测零处需要改动,改动只落在五个驱动自己的测试里。 + + 标 major 的依据与 #5181 一致:**源码级破坏性**(调用点内联字面量),运行时行为零变化。`check:api-surface` 只记录导出的存在与否、不记录签名,因此这条说明同样是该变更唯一的下游载体。 + + `aggregate` / `distinct` / `syncSchemasBatch` 不在本次范围内 —— 它们不是 `IDataDriver` 收窄的那六个方法,其中 `syncSchemasBatch` 的条目里 `object` 是被真实读取的必填键,`expand` 条目里的 `object` 同理命名的是关联对象,都不是冗余。 + +### Minor Changes + +- 82397b6: feat(drivers,objectql): `$regex` / `$options` are refused everywhere, and `$icontains` is implemented on the SQL family (#5702) + + The driver half of the #4706 ruling. #5701 landed the contract (the vocabulary, + the `RETIRED_FILTER_OPERATORS` prescriptions, the shared text case-set) and + #5710 flipped the last live producer — `plugin-auth`'s ObjectQL adapter, which + emitted `$regex` on the authentication path — so the refusal can now land + without breaking sign-in. + + **BREAKING for anyone writing `$regex` or `$options` in a filter.** Both are + refused on every backend with `INVALID_FILTER` / 400 and a message that names + the replacement. `$regex` was never a declared operator: `driver-sql` compiled + it to a LIKE-escaped substring (so `a.b` matched only the literal `a.b`), + `driver-memory` ran it as a real `RegExp` (so the same filter also matched + `axb`, and an _invalid_ pattern was caught and answered `false` — zero rows, in + silence), and `objectql`'s `having` did the same. Write `$icontains` for the + case-insensitive substring search this was almost always used for, `$contains` + for a case-sensitive one; a pattern that genuinely needs a regex has no + filter-level replacement. + + **`$icontains` now runs on the SQL family** — `driver-sql`, `driver-sqlite-wasm`, + and both of `driver-turso`'s transports (the remote one does not go through + knex, so it needed its own). It compiles to `LOWER(col) LIKE LOWER(?) ESCAPE ?` + through the same `applyLike` / `pushLike` that carries the `%` / `_` / `\` + escaping, as a `fold` parameter rather than a second emitter — a copied emitter + is where the escape class would have been dropped, and an unescaped `%` matches + every row. An empty or non-string comparand is refused on the validating walk + (an empty one matches every row, which widens rather than narrows). On SQLite + `lower()` folds ASCII only, which IS the contract (#4706 Q1 = A): `$icontains: +'café'` does not match `CAFÉ`. + + + + `driver-mongodb`'s unknown-operator arm was throwing a bare `Error` with no + `code` and no `status`, three lines from the helper in its own file that sets + `INVALID_FILTER` / 400 — a 500-shaped body for a 400-class client mistake. It + now speaks the same envelope as its three siblings. + + Two parts of the ruling are deliberately NOT in this change and stay tracked in + `scripts/check-driver-conformance.mjs`'s ledger: the `$contains` family's + case-sensitivity (#4706 Q2 = A) needs SQLite's `LIKE` replaced by a case-exact + construct in the driver, the RLS lowering and the analytics lowering together, + or one permission rule compiles to two row sets (#6518); and `$icontains` on the + JS evaluation faces needs the spec vocabulary to take the operator, which cannot + happen before `driver-memory` has an arm for it (#6520). + +### Patch Changes + +- 3510e4a: refactor(spec,drivers,lint): one implementation of the filter identity reduction (#5659) + + `{ $and: [] }` matches every row, `{ $or: [] }` matches none, `{}` is a TRUE + disjunct that absorbs its `$or`, `{ $not: {} }` is FALSE. That is a ruling + (#5322/#5134) pinned for every backend by the four identity cases in + `FILTER_LOGIC_CASES` — and it was implemented four times over: `reduceFilterNode` + in `driver-sql`, the same function again in `driver-mongodb`, the + `every`/`some`/truthiness algebra of `driver-memory`'s matcher, and nearly a + fifth hand-written copy inside `@objectstack/lint`, which declined to write one + and filed this issue instead. + + **New in `@objectstack/spec` (`@objectstack/spec/data`): `reduceFilterVerdict`**, + beside the case table that proves it. It answers `'true' | 'false' | 'clause'` + for a filter node and never throws on its own; each backend's own refusals — the + undeclared `$`-combinator and the `undefined` comparand in `driver-sql`, the + query-level keys and the `$null` comparand in `driver-mongodb` — are passed in as + `FilterVerdictHooks` and are invoked from exactly the positions they were invoked + from before. `reduceFilterKeyVerdict` answers the same question for one key, which + is what both SQL and MongoDB emitters consult while walking a node. + + **No behaviour changes in the three drivers.** The move is mechanical: the shared + algebra replaces each private copy, the refusals stay where they were, and the + `FILTER_LOGIC_CASES` conformance suites are green on both sides of the change — + including the SQL-inheriting `driver-sqlite-wasm` and `driver-turso`. + + **`@objectstack/lint` gains two warnings it was structurally blind to.** The + `multi: true` unbounded-bulk-write rule (#5482) asked "does this filter have zero + keys", so a `delete_record` bounded by `filter: { $and: [] }` or + `filter: { $or: [{}] }` — a whole-object write by the ruling every driver executes + — passed silently. It now asks the reduction, and it warns about both while + staying quiet on `{ $or: [] }` and `{ $not: {} }`, which match nothing. The + message names the shape it saw (`a filter that REDUCES TO TRUE ({"$and":[]})`) + rather than calling a non-empty filter "empty". + + If you have a flow declaring a bulk write bounded by one of those two shapes, the + lint will now tell you so — the write was already unbounded at run time; only the + feedback is new. + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/drivers/driver-memory/package.json b/packages/drivers/driver-memory/package.json index a2bbd680fe..e0a71c3363 100644 --- a/packages/drivers/driver-memory/package.json +++ b/packages/drivers/driver-memory/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-memory", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "In-Memory Driver for ObjectStack (Reference Implementation)", "main": "dist/index.js", diff --git a/packages/drivers/driver-mongodb/CHANGELOG.md b/packages/drivers/driver-mongodb/CHANGELOG.md index 232a600c02..8e47fc9c27 100644 --- a/packages/drivers/driver-mongodb/CHANGELOG.md +++ b/packages/drivers/driver-mongodb/CHANGELOG.md @@ -1,5 +1,221 @@ # @objectstack/driver-mongodb +## 17.0.0-rc.6 + +### Major Changes + +- 262e40d: refactor(drivers)!: memory / mongodb 的 `aggregate` / `distinct` 也收进 `DriverQuery`,契约没覆盖的方法不再要求把对象名写两遍 (#6212 批 C) + + #6210 的 changeset 结尾专门留了一句:`aggregate` / `distinct` **不在**那次范围内,因为它们不是 `IDataDriver` 收窄的那六个方法。#6212 记下了这笔账,本次结清 memory 与 mongodb 这两个包的部分。 + + 这批方法的第一个实参**已经是对象名**,query 里却仍旧要求再写一遍: + + | 位置 | 收窄前 | 收窄后 | + | :------------------------------------------ | :---------------------------------- | :------------------------------------- | + | `MongoDBDriver.aggregate` | `query: QueryAST` | `query: DriverQuery` | + | `InMemoryDriver.distinct` | `query?: QueryInput` | `query?: DriverQuery` | + | `InMemoryDriver.aggregate` | `Record[] \| QueryAST` | `Record[] \| DriverQuery` | + | `InMemoryDriver.performAggregation`(私有) | `Omit` | `DriverQuery` | + + 因为 `QueryAST` / `QueryInput` 都把 `object` 声明成**必填**,一个手上只有 `where` 的调用方根本叫不出这个类型的名字,于是伸手去拿 `as any` —— 连 `where` / `orderBy` / `limit` 的检查一起关掉。这正是 #5181 记过账的那笔代价(cloud#1053 实测 20 处,cloud#1030 的 `$like` 就是从这个口子活到运行时的)。收窄之后调用方可以直接写字面量: + + ```ts + // 收窄前:object 是必填,这句编译不过,于是 ... as any + // 收窄后:直接过,且 where / orderBy / aggregations 逐个受检 + await driver.aggregate("order", { + groupBy: ["region"], + aggregations: [{ function: "sum", field: "amount", alias: "total" }], + }); + ``` + + 同一次改动收回了 4 处已经多余的 `as any`(memory 2、mongodb 2),`check:query-options-erasure` 的测试面因此从 267 降到 263,baseline 已按门禁要求同 PR `--update`。 + + **`InMemoryDriver.aggregate` 的联合刻意保留。** 两条分支都有活体生产者:mongo 管线数组那支由 `memory-analytics.ts` 喂,AST 那支由 objectql 引擎与 `@objectstack/verify` 的日期分桶探针喂。退役任何一支都会打断其中一条。 + + **顺带把 `#6212` 正文的一处归因证伪了**:正文说 `performAggregation` 当初选 `Omit` 是被 `groupBy` 的元素类型差异逼的。实测 `QueryInput` 与 `QueryAST` 在 `groupBy` 上**逐字相同**,差异只在 `search` / `orderBy` / `expand`;直接换 `DriverQuery` 零报错。所以那不是被迫的选择,契约优先取 `DriverQuery`,不再引入第二个查询类型家族。 + + **零运行时改动。** 非测试改动 100% 是类型注解,无逻辑、无行为、无 emit 差异(`as` 断言在编译期即被抹除)。测试全绿:memory 532、mongodb 206(另 137 条需真实 mongod,按既有 opt-in 规则跳过)。这也是 #5499 冻结面上被允许的处置口径 —— 与 #6210 在同一批驱动上走的是同一条。 + + **迁移面:删掉调用字面量里的 `object:` 键**,与 #5181 / #6210 同一句话,现在覆盖到 `aggregate` / `distinct`。编译器会逐处指出来: + + ``` + error TS2353: Object literal may only specify known properties, + and 'object' does not exist in type 'DriverQuery'. + ``` + + 本仓实测只有一处需要改(`memory-driver.test.ts` 的 `distinct` 用例),且它写的值与第一实参逐字相等,纯冗余。 + + 标 major 的依据与 #5181 / #6210 一致:**源码级破坏性**(调用点内联字面量),运行时行为零变化。`check:api-surface` 只记录导出的存在与否、不记录签名,因此这条说明同样是该变更唯一的下游载体。 + +- d367f03: refactor(drivers)!: 五个驱动的 query 参数跟进 `DriverQuery`,休眠的类型谎言就此没有藏身处 (#6075) + + #5181(PR #6076)把 `IDataDriver.find/findOne/count/updateMany/deleteMany/explain` 的 query 参数收窄为 `DriverQuery`(`Omit`),并在同一条 changeset 里写明:「把驱动签名一并迁到 `DriverQuery` 是后续的机械收尾」。这就是那次收尾。 + + 在此之前,五个驱动的实现仍旧声明 `query: QueryAST`(turso 侧是 `query: any`)。**它不红,也不会红** —— 方法参数按双变比较,实现声明得比契约宽照样满足契约。但调用方现在**有权**省略 `object`,于是这些实现的类型说 `query.object` 是 `string`,运行期却可能是 `undefined`:一句休眠的谎言,没有任何门拦得住下一个照着它写代码的人。 + + 收尾之后,「驱动读 `query.object`」直接变成编译错误: + + ```ts + // 收窄前:编译通过,运行期可能是 undefined —— 谎言 + // 收窄后:error TS2339: Property 'object' does not exist on type 'DriverQuery'. + const name = query.object; + ``` + + **零运行时改动。** 本次改的全部是类型注解:五个驱动的六个契约方法签名,以及为让类型自洽而必须跟进的少量私有辅助方法参数(mongodb 的 `buildFindOptions` / `buildSortSpec`,sql 的 `findRows` / `orderKeysFor`,turso 的 `toRemoteQuery` / `toRemoteReadQuery`,memory 的 `performAggregation`)—— 它们都只转发或读取 `where` / `orderBy` / `groupBy` 这些字段,本来就不读 `object`。turso 的几处 `query: any` 一并收紧,多拿回一批本已放弃的检查。emit 无差异,测试全绿(memory 524、mongodb 206、sql 906、sqlite-wasm 254、turso 788)。 + + **迁移面:删掉驱动调用字面量里的 `object:` 键**,与 #5181 是同一句话,只是现在也覆盖了直接按具体驱动类(`SqlDriver` / `MemoryDriver` / …)而非按 `IDataDriver` 取类型的调用方。编译器会逐处指出来(TS2353 `'object' does not exist in type 'DriverQuery'`)。本仓下游 25 个包实测零处需要改动,改动只落在五个驱动自己的测试里。 + + 标 major 的依据与 #5181 一致:**源码级破坏性**(调用点内联字面量),运行时行为零变化。`check:api-surface` 只记录导出的存在与否、不记录签名,因此这条说明同样是该变更唯一的下游载体。 + + `aggregate` / `distinct` / `syncSchemasBatch` 不在本次范围内 —— 它们不是 `IDataDriver` 收窄的那六个方法,其中 `syncSchemasBatch` 的条目里 `object` 是被真实读取的必填键,`expand` 条目里的 `object` 同理命名的是关联对象,都不是冗余。 + +### Minor Changes + +- 82397b6: feat(drivers,objectql): `$regex` / `$options` are refused everywhere, and `$icontains` is implemented on the SQL family (#5702) + + The driver half of the #4706 ruling. #5701 landed the contract (the vocabulary, + the `RETIRED_FILTER_OPERATORS` prescriptions, the shared text case-set) and + #5710 flipped the last live producer — `plugin-auth`'s ObjectQL adapter, which + emitted `$regex` on the authentication path — so the refusal can now land + without breaking sign-in. + + **BREAKING for anyone writing `$regex` or `$options` in a filter.** Both are + refused on every backend with `INVALID_FILTER` / 400 and a message that names + the replacement. `$regex` was never a declared operator: `driver-sql` compiled + it to a LIKE-escaped substring (so `a.b` matched only the literal `a.b`), + `driver-memory` ran it as a real `RegExp` (so the same filter also matched + `axb`, and an _invalid_ pattern was caught and answered `false` — zero rows, in + silence), and `objectql`'s `having` did the same. Write `$icontains` for the + case-insensitive substring search this was almost always used for, `$contains` + for a case-sensitive one; a pattern that genuinely needs a regex has no + filter-level replacement. + + **`$icontains` now runs on the SQL family** — `driver-sql`, `driver-sqlite-wasm`, + and both of `driver-turso`'s transports (the remote one does not go through + knex, so it needed its own). It compiles to `LOWER(col) LIKE LOWER(?) ESCAPE ?` + through the same `applyLike` / `pushLike` that carries the `%` / `_` / `\` + escaping, as a `fold` parameter rather than a second emitter — a copied emitter + is where the escape class would have been dropped, and an unescaped `%` matches + every row. An empty or non-string comparand is refused on the validating walk + (an empty one matches every row, which widens rather than narrows). On SQLite + `lower()` folds ASCII only, which IS the contract (#4706 Q1 = A): `$icontains: +'café'` does not match `CAFÉ`. + + + + `driver-mongodb`'s unknown-operator arm was throwing a bare `Error` with no + `code` and no `status`, three lines from the helper in its own file that sets + `INVALID_FILTER` / 400 — a 500-shaped body for a 400-class client mistake. It + now speaks the same envelope as its three siblings. + + Two parts of the ruling are deliberately NOT in this change and stay tracked in + `scripts/check-driver-conformance.mjs`'s ledger: the `$contains` family's + case-sensitivity (#4706 Q2 = A) needs SQLite's `LIKE` replaced by a case-exact + construct in the driver, the RLS lowering and the analytics lowering together, + or one permission rule compiles to two row sets (#6518); and `$icontains` on the + JS evaluation faces needs the spec vocabulary to take the operator, which cannot + happen before `driver-memory` has an arm for it (#6520). + +### Patch Changes + +- 3510e4a: refactor(spec,drivers,lint): one implementation of the filter identity reduction (#5659) + + `{ $and: [] }` matches every row, `{ $or: [] }` matches none, `{}` is a TRUE + disjunct that absorbs its `$or`, `{ $not: {} }` is FALSE. That is a ruling + (#5322/#5134) pinned for every backend by the four identity cases in + `FILTER_LOGIC_CASES` — and it was implemented four times over: `reduceFilterNode` + in `driver-sql`, the same function again in `driver-mongodb`, the + `every`/`some`/truthiness algebra of `driver-memory`'s matcher, and nearly a + fifth hand-written copy inside `@objectstack/lint`, which declined to write one + and filed this issue instead. + + **New in `@objectstack/spec` (`@objectstack/spec/data`): `reduceFilterVerdict`**, + beside the case table that proves it. It answers `'true' | 'false' | 'clause'` + for a filter node and never throws on its own; each backend's own refusals — the + undeclared `$`-combinator and the `undefined` comparand in `driver-sql`, the + query-level keys and the `$null` comparand in `driver-mongodb` — are passed in as + `FilterVerdictHooks` and are invoked from exactly the positions they were invoked + from before. `reduceFilterKeyVerdict` answers the same question for one key, which + is what both SQL and MongoDB emitters consult while walking a node. + + **No behaviour changes in the three drivers.** The move is mechanical: the shared + algebra replaces each private copy, the refusals stay where they were, and the + `FILTER_LOGIC_CASES` conformance suites are green on both sides of the change — + including the SQL-inheriting `driver-sqlite-wasm` and `driver-turso`. + + **`@objectstack/lint` gains two warnings it was structurally blind to.** The + `multi: true` unbounded-bulk-write rule (#5482) asked "does this filter have zero + keys", so a `delete_record` bounded by `filter: { $and: [] }` or + `filter: { $or: [{}] }` — a whole-object write by the ruling every driver executes + — passed silently. It now asks the reduction, and it warns about both while + staying quiet on `{ $or: [] }` and `{ $not: {} }`, which match nothing. The + message names the shape it saw (`a filter that REDUCES TO TRUE ({"$and":[]})`) + rather than calling a non-empty filter "empty". + + If you have a flow declaring a bulk write bounded by one of those two shapes, the + lint will now tell you so — the write was already unbounded at run time; only the + feedback is new. + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/drivers/driver-mongodb/package.json b/packages/drivers/driver-mongodb/package.json index a3da81873f..130e96de2a 100644 --- a/packages/drivers/driver-mongodb/package.json +++ b/packages/drivers/driver-mongodb/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-mongodb", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "MongoDB Driver for ObjectStack - Native document database driver via official mongodb client", "main": "dist/index.js", diff --git a/packages/drivers/driver-sql/CHANGELOG.md b/packages/drivers/driver-sql/CHANGELOG.md index d894125a48..c26afbea77 100644 --- a/packages/drivers/driver-sql/CHANGELOG.md +++ b/packages/drivers/driver-sql/CHANGELOG.md @@ -1,5 +1,420 @@ # @objectstack/driver-sql +## 17.0.0-rc.6 + +### Major Changes + +- 29e28a3: refactor(drivers)!: `aggregate` 的 query 参数收窄到 `DriverQuery`,并退役 `aggregate` / `func` 两个未声明别名 (#6212 批 B、#6321) + + #5181(PR #6076)收窄了 `IDataDriver` 声明的六个方法,#6075(PR #6210)让五个驱动的实现跟上,#6212 批 A+E 处理了 SQL 驱动自有的另两道门。本次是同一条线上的 `aggregate`:`driver-sql`、`driver-turso` 的转发层与 `RemoteTransport` 三处,全部从 `query: any` 收到 `DriverQuery`(`@objectstack/spec/contracts`)。 + + `any` 在 query 参数上不是「对象名没检查」,而是**检查全关**:`where` 的 filter 方言、`groupBy` 的节点联合、`aggregations` 的节点形状——而这三样恰恰是这几个方法体读的全部内容。 + + ## 一、退役两个协议从未声明的别名(#6321,ADR-0049) + + ```ts + const aggregates = query.aggregations || query.aggregate; // driver-sql + const funcName = agg.function || agg.func; + const aggregations = query?.aggregations || query?.aggregate || []; // RemoteTransport + const func = String(agg.function || agg.func || ""); + ``` + + `QueryASTSchema` 声明的是 `aggregations`,`AggregationNodeSchema` 声明的是 `function`;`aggregate` / `func` 在 `packages/spec` 里**一个字都没有**。实测全仓唯一书写者是这两个驱动包自己的 fixture(`sql-driver-advanced` 7 处、`sql-driver-queryast` 1 处、`sqlite-wasm-driver-advanced` 7 处、`sqlite-wasm-driver-queryast` 1 处),非测试面零书写者——#4984 那一家:**fixture 拼着别名,宽容分支就永远绿着活下去,没有任何测试能在删掉它时转红**。fixture 已按已声明拼写重拼,写者归零,PD#12 与 ADR-0049 enforce-or-remove 于是把这两条 `||` 一并删掉。 + + 顺带删掉的还有 `|| ''`:它只在**两个键都没写**时才生效,而那时这一面把名字回引成 `""`、本地面回引成 `"undefined"`,同一份越界输入两种措辞(#5240)。别名在时这条岔路够不着,删别名恰恰让它够得着,所以同一次关掉。 + + **迁移**:`aggregate:` → `aggregations:`,`func:` → `function:`。写旧拼写的内联字面量现在是编译错误(TS2353);越过 `tsc` 的 JS 调用方,`aggregate:` 会静默拿不到聚合列,`func:` 则拿到已有的具名 400(`INVALID_QUERY`,#5907)。本仓实测需要改动的非测试调用点为零。 + + ## 二、一处真实行为改动:`RemoteTransport` 现在会编 `GroupByNode` 联合 + + `GroupByNodeSchema` 是 `z.union([z.string(), z.object({ field, dateGranularity?, alias? })])`,而这一层把它当 `string[]` 读。收窄后 `tsc` 直接把这条假设摆上台面(TS2322)。联合的两半状况完全不同,所以这不是一个 cast 能了事的: + + - **无 granularity 的结构化条目**(`{ field: 'region' }`)是 spec 合法、且**今天就会下推到驱动**的形状:objectql 的 aggregate 派发对它一律判为「受支持」(`engine.ts` 里逐字写着 `plain {field} object is fine`),`objectql/src/secret-fields.test.ts:341` 就是这个形状的活体。本驱动的**本地面**把它编成普通的 `GROUP BY "region"`,远端面却把它插值成 `"[object Object]"`、死在标识符安全检查里——一条查询两种答案、由连接串决定,正是 #6203 那个形状,而且**是活体不是休眠**:能力位 `queryDateGranularity` 只管带 granularity 的那一半,管不到这一半。现在读 `.field`,两面收敛。 + - **带 dateGranularity 的条目**远端确实编不出来,而这一点是**已声明**的:remote 模式发布 `queryDateGranularity: {}`,引擎据此全部落到内存分桶,因此不会下推。缺的是「绕过能力位、直连驱动」的那个调用方该得到什么答案——现在得到 ADR-0112 信封(`NOT_IMPLEMENTED` / 501),与聚合函数「协议已声明、本后端编不出」用的是同一类,而不是一句 SQL 注入告警。 + + `alias` **不读**,与本地面一致:`SqlDriver.aggregate` 也不读它,只在这一面读会是新的分叉而不是修复。 + + ## 三、`SqlDriver` 那一面的同一条件也换上了信封 + + `SqlDriver.aggregate` 对「本方言编不出这个 granularity」原本抛裸 `Error`(`code`/`status` 皆 `undefined` ⇒ `mapDataError` 落默认分支,一个具名能力缺口以不透明 500 到达调用方)。只给远端面加信封就会造出 #5907 花一整个 issue 才关掉的那种分叉——`TursoDriver` 由 `url` 选面,同一条件不能有两种线上身份。两面首句逐字一致(`Date bucketing by '' is not supported by this backend.`),尾句各报**本面**编得出的 granularity,由一条跨包 parity 用例比对两个**运行时**消息钉住。 + + **消息文本变更**(可能影响按文本匹配的下游断言): + + ``` + - SqlDriver: dateGranularity 'week' not supported on dialect 'better-sqlite3'. Engine must fall back to in-memory bucketing. + + Date bucketing by 'week' is not supported by this backend. Bucketed here: day, month, quarter, year (dialect 'better-sqlite3'). … (code=NOT_IMPLEMENTED, status=501) + ``` + + ## 定级依据 + + 标 major 与 #5181 / #6075 / #6210 一致:**源码级破坏性**(调用点内联字面量、以及被删的两个别名键),加上第二、三节两处真实的运行期改动。`check:api-surface` 只记录导出的存在与否、不记录签名,所以这条说明是该变更唯一的下游载体。 + + `driver-sqlite-wasm` 未列入:它整个继承 `SqlDriver.aggregate`,自身源码零改动(改的只有它的 fixture 与一条断言)——与批 A+E 的处理一致。它读的是 driver-sql 的 `dist/*.d.ts`,因此验证时**必须先重建 driver-sql** 再 typecheck/test,否则是假绿。 + + + +- d367f03: refactor(drivers)!: 五个驱动的 query 参数跟进 `DriverQuery`,休眠的类型谎言就此没有藏身处 (#6075) + + #5181(PR #6076)把 `IDataDriver.find/findOne/count/updateMany/deleteMany/explain` 的 query 参数收窄为 `DriverQuery`(`Omit`),并在同一条 changeset 里写明:「把驱动签名一并迁到 `DriverQuery` 是后续的机械收尾」。这就是那次收尾。 + + 在此之前,五个驱动的实现仍旧声明 `query: QueryAST`(turso 侧是 `query: any`)。**它不红,也不会红** —— 方法参数按双变比较,实现声明得比契约宽照样满足契约。但调用方现在**有权**省略 `object`,于是这些实现的类型说 `query.object` 是 `string`,运行期却可能是 `undefined`:一句休眠的谎言,没有任何门拦得住下一个照着它写代码的人。 + + 收尾之后,「驱动读 `query.object`」直接变成编译错误: + + ```ts + // 收窄前:编译通过,运行期可能是 undefined —— 谎言 + // 收窄后:error TS2339: Property 'object' does not exist on type 'DriverQuery'. + const name = query.object; + ``` + + **零运行时改动。** 本次改的全部是类型注解:五个驱动的六个契约方法签名,以及为让类型自洽而必须跟进的少量私有辅助方法参数(mongodb 的 `buildFindOptions` / `buildSortSpec`,sql 的 `findRows` / `orderKeysFor`,turso 的 `toRemoteQuery` / `toRemoteReadQuery`,memory 的 `performAggregation`)—— 它们都只转发或读取 `where` / `orderBy` / `groupBy` 这些字段,本来就不读 `object`。turso 的几处 `query: any` 一并收紧,多拿回一批本已放弃的检查。emit 无差异,测试全绿(memory 524、mongodb 206、sql 906、sqlite-wasm 254、turso 788)。 + + **迁移面:删掉驱动调用字面量里的 `object:` 键**,与 #5181 是同一句话,只是现在也覆盖了直接按具体驱动类(`SqlDriver` / `MemoryDriver` / …)而非按 `IDataDriver` 取类型的调用方。编译器会逐处指出来(TS2353 `'object' does not exist in type 'DriverQuery'`)。本仓下游 25 个包实测零处需要改动,改动只落在五个驱动自己的测试里。 + + 标 major 的依据与 #5181 一致:**源码级破坏性**(调用点内联字面量),运行时行为零变化。`check:api-surface` 只记录导出的存在与否、不记录签名,因此这条说明同样是该变更唯一的下游载体。 + + `aggregate` / `distinct` / `syncSchemasBatch` 不在本次范围内 —— 它们不是 `IDataDriver` 收窄的那六个方法,其中 `syncSchemasBatch` 的条目里 `object` 是被真实读取的必填键,`expand` 条目里的 `object` 同理命名的是关联对象,都不是冗余。 + +- 62159bd: refactor(driver-sql)!: `SqlDriver.distinct` 的第三参收成裸 `FilterCondition`,一个静默返回全集的写法就此编译不过 (#6320) + + `distinct` 不在 `IDataDriver` 上,所以 #5181(PR #6076)与 #6075(PR #6210)的收窄都没走到它,#6212 批 A+E(#6355)收的是 `analyzeQuery` / `findWithWindowFunctions`,也没覆盖它。它的方法体一直说得很清楚——`applyFilters(builder, filters)` 拿的是**实参本身**,因此它要的是 `find()` 放在 `query.where` 里的那个值,**不是 query 信封**;`filters?: any` 只是没把这句话写进类型里。 + + ```ts + // 收窄前后都成立,一处调用点都不用改 + await driver.distinct("orders", "product", { status: "completed" }); + ``` + + **收窄真正买到的东西,是实测出来的,不是推断的。** 三行数据(`Laptop`/`Mouse` 为 `completed`,`Ghost` 为 `pending`),逐个形状喂给 `distinct('orders','product', …)`: + + | 第三参 | 收窄前 | 收窄后 | + | :--------------------------- | :--------------------------------- | :----------- | + | `{ status: 'completed' }` | 返回 `["Laptop","Mouse"]` | 不变 | + | 省略 | 返回全集 | 不变 | + | `'completed'`(标量) | **编译通过,返回全集** | **编译错误** | + | `{ object, where }`(信封) | 抛 `INVALID_FILTER` / 400 | 不变 | + | `['status','=','completed']` | 抛 `INVALID_FILTER` / 400(#5158) | 不变 | + + 第三行就是本次消掉的那一格:一个真心想问「completed 订单里有哪些商品」的调用,编译通过,然后拿到**每一个**商品。`applyFilters` 对「真值但非对象、非数组」的 filter 不发射任何谓词(该方法尾注写着这件事),于是过滤条件被整条丢掉。方向是**放宽**——这正是 #6320 与 #5234 同族的那类「静默错答案」。 + + **有一格是任何类型都关不上的,本次如实写进注释而不是假装关上了。** `FilterCondition` 的键**就是字段名**,所以它是开放映射(`[key: string]: any`):`{ object, where }` 在结构上是一个完全合法的 filter——约束两个分别叫 `object` 和 `where` 的列。没有任何注解能把它和正当 filter 分开。#6320 提出的「让反向错配也编译不过」在这个参数上**不可达**,实测确认;能拿到的保证是**运行期响亮失败**:信封里的 `where` 是对象,而没有任何比较值可以是对象,于是 `assertCompilableComparand` 抛 `INVALID_FILTER` / 400。这半边 driver-sql 从来就不是静默的;`driver-memory` 那半边(裸 filter 交给它会静默返回全集)留在 #5499 冻结面内,本次不碰。 + + **零运行时改动**:非测试改动 100% 是一个类型注解加一段注释,无逻辑、无行为、无 emit 差异。 + + **逐处复核了全部 14 个调用点**(本单正文记的是 3 处,实测偏低):driver-sql 11 处、driver-sqlite-wasm 3 处、driver-turso 0 处;其中真正传第三参的是 4 处(driver-sql 2 + driver-sqlite-wasm 2),全部本来就写的裸 filter,**零报错、零 fixture 改动**。 + + **driver-sqlite-wasm 也标 major**:`SqliteWasmDriver extends SqlDriver` 且不覆写 `distinct`,所以它**已发布的 `.d.ts`** 里这个方法的签名同样收窄,它的使用者看到的是同一个变化。该包读的是 driver-sql 构建后的 `dist/*.d.ts` 而非源码,是一处已知门禁盲区,本次用「往参数类型里临时塞一个调用方不可能满足的成员、重建、看调用点是否逐一变红」证明它确实读到了新 d.ts:driver-sql 6 处红、driver-sqlite-wasm 3 处红,与预判逐一相符。 + + ### 迁移 + + 调用点若把**标量**(或任何非 `FilterCondition` 值)交给第三参,编译器会指出来: + + ``` + error TS2345: Argument of type 'string' is not assignable to parameter of type 'FilterCondition'. + ``` + + 改法是把它写成它本来就该是的裸 filter 对象(`'completed'` → `{ status: 'completed' }`)。⚠️ 这类调用点在收窄前拿到的是**未过滤的全集**,所以这不是一次等价改写:修完之后返回值会变,而变化后的那个才是调用方本来想要的答案。本仓零处这样的调用点。 + + ⚠️ 无类型的 JS 调用方**既不会拿到编译错误、也不会有任何行为变化**(本次零运行时改动)。对他们而言,上面那条是「你一直没在过滤」的**唯一通知渠道** —— 这也是本次记台账条目的理由,见下。 + + + +- d48aad5: refactor(driver-sql)!: `analyzeQuery` / `findWithWindowFunctions` 不再吃 `any`,窗口门自带扁平形类型 (#6212 批 A+E) + + #5181(PR #6076)收窄了 `IDataDriver` 声明的六个方法,#6075(PR #6210)让五个驱动的实现跟上。收尾漏下的是**驱动自有、不在 `IDataDriver` 上**的那批查询门:它们同样吃 query AST,签名却是 `any`。本次处理 SQL 驱动的两个。 + + `any` 在 query 参数上不是「对象名没检查」,而是**检查全关**:`where` 的 filter 方言、`orderBy` 的 sort node 形状、`limit`/`offset` 是不是数字,全部被抹掉——而这两个方法体读的恰恰就是这些字段。`$like` 当年就是从同一个口子活到运行时的(cloud#1030、cloud#1053 实测 20 处)。 + + **`analyzeQuery` → `DriverQuery`。** 它是 `explain()` 的实现体,而 `explain()` 本来就声明 `DriverQuery` 并一行转发过来——收窄前这一对是自相矛盾的:契约门声明 AST,它背后的实现声明 `any`。方法体只读 `fields` / `where` / `orderBy` / `limit` / `offset`,全在 `DriverQuery` 内,因此这是一次纯注解:driver-sql 与 driver-sqlite-wasm 实测零报错、零 fixture 改动。 + + **`findWithWindowFunctions` → 驱动本地的扁平形类型**,新导出 `SqlWindowFunctionQuery` / `SqlWindowFunctionSpec`: + + ```ts + import type { SqlWindowFunctionQuery } from "@objectstack/driver-sql"; + + const ranked = await sqlDriver.findWithWindowFunctions("employee", { + windowFunctions: [ + { + function: "rank", + alias: "salary_rank", + partitionBy: ["department"], + orderBy: [{ field: "salary", order: "desc" }], + }, + ], + }); + ``` + + 它**不能**标 `DriverQuery`:`query.windowFunctions` 在 spec 是 `retiredKey()` 墓碑(#4286),`QueryAST['windowFunctions']` 解析为 `undefined`,标上去会让这道门自己已发布文档里的载荷编译不过。类型因此写成 `Omit & { windowFunctions?: SqlWindowFunctionSpec[] }`——契约那一半照旧受检,驱动私有那一半由驱动自己声明。 + + 类型放在驱动层、**不进 `packages/spec`**,是接着 #4286 的判断往下走:那次删掉 `WindowFunctionNodeSchema` 的理由正是它声明了 `field` / `over` / `frame` 这些门从不读的成员;再往 spec 加一套窗口词汇就是反悔那个判断。spec 的删除注记与 `migrations/registry.ts` 的迁移处方里逐字写着的 `{ function, alias, partitionBy?, orderBy? }`,就是这个类型的出处,三处必须始终说同一句话。请求面的墓碑**没有**被重新打开:`analyzeQuery('o', { windowFunctions: [...] })` 依然是编译错误。 + + **顺带(#6212 批 F)**:`@objectstack/verify` 的 `BucketableDriver.aggregate` 从 `query: unknown` 收到 `DriverQuery`。这是一个**已发布**的结构替身,cloud 的 driver-turso 照着它实现——声明 `unknown` 不叫「最小」,叫没检查,并且放任该文件里两处 AST 字面量各自把对象名多写一遍(#5181 的那种冗余)。同时删掉一处 `as never`:那个 cast 只是因为字面量推断把 `'count'` 放宽成了 `string`,注上类型就不需要它了。这里**不预断**驱动自身 `aggregate` 参数类型的收窄(#6212 批 B,排在 #6203 之后)——方法参数按双变比较,驱动那边声明 `any`、`QueryAST` 还是收窄后的类型,都照样满足这个替身。 + + **零运行时改动**,全部是类型注解与两处冗余键的删除(实测全仓驱动无一读 `query.object`)。测试:driver-sql 935、driver-sqlite-wasm 254、driver-turso 804、verify 17、dogfood 520 全绿。 + + **迁移面**:直接调用这两道门的嵌入方,把内联字面量里编译器指出来的键改对即可(TS2353)。本仓实测非测试生产者为零,两道门只有各自驱动包的测试在用,零处需要改动。标 major 的依据与 #5181 / #6075 一致:**源码级破坏性**(调用点内联字面量与 `BucketableDriver` 的导出形状),运行时行为零变化;`check:api-surface` 只记录导出的存在与否、不记录签名,所以这条说明是该变更唯一的下游载体。 + +### Minor Changes + +- 82397b6: feat(drivers,objectql): `$regex` / `$options` are refused everywhere, and `$icontains` is implemented on the SQL family (#5702) + + The driver half of the #4706 ruling. #5701 landed the contract (the vocabulary, + the `RETIRED_FILTER_OPERATORS` prescriptions, the shared text case-set) and + #5710 flipped the last live producer — `plugin-auth`'s ObjectQL adapter, which + emitted `$regex` on the authentication path — so the refusal can now land + without breaking sign-in. + + **BREAKING for anyone writing `$regex` or `$options` in a filter.** Both are + refused on every backend with `INVALID_FILTER` / 400 and a message that names + the replacement. `$regex` was never a declared operator: `driver-sql` compiled + it to a LIKE-escaped substring (so `a.b` matched only the literal `a.b`), + `driver-memory` ran it as a real `RegExp` (so the same filter also matched + `axb`, and an _invalid_ pattern was caught and answered `false` — zero rows, in + silence), and `objectql`'s `having` did the same. Write `$icontains` for the + case-insensitive substring search this was almost always used for, `$contains` + for a case-sensitive one; a pattern that genuinely needs a regex has no + filter-level replacement. + + **`$icontains` now runs on the SQL family** — `driver-sql`, `driver-sqlite-wasm`, + and both of `driver-turso`'s transports (the remote one does not go through + knex, so it needed its own). It compiles to `LOWER(col) LIKE LOWER(?) ESCAPE ?` + through the same `applyLike` / `pushLike` that carries the `%` / `_` / `\` + escaping, as a `fold` parameter rather than a second emitter — a copied emitter + is where the escape class would have been dropped, and an unescaped `%` matches + every row. An empty or non-string comparand is refused on the validating walk + (an empty one matches every row, which widens rather than narrows). On SQLite + `lower()` folds ASCII only, which IS the contract (#4706 Q1 = A): `$icontains: +'café'` does not match `CAFÉ`. + + + + `driver-mongodb`'s unknown-operator arm was throwing a bare `Error` with no + `code` and no `status`, three lines from the helper in its own file that sets + `INVALID_FILTER` / 400 — a 500-shaped body for a 400-class client mistake. It + now speaks the same envelope as its three siblings. + + Two parts of the ruling are deliberately NOT in this change and stay tracked in + `scripts/check-driver-conformance.mjs`'s ledger: the `$contains` family's + case-sensitivity (#4706 Q2 = A) needs SQLite's `LIKE` replaced by a case-exact + construct in the driver, the RLS lowering and the analytics lowering together, + or one permission rule compiles to two row sets (#6518); and `$icontains` on the + JS evaluation faces needs the spec vocabulary to take the operator, which cannot + happen before `driver-memory` has an arm for it (#6520). + +- 3264516: fix(driver-sql,service-analytics)!: 两类无意义比较对象不再编译成「静默空谓词」——`$in`/`$nin` 的对象成员与 LIKE 族的对象比较值一律拒收 (#5234) + + 两个形状此前都**编译通过、执行、并给出一个作者没写过的答案**,而且没有任何东西记录这件事: + + | filter | 改前 | 改后 | + | ---------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------- | + | `{status: {$in: ['a', {foo: 1}]}}` | 该成员绑不上任何行,查询答得**就像第二个成员从没被写过** | `INVALID_FILTER` / 400,点名 `index 1` | + | `{status: {$nin: [{foo: 1}]}}` | `NOT IN ('[object Object]')` —— **一行都没排除**,作者写下的排除悄悄没发生 | 同上 | + | `{name: {$contains: {}}}` | `LIKE '%[object Object]%'` —— 对一行文本恰好是 `[object Object]` 的记录,**真的命中了** | `INVALID_FILTER` / 400,点名 `StringOperatorSchema` | + | `{name: {$notContains: {}}}` | 反过来:为一个没人记录的理由**排除了一条真实记录** | 同上 | + + #5041(PR #5223)在 `assertCompilableComparand` 的头注释里把这两个形状写为 "Deliberately NOT + extended",理由是它们 fail-closed(只收窄结果集)、比 #5041 实测的裸 `TypeError` 低一级。**实测下来这 + 两条理由都不成立**:`$nin` / `$notContains` 方向是**放宽**(该排除的没排除,在 read-scope 下即 #5347 / + #5324 判过的 over-reach);而 `$contains: {}` 给的从来不是「零行」,是**错行**。 + + ## 三份实现一起动,否则修完仍是方言 + + 同一个 `String()` 宽容在本仓有多份;只收紧 `driver-sql` 会变成「哪个面接的就是哪个答案」—— + #5146 / #5332 / #5567 各花一轮消掉的那类分叉。守卫因此落在**每个包自己的收口点**,而不是三个发射器: + + - **`driver-sql`** —— `assertCompilableComparand`,#5041 已有的那一个门。 + - **`service-analytics` 的 `where` 门** —— `filter-normalizer.ts` 的 `fieldLeaves`。它是本包**唯一**的 + leaf 生产者,所以一处拒收同时覆盖三个消费方:`NativeSQLStrategy`(真正执行的语句)、 + `ObjectQLStrategy.generateSql`(`/analytics/sql` 回显)与 `ObjectQLStrategy.convertFilter`(引擎路径)。 + 这个顺序是关键而非顺手:`convertFilter` 是**生产者**,在那里 `String()` 会把对象洗成一个类型完全正确 + 的 `'[object Object]'` 字符串交给驱动,下游再严格的驱动也永远看不到它该严格的那个形状。 + - **`service-analytics` 的 read-scope 门** —— `read-scope-sql.ts` 的 `compileOperator`,它编译的 + `FilterCondition` 不经过上面那个门。 + + `like-pattern.ts` 与 `applyLike` 里的 `String(value)` **原样保留**:它们不再是缺陷所在,因为门前已经没有 + 渲染不出来的值能到达。两包的谓词由 `like-metacharacter-escape.test.ts` 逐值互锁——正是该文件已经用来锁 + 转义表达式的同一套办法。 + + ## 围栏是 allow-list,而且每一条都是实测后决定的 + + 抄 `driver-turso` `RemoteTransport` 的形状(cloud#1004 / #1058):deny-list 会把下一个被发明出来的值形状 + 悄悄放进来,这正是那个 bug 熬过第一次修复的原因。顺带说明,**turso 自 #1058 起就已经拒收这两个形状**, + 所以本地 SQLite 与远程 SQLite 此前对同一条查询给的是不同答案;本次改动把它们收敛到一起。 + + 留在围栏内的(逐条实测,不是假设): + + - **数字 / 布尔 / `null`**:`{$contains: 5}` → `%5%`、`{$contains: null}` → `%null%` 在 `driver-sql`、 + `driver-memory` 与 analytics 两个面上**今天答案一致**,#5526 还专门把 `null` 这条钉住了。拒收它们是在 + **破坏**一致,不是建立一致——所以只拒**对象**。 + - **`Date`**:turso 的 allow-list 把它作为唯一的对象转换保留,拒收会重新叉开本地与远程。 + - **binary**:`$in` 成员照收(`isBindableComparand` 与写路径 `formatInput` 同一套分类),LIKE 拒收——它 + 绑得上但渲染不出作者想要的东西。这就是两个谓词而不是一个带 flag 的原因。 + - **`undefined`**:不可授权(JSON 没有 `undefined`),analytics 门按 #5526 / #5332 归一为 `null` 而非拒收; + 在 `driver-sql` 拒收它会**造出**一个分歧而不是消除一个,故照旧。 + + 被拒的**数组**是本次唯一一个「拒收即消分叉」的形状:`{name: {$contains: ['al','be']}}` 在 `read-scope-sql` + (与 `driver-sql`)绑 `%al,be%`,在 analytics 的 `where` 门却绑 `%al%`(它读 `values[0]`,后面的成员被 + 静默丢弃)。同一个包对同一条 filter 有两个答案,两个门现在都拒。 + + ## 作者需要知道的迁移 + + 这两个形状本来就没有能用的读法——`filter.zod.ts` 的 `StringOperatorSchema` 早就把 LIKE 族比较数声明为 + `z.string()`,本次只是让声明变成强制(Prime Directive #12,declared = enforced)。改后它们答 400 而不是 + 一个错答案;把比较数换成字面值即可。`{$eq: {…}}` **不在本次范围**,仍按 `toSqlBindValue` 绑 JSON(#5526 + 钉住的行为)。 + +### Patch Changes + +- 6f6fec7: fix(objectql,driver-sql): 自增号播种按声明的 `suffix` 定位计数器,两侧收敛到同一答案 (#6468) + + `autonumberFormat` 允许序号槽 `{0..0}` **后面**还有 token —— `renderAutonumber` + 专门返回 `suffix`,其契约就是 `prefix + zero-padded(seq) + suffix`。这类格式渲染 + 出的值**序号不在串尾**:`{000}-{YYYY}` 渲染成 `001-2026`,是很常见的单号写法。 + + 两侧的播种解析却都假定「串尾的数字就是计数器」,而且各错各的: + + - 引擎兜底播种 `seedAutonumber()` 取整串的**最后一个**数字段 —— 读到的是年份。 + 库里三行 `001-2026`/`002-2026`/`003-2026`(真实计数器 3)把计数器播种成 **2026**, + 下一个发出的号直接跳到 `2027-2026`; + - driver-sql 的 `scanMaxNumericTail()` 把 tail 里**所有**数字拼接后 `parseInt` —— + 同样三行读成 **12026**,下一个号是 `12027-2026`。 + + 于是**同一份元数据、同一批行,换个驱动号段就不一样**;中间跳过的号已经烧掉,事后 + 无法回收。只修一侧会把「两个不同的错误答案」变成「一个对一个错」,跨驱动仍不一致, + 所以两侧同 PR 修。 + + **修法:两侧解析器尊重已声明的 `prefix`/`suffix`。** 两个字符串都由调用方从 + `renderAutonumber` 的返回值取得后传入 —— 两侧都不再自行理解格式,driver-sql 只收 + 参数(`getNextSequenceValue` 仅多转发一个位置参数,序列逻辑本身未动): + + - **prefix / suffix 任一非空 ⇒ 计数器「有锚」**:取 prefix 之后的**首个**数字段, + 并在该行确实带有声明的 suffix 时先把它去掉; + - **两者皆空 ⇒ 「无锚」**:各自的既有读法**逐字保留**(引擎取整串最后一个数字段, + driver-sql 拼接全部数字)—— 无 `{0..0}` 槽的格式渲染的就是串尾裸计数器,而早于 + 格式存在的历史值根本没有锚可依。 + + **suffix 只在匹配时剥离,绝不要求匹配。** `{000}-{YYYY}` 的计数器 scope 是渲染后的 + **prefix**(此处为空),即全局一个计数器、只有显示的年份在变,所以去年的 `007-2025` + 持有计数器 7,必须计入。把 suffix 下推成 `like '%-2026'` 会把这些行整批漏掉、播种 + **低于**真实 max —— 那正是 #6249 修掉的重复单号伤害,自己再造一遍。因此 SQL 谓词 + 保持 `like 'prefix%'`,suffix 只在 JS 侧逐行使用。 + + 无后缀格式(`D-{0000}`、`{0000}`)两侧本来就正确,行为不变并已 pin 住;#6467 的 + 播种扫描结构未触碰。 + +- 3510e4a: refactor(spec,drivers,lint): one implementation of the filter identity reduction (#5659) + + `{ $and: [] }` matches every row, `{ $or: [] }` matches none, `{}` is a TRUE + disjunct that absorbs its `$or`, `{ $not: {} }` is FALSE. That is a ruling + (#5322/#5134) pinned for every backend by the four identity cases in + `FILTER_LOGIC_CASES` — and it was implemented four times over: `reduceFilterNode` + in `driver-sql`, the same function again in `driver-mongodb`, the + `every`/`some`/truthiness algebra of `driver-memory`'s matcher, and nearly a + fifth hand-written copy inside `@objectstack/lint`, which declined to write one + and filed this issue instead. + + **New in `@objectstack/spec` (`@objectstack/spec/data`): `reduceFilterVerdict`**, + beside the case table that proves it. It answers `'true' | 'false' | 'clause'` + for a filter node and never throws on its own; each backend's own refusals — the + undeclared `$`-combinator and the `undefined` comparand in `driver-sql`, the + query-level keys and the `$null` comparand in `driver-mongodb` — are passed in as + `FilterVerdictHooks` and are invoked from exactly the positions they were invoked + from before. `reduceFilterKeyVerdict` answers the same question for one key, which + is what both SQL and MongoDB emitters consult while walking a node. + + **No behaviour changes in the three drivers.** The move is mechanical: the shared + algebra replaces each private copy, the refusals stay where they were, and the + `FILTER_LOGIC_CASES` conformance suites are green on both sides of the change — + including the SQL-inheriting `driver-sqlite-wasm` and `driver-turso`. + + **`@objectstack/lint` gains two warnings it was structurally blind to.** The + `multi: true` unbounded-bulk-write rule (#5482) asked "does this filter have zero + keys", so a `delete_record` bounded by `filter: { $and: [] }` or + `filter: { $or: [{}] }` — a whole-object write by the ruling every driver executes + — passed silently. It now asks the reduction, and it warns about both while + staying quiet on `{ $or: [] }` and `{ $not: {} }`, which match nothing. The + message names the shape it saw (`a filter that REDUCES TO TRUE ({"$and":[]})`) + rather than calling a non-empty filter "empty". + + If you have a flow declaring a bulk write bounded by one of those two shapes, the + lint will now tell you so — the write was already unbounded at run time; only the + feedback is new. + +- 939f579: drivers(sql,turso): 聚合函数拒收带上 ADR-0112 信封,并把两类条件分开措辞 + + `SqlDriver.mapAggregateFunc()` 与 `RemoteTransport.aggregate()` 此前对同一条件各抛一个裸 + `Error`(`code`/`status` 皆 `undefined`),`mapDataError` 因此落默认分支——一条本该 4xx 的 + 调用方错误以不透明 500 到达客户端。两处同时改,同一信封体例、首句逐字一致(#5240): + + - **协议未声明的函数名**(如 `median`)→ `INVALID_QUERY` / 400。这正是协议门 + (`metadata-protocol` 的 `invalidQueryError`,#4254)对同一条件已经给出的码,于是 + 进程内调用方与 REST 调用方读到同一个答案。 + - **协议已声明、本后端编不出**(`count_distinct` / `array_agg` / `string_agg`)→ + `NOT_IMPLEMENTED` / 501。这是能力缺口而不是调用方的错(`driver-mongodb` 编得出这三个), + 措辞明确说明查询拼写无误,不把作者说成打错字。 + + 两面都只改拒收的身份:编得出的五个函数生成的 SQL 逐字节不变。 + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/observability@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/drivers/driver-sql/package.json b/packages/drivers/driver-sql/package.json index 758508e97d..d35d0e5560 100644 --- a/packages/drivers/driver-sql/package.json +++ b/packages/drivers/driver-sql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-sql", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "SQL Driver for ObjectStack - Supports PostgreSQL, MySQL, SQLite via Knex", "main": "dist/index.js", diff --git a/packages/drivers/driver-sqlite-wasm/CHANGELOG.md b/packages/drivers/driver-sqlite-wasm/CHANGELOG.md index 7284038054..f90b2bb900 100644 --- a/packages/drivers/driver-sqlite-wasm/CHANGELOG.md +++ b/packages/drivers/driver-sqlite-wasm/CHANGELOG.md @@ -1,5 +1,190 @@ # @objectstack/driver-sqlite-wasm +## 17.0.0-rc.6 + +### Major Changes + +- d367f03: refactor(drivers)!: 五个驱动的 query 参数跟进 `DriverQuery`,休眠的类型谎言就此没有藏身处 (#6075) + + #5181(PR #6076)把 `IDataDriver.find/findOne/count/updateMany/deleteMany/explain` 的 query 参数收窄为 `DriverQuery`(`Omit`),并在同一条 changeset 里写明:「把驱动签名一并迁到 `DriverQuery` 是后续的机械收尾」。这就是那次收尾。 + + 在此之前,五个驱动的实现仍旧声明 `query: QueryAST`(turso 侧是 `query: any`)。**它不红,也不会红** —— 方法参数按双变比较,实现声明得比契约宽照样满足契约。但调用方现在**有权**省略 `object`,于是这些实现的类型说 `query.object` 是 `string`,运行期却可能是 `undefined`:一句休眠的谎言,没有任何门拦得住下一个照着它写代码的人。 + + 收尾之后,「驱动读 `query.object`」直接变成编译错误: + + ```ts + // 收窄前:编译通过,运行期可能是 undefined —— 谎言 + // 收窄后:error TS2339: Property 'object' does not exist on type 'DriverQuery'. + const name = query.object; + ``` + + **零运行时改动。** 本次改的全部是类型注解:五个驱动的六个契约方法签名,以及为让类型自洽而必须跟进的少量私有辅助方法参数(mongodb 的 `buildFindOptions` / `buildSortSpec`,sql 的 `findRows` / `orderKeysFor`,turso 的 `toRemoteQuery` / `toRemoteReadQuery`,memory 的 `performAggregation`)—— 它们都只转发或读取 `where` / `orderBy` / `groupBy` 这些字段,本来就不读 `object`。turso 的几处 `query: any` 一并收紧,多拿回一批本已放弃的检查。emit 无差异,测试全绿(memory 524、mongodb 206、sql 906、sqlite-wasm 254、turso 788)。 + + **迁移面:删掉驱动调用字面量里的 `object:` 键**,与 #5181 是同一句话,只是现在也覆盖了直接按具体驱动类(`SqlDriver` / `MemoryDriver` / …)而非按 `IDataDriver` 取类型的调用方。编译器会逐处指出来(TS2353 `'object' does not exist in type 'DriverQuery'`)。本仓下游 25 个包实测零处需要改动,改动只落在五个驱动自己的测试里。 + + 标 major 的依据与 #5181 一致:**源码级破坏性**(调用点内联字面量),运行时行为零变化。`check:api-surface` 只记录导出的存在与否、不记录签名,因此这条说明同样是该变更唯一的下游载体。 + + `aggregate` / `distinct` / `syncSchemasBatch` 不在本次范围内 —— 它们不是 `IDataDriver` 收窄的那六个方法,其中 `syncSchemasBatch` 的条目里 `object` 是被真实读取的必填键,`expand` 条目里的 `object` 同理命名的是关联对象,都不是冗余。 + +- 62159bd: refactor(driver-sql)!: `SqlDriver.distinct` 的第三参收成裸 `FilterCondition`,一个静默返回全集的写法就此编译不过 (#6320) + + `distinct` 不在 `IDataDriver` 上,所以 #5181(PR #6076)与 #6075(PR #6210)的收窄都没走到它,#6212 批 A+E(#6355)收的是 `analyzeQuery` / `findWithWindowFunctions`,也没覆盖它。它的方法体一直说得很清楚——`applyFilters(builder, filters)` 拿的是**实参本身**,因此它要的是 `find()` 放在 `query.where` 里的那个值,**不是 query 信封**;`filters?: any` 只是没把这句话写进类型里。 + + ```ts + // 收窄前后都成立,一处调用点都不用改 + await driver.distinct("orders", "product", { status: "completed" }); + ``` + + **收窄真正买到的东西,是实测出来的,不是推断的。** 三行数据(`Laptop`/`Mouse` 为 `completed`,`Ghost` 为 `pending`),逐个形状喂给 `distinct('orders','product', …)`: + + | 第三参 | 收窄前 | 收窄后 | + | :--------------------------- | :--------------------------------- | :----------- | + | `{ status: 'completed' }` | 返回 `["Laptop","Mouse"]` | 不变 | + | 省略 | 返回全集 | 不变 | + | `'completed'`(标量) | **编译通过,返回全集** | **编译错误** | + | `{ object, where }`(信封) | 抛 `INVALID_FILTER` / 400 | 不变 | + | `['status','=','completed']` | 抛 `INVALID_FILTER` / 400(#5158) | 不变 | + + 第三行就是本次消掉的那一格:一个真心想问「completed 订单里有哪些商品」的调用,编译通过,然后拿到**每一个**商品。`applyFilters` 对「真值但非对象、非数组」的 filter 不发射任何谓词(该方法尾注写着这件事),于是过滤条件被整条丢掉。方向是**放宽**——这正是 #6320 与 #5234 同族的那类「静默错答案」。 + + **有一格是任何类型都关不上的,本次如实写进注释而不是假装关上了。** `FilterCondition` 的键**就是字段名**,所以它是开放映射(`[key: string]: any`):`{ object, where }` 在结构上是一个完全合法的 filter——约束两个分别叫 `object` 和 `where` 的列。没有任何注解能把它和正当 filter 分开。#6320 提出的「让反向错配也编译不过」在这个参数上**不可达**,实测确认;能拿到的保证是**运行期响亮失败**:信封里的 `where` 是对象,而没有任何比较值可以是对象,于是 `assertCompilableComparand` 抛 `INVALID_FILTER` / 400。这半边 driver-sql 从来就不是静默的;`driver-memory` 那半边(裸 filter 交给它会静默返回全集)留在 #5499 冻结面内,本次不碰。 + + **零运行时改动**:非测试改动 100% 是一个类型注解加一段注释,无逻辑、无行为、无 emit 差异。 + + **逐处复核了全部 14 个调用点**(本单正文记的是 3 处,实测偏低):driver-sql 11 处、driver-sqlite-wasm 3 处、driver-turso 0 处;其中真正传第三参的是 4 处(driver-sql 2 + driver-sqlite-wasm 2),全部本来就写的裸 filter,**零报错、零 fixture 改动**。 + + **driver-sqlite-wasm 也标 major**:`SqliteWasmDriver extends SqlDriver` 且不覆写 `distinct`,所以它**已发布的 `.d.ts`** 里这个方法的签名同样收窄,它的使用者看到的是同一个变化。该包读的是 driver-sql 构建后的 `dist/*.d.ts` 而非源码,是一处已知门禁盲区,本次用「往参数类型里临时塞一个调用方不可能满足的成员、重建、看调用点是否逐一变红」证明它确实读到了新 d.ts:driver-sql 6 处红、driver-sqlite-wasm 3 处红,与预判逐一相符。 + + ### 迁移 + + 调用点若把**标量**(或任何非 `FilterCondition` 值)交给第三参,编译器会指出来: + + ``` + error TS2345: Argument of type 'string' is not assignable to parameter of type 'FilterCondition'. + ``` + + 改法是把它写成它本来就该是的裸 filter 对象(`'completed'` → `{ status: 'completed' }`)。⚠️ 这类调用点在收窄前拿到的是**未过滤的全集**,所以这不是一次等价改写:修完之后返回值会变,而变化后的那个才是调用方本来想要的答案。本仓零处这样的调用点。 + + ⚠️ 无类型的 JS 调用方**既不会拿到编译错误、也不会有任何行为变化**(本次零运行时改动)。对他们而言,上面那条是「你一直没在过滤」的**唯一通知渠道** —— 这也是本次记台账条目的理由,见下。 + + + +### Minor Changes + +- 82397b6: feat(drivers,objectql): `$regex` / `$options` are refused everywhere, and `$icontains` is implemented on the SQL family (#5702) + + The driver half of the #4706 ruling. #5701 landed the contract (the vocabulary, + the `RETIRED_FILTER_OPERATORS` prescriptions, the shared text case-set) and + #5710 flipped the last live producer — `plugin-auth`'s ObjectQL adapter, which + emitted `$regex` on the authentication path — so the refusal can now land + without breaking sign-in. + + **BREAKING for anyone writing `$regex` or `$options` in a filter.** Both are + refused on every backend with `INVALID_FILTER` / 400 and a message that names + the replacement. `$regex` was never a declared operator: `driver-sql` compiled + it to a LIKE-escaped substring (so `a.b` matched only the literal `a.b`), + `driver-memory` ran it as a real `RegExp` (so the same filter also matched + `axb`, and an _invalid_ pattern was caught and answered `false` — zero rows, in + silence), and `objectql`'s `having` did the same. Write `$icontains` for the + case-insensitive substring search this was almost always used for, `$contains` + for a case-sensitive one; a pattern that genuinely needs a regex has no + filter-level replacement. + + **`$icontains` now runs on the SQL family** — `driver-sql`, `driver-sqlite-wasm`, + and both of `driver-turso`'s transports (the remote one does not go through + knex, so it needed its own). It compiles to `LOWER(col) LIKE LOWER(?) ESCAPE ?` + through the same `applyLike` / `pushLike` that carries the `%` / `_` / `\` + escaping, as a `fold` parameter rather than a second emitter — a copied emitter + is where the escape class would have been dropped, and an unescaped `%` matches + every row. An empty or non-string comparand is refused on the validating walk + (an empty one matches every row, which widens rather than narrows). On SQLite + `lower()` folds ASCII only, which IS the contract (#4706 Q1 = A): `$icontains: +'café'` does not match `CAFÉ`. + + + + `driver-mongodb`'s unknown-operator arm was throwing a bare `Error` with no + `code` and no `status`, three lines from the helper in its own file that sets + `INVALID_FILTER` / 400 — a 500-shaped body for a 400-class client mistake. It + now speaks the same envelope as its three siblings. + + Two parts of the ruling are deliberately NOT in this change and stay tracked in + `scripts/check-driver-conformance.mjs`'s ledger: the `$contains` family's + case-sensitivity (#4706 Q2 = A) needs SQLite's `LIKE` replaced by a case-exact + construct in the driver, the RLS lowering and the analytics lowering together, + or one permission rule compiles to two row sets (#6518); and `$icontains` on the + JS evaluation faces needs the spec vocabulary to take the operator, which cannot + happen before `driver-memory` has an arm for it (#6520). + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [29e28a3] +- Updated dependencies [259459d] +- Updated dependencies [6f6fec7] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d367f03] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [82397b6] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [3264516] +- Updated dependencies [2fdb36e] +- Updated dependencies [62159bd] +- Updated dependencies [d48aad5] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [939f579] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/driver-sql@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/drivers/driver-sqlite-wasm/package.json b/packages/drivers/driver-sqlite-wasm/package.json index 59097b2fd4..5b2fc84bf2 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": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 bb8c6de647..87261276f9 100644 --- a/packages/drivers/driver-turso/CHANGELOG.md +++ b/packages/drivers/driver-turso/CHANGELOG.md @@ -1,5 +1,253 @@ # @objectstack/driver-turso +## 17.0.0-rc.6 + +### Major Changes + +- 29e28a3: refactor(drivers)!: `aggregate` 的 query 参数收窄到 `DriverQuery`,并退役 `aggregate` / `func` 两个未声明别名 (#6212 批 B、#6321) + + #5181(PR #6076)收窄了 `IDataDriver` 声明的六个方法,#6075(PR #6210)让五个驱动的实现跟上,#6212 批 A+E 处理了 SQL 驱动自有的另两道门。本次是同一条线上的 `aggregate`:`driver-sql`、`driver-turso` 的转发层与 `RemoteTransport` 三处,全部从 `query: any` 收到 `DriverQuery`(`@objectstack/spec/contracts`)。 + + `any` 在 query 参数上不是「对象名没检查」,而是**检查全关**:`where` 的 filter 方言、`groupBy` 的节点联合、`aggregations` 的节点形状——而这三样恰恰是这几个方法体读的全部内容。 + + ## 一、退役两个协议从未声明的别名(#6321,ADR-0049) + + ```ts + const aggregates = query.aggregations || query.aggregate; // driver-sql + const funcName = agg.function || agg.func; + const aggregations = query?.aggregations || query?.aggregate || []; // RemoteTransport + const func = String(agg.function || agg.func || ""); + ``` + + `QueryASTSchema` 声明的是 `aggregations`,`AggregationNodeSchema` 声明的是 `function`;`aggregate` / `func` 在 `packages/spec` 里**一个字都没有**。实测全仓唯一书写者是这两个驱动包自己的 fixture(`sql-driver-advanced` 7 处、`sql-driver-queryast` 1 处、`sqlite-wasm-driver-advanced` 7 处、`sqlite-wasm-driver-queryast` 1 处),非测试面零书写者——#4984 那一家:**fixture 拼着别名,宽容分支就永远绿着活下去,没有任何测试能在删掉它时转红**。fixture 已按已声明拼写重拼,写者归零,PD#12 与 ADR-0049 enforce-or-remove 于是把这两条 `||` 一并删掉。 + + 顺带删掉的还有 `|| ''`:它只在**两个键都没写**时才生效,而那时这一面把名字回引成 `""`、本地面回引成 `"undefined"`,同一份越界输入两种措辞(#5240)。别名在时这条岔路够不着,删别名恰恰让它够得着,所以同一次关掉。 + + **迁移**:`aggregate:` → `aggregations:`,`func:` → `function:`。写旧拼写的内联字面量现在是编译错误(TS2353);越过 `tsc` 的 JS 调用方,`aggregate:` 会静默拿不到聚合列,`func:` 则拿到已有的具名 400(`INVALID_QUERY`,#5907)。本仓实测需要改动的非测试调用点为零。 + + ## 二、一处真实行为改动:`RemoteTransport` 现在会编 `GroupByNode` 联合 + + `GroupByNodeSchema` 是 `z.union([z.string(), z.object({ field, dateGranularity?, alias? })])`,而这一层把它当 `string[]` 读。收窄后 `tsc` 直接把这条假设摆上台面(TS2322)。联合的两半状况完全不同,所以这不是一个 cast 能了事的: + + - **无 granularity 的结构化条目**(`{ field: 'region' }`)是 spec 合法、且**今天就会下推到驱动**的形状:objectql 的 aggregate 派发对它一律判为「受支持」(`engine.ts` 里逐字写着 `plain {field} object is fine`),`objectql/src/secret-fields.test.ts:341` 就是这个形状的活体。本驱动的**本地面**把它编成普通的 `GROUP BY "region"`,远端面却把它插值成 `"[object Object]"`、死在标识符安全检查里——一条查询两种答案、由连接串决定,正是 #6203 那个形状,而且**是活体不是休眠**:能力位 `queryDateGranularity` 只管带 granularity 的那一半,管不到这一半。现在读 `.field`,两面收敛。 + - **带 dateGranularity 的条目**远端确实编不出来,而这一点是**已声明**的:remote 模式发布 `queryDateGranularity: {}`,引擎据此全部落到内存分桶,因此不会下推。缺的是「绕过能力位、直连驱动」的那个调用方该得到什么答案——现在得到 ADR-0112 信封(`NOT_IMPLEMENTED` / 501),与聚合函数「协议已声明、本后端编不出」用的是同一类,而不是一句 SQL 注入告警。 + + `alias` **不读**,与本地面一致:`SqlDriver.aggregate` 也不读它,只在这一面读会是新的分叉而不是修复。 + + ## 三、`SqlDriver` 那一面的同一条件也换上了信封 + + `SqlDriver.aggregate` 对「本方言编不出这个 granularity」原本抛裸 `Error`(`code`/`status` 皆 `undefined` ⇒ `mapDataError` 落默认分支,一个具名能力缺口以不透明 500 到达调用方)。只给远端面加信封就会造出 #5907 花一整个 issue 才关掉的那种分叉——`TursoDriver` 由 `url` 选面,同一条件不能有两种线上身份。两面首句逐字一致(`Date bucketing by '' is not supported by this backend.`),尾句各报**本面**编得出的 granularity,由一条跨包 parity 用例比对两个**运行时**消息钉住。 + + **消息文本变更**(可能影响按文本匹配的下游断言): + + ``` + - SqlDriver: dateGranularity 'week' not supported on dialect 'better-sqlite3'. Engine must fall back to in-memory bucketing. + + Date bucketing by 'week' is not supported by this backend. Bucketed here: day, month, quarter, year (dialect 'better-sqlite3'). … (code=NOT_IMPLEMENTED, status=501) + ``` + + ## 定级依据 + + 标 major 与 #5181 / #6075 / #6210 一致:**源码级破坏性**(调用点内联字面量、以及被删的两个别名键),加上第二、三节两处真实的运行期改动。`check:api-surface` 只记录导出的存在与否、不记录签名,所以这条说明是该变更唯一的下游载体。 + + `driver-sqlite-wasm` 未列入:它整个继承 `SqlDriver.aggregate`,自身源码零改动(改的只有它的 fixture 与一条断言)——与批 A+E 的处理一致。它读的是 driver-sql 的 `dist/*.d.ts`,因此验证时**必须先重建 driver-sql** 再 typecheck/test,否则是假绿。 + + + +- d367f03: refactor(drivers)!: 五个驱动的 query 参数跟进 `DriverQuery`,休眠的类型谎言就此没有藏身处 (#6075) + + #5181(PR #6076)把 `IDataDriver.find/findOne/count/updateMany/deleteMany/explain` 的 query 参数收窄为 `DriverQuery`(`Omit`),并在同一条 changeset 里写明:「把驱动签名一并迁到 `DriverQuery` 是后续的机械收尾」。这就是那次收尾。 + + 在此之前,五个驱动的实现仍旧声明 `query: QueryAST`(turso 侧是 `query: any`)。**它不红,也不会红** —— 方法参数按双变比较,实现声明得比契约宽照样满足契约。但调用方现在**有权**省略 `object`,于是这些实现的类型说 `query.object` 是 `string`,运行期却可能是 `undefined`:一句休眠的谎言,没有任何门拦得住下一个照着它写代码的人。 + + 收尾之后,「驱动读 `query.object`」直接变成编译错误: + + ```ts + // 收窄前:编译通过,运行期可能是 undefined —— 谎言 + // 收窄后:error TS2339: Property 'object' does not exist on type 'DriverQuery'. + const name = query.object; + ``` + + **零运行时改动。** 本次改的全部是类型注解:五个驱动的六个契约方法签名,以及为让类型自洽而必须跟进的少量私有辅助方法参数(mongodb 的 `buildFindOptions` / `buildSortSpec`,sql 的 `findRows` / `orderKeysFor`,turso 的 `toRemoteQuery` / `toRemoteReadQuery`,memory 的 `performAggregation`)—— 它们都只转发或读取 `where` / `orderBy` / `groupBy` 这些字段,本来就不读 `object`。turso 的几处 `query: any` 一并收紧,多拿回一批本已放弃的检查。emit 无差异,测试全绿(memory 524、mongodb 206、sql 906、sqlite-wasm 254、turso 788)。 + + **迁移面:删掉驱动调用字面量里的 `object:` 键**,与 #5181 是同一句话,只是现在也覆盖了直接按具体驱动类(`SqlDriver` / `MemoryDriver` / …)而非按 `IDataDriver` 取类型的调用方。编译器会逐处指出来(TS2353 `'object' does not exist in type 'DriverQuery'`)。本仓下游 25 个包实测零处需要改动,改动只落在五个驱动自己的测试里。 + + 标 major 的依据与 #5181 一致:**源码级破坏性**(调用点内联字面量),运行时行为零变化。`check:api-surface` 只记录导出的存在与否、不记录签名,因此这条说明同样是该变更唯一的下游载体。 + + `aggregate` / `distinct` / `syncSchemasBatch` 不在本次范围内 —— 它们不是 `IDataDriver` 收窄的那六个方法,其中 `syncSchemasBatch` 的条目里 `object` 是被真实读取的必填键,`expand` 条目里的 `object` 同理命名的是关联对象,都不是冗余。 + +### Minor Changes + +- 82397b6: feat(drivers,objectql): `$regex` / `$options` are refused everywhere, and `$icontains` is implemented on the SQL family (#5702) + + The driver half of the #4706 ruling. #5701 landed the contract (the vocabulary, + the `RETIRED_FILTER_OPERATORS` prescriptions, the shared text case-set) and + #5710 flipped the last live producer — `plugin-auth`'s ObjectQL adapter, which + emitted `$regex` on the authentication path — so the refusal can now land + without breaking sign-in. + + **BREAKING for anyone writing `$regex` or `$options` in a filter.** Both are + refused on every backend with `INVALID_FILTER` / 400 and a message that names + the replacement. `$regex` was never a declared operator: `driver-sql` compiled + it to a LIKE-escaped substring (so `a.b` matched only the literal `a.b`), + `driver-memory` ran it as a real `RegExp` (so the same filter also matched + `axb`, and an _invalid_ pattern was caught and answered `false` — zero rows, in + silence), and `objectql`'s `having` did the same. Write `$icontains` for the + case-insensitive substring search this was almost always used for, `$contains` + for a case-sensitive one; a pattern that genuinely needs a regex has no + filter-level replacement. + + **`$icontains` now runs on the SQL family** — `driver-sql`, `driver-sqlite-wasm`, + and both of `driver-turso`'s transports (the remote one does not go through + knex, so it needed its own). It compiles to `LOWER(col) LIKE LOWER(?) ESCAPE ?` + through the same `applyLike` / `pushLike` that carries the `%` / `_` / `\` + escaping, as a `fold` parameter rather than a second emitter — a copied emitter + is where the escape class would have been dropped, and an unescaped `%` matches + every row. An empty or non-string comparand is refused on the validating walk + (an empty one matches every row, which widens rather than narrows). On SQLite + `lower()` folds ASCII only, which IS the contract (#4706 Q1 = A): `$icontains: +'café'` does not match `CAFÉ`. + + + + `driver-mongodb`'s unknown-operator arm was throwing a bare `Error` with no + `code` and no `status`, three lines from the helper in its own file that sets + `INVALID_FILTER` / 400 — a 500-shaped body for a 400-class client mistake. It + now speaks the same envelope as its three siblings. + + Two parts of the ruling are deliberately NOT in this change and stay tracked in + `scripts/check-driver-conformance.mjs`'s ledger: the `$contains` family's + case-sensitivity (#4706 Q2 = A) needs SQLite's `LIKE` replaced by a case-exact + construct in the driver, the RLS lowering and the analytics lowering together, + or one permission rule compiles to two row sets (#6518); and `$icontains` on the + JS evaluation faces needs the spec vocabulary to take the operator, which cannot + happen before `driver-memory` has an arm for it (#6520). + +### Patch Changes + +- e195092: test(verify): `checkDateBucketParity` / `checkReadCoercion` 的调用点不再 `as never`,替身的编译期检查恢复生效 (#6354) + + `@objectstack/verify` 用 `BucketableDriver` / `CoercibleDriver` 两个**结构替身**表达「被测驱动确实具备这组方法」。这是一个**已发布**的契约面——仓外驱动(cloud 的 `driver-turso`)照着它实现自己的一致性测试。但全仓 **10 个**调用点无一例外把驱动 `as never` 之后再传进去,于是这件事**一次也没有被检查过**:替身存在的全部意义,被 100% 的调用点关掉了。 + + 本次逐处删掉这 10 个 cast,一个不留: + + - `packages/qa/dogfood/test/date-bucket-parity-conformance.test.ts` **6 处**(真实 `SqlDriver` / `SqliteWasmDriver` 1 处,负向控制的假驱动 5 处) + - `packages/drivers/driver-turso/src/date-bucket-parity.test.ts` **2 处**(`TursoDriver` 本地模式 + 那条 `week` 绊线) + - `packages/qa/dogfood/test/read-coercion-conformance.test.ts` **2 处**(`checkReadCoercion` 同族,同形且同样是死 cast) + + **零运行时改动,零新增逃逸口**——只删不加,全程未引入任何 `as any` / `as unknown as` / `@ts-expect-error` / `as never`。三个包 typecheck 全绿:这些 cast 每一个都是死的,替身与真实驱动的形状本来就一致,被抹掉的只是**说出这件事**的能力。 + + 代价原本是休眠的,也正因为休眠才值得修:哪天某个驱动少掉替身要求的一个方法、或替身自身长出新成员,10 个调用点一个都不会红,`checkDateBucketParity` 会在运行期抛 `driver.aggregate is not a function`,而不是在 `tsc` 里被拦下。对仓外驱动作者而言,这个替身是他们唯一能对照的形状说明书,而说明书此前从不校验。 + +- 939f579: drivers(sql,turso): 聚合函数拒收带上 ADR-0112 信封,并把两类条件分开措辞 + + `SqlDriver.mapAggregateFunc()` 与 `RemoteTransport.aggregate()` 此前对同一条件各抛一个裸 + `Error`(`code`/`status` 皆 `undefined`),`mapDataError` 因此落默认分支——一条本该 4xx 的 + 调用方错误以不透明 500 到达客户端。两处同时改,同一信封体例、首句逐字一致(#5240): + + - **协议未声明的函数名**(如 `median`)→ `INVALID_QUERY` / 400。这正是协议门 + (`metadata-protocol` 的 `invalidQueryError`,#4254)对同一条件已经给出的码,于是 + 进程内调用方与 REST 调用方读到同一个答案。 + - **协议已声明、本后端编不出**(`count_distinct` / `array_agg` / `string_agg`)→ + `NOT_IMPLEMENTED` / 501。这是能力缺口而不是调用方的错(`driver-mongodb` 编得出这三个), + 措辞明确说明查询拼写无误,不把作者说成打错字。 + + 两面都只改拒收的身份:编得出的五个函数生成的 SQL 逐字节不变。 + +- 67e935c: drivers(turso): remote 聚合函数名不再大小写归一化,两面只认协议声明的小写拼写 (#6203) + + `TursoDriver` 按连接串 `url` 选面:本地/副本继承 `SqlDriver`,远程委派 `RemoteTransport`。 + 两面此前对聚合函数名的归一化不一致 —— remote 先 `.toLowerCase()` 再查自己的编译表,local + 拿到什么查什么。于是同一个驱动、同一条查询,答案取决于连接串: + + ``` + COUNT REMOTE -> RESOLVED "SELECT count(\"stage\") AS \"n\" FROM \"deal\"" + LOCAL -> THREW INVALID_QUERY / 400 + ``` + + 本次删掉 remote 侧的 `.toLowerCase()`。`AggregationFunction` 是**大小写敏感**的 `z.enum` + (`AggregationFunction.parse('COUNT')` 直接抛错),`COUNT` 是协议从未声明的拼写,remote + 多认的是一种私有方言;按契约优先(PD#12)收紧消费端,而不是把方言固化成第二套事实契约。 + + **升级说明(user-visible)**:remote 连接不再接受大写或混合大小写的聚合函数名。 + `COUNT` / `Count` / `SUM` 等此前在 remote 能编出 SQL 的拼写,现在与 local 一样统一落 + `INVALID_QUERY` / 400(「不是已声明的聚合函数」)。**作者侧修法是改用小写** —— 把 + `aggregations[].function` 写成协议声明的 `count` / `sum` / `avg` / `min` / `max` + (以及已声明但本后端未实现的 `count_distinct` / `array_agg` / `string_agg`)。 + + 经 REST/协议门进来的查询不受影响:大写拼写在 `AggregationNodeSchema` 就被拒,到不了驱动; + 仓内亦无任何发送大写拼写的调用方。受影响的只有绕过 spec 校验、直接调用远程驱动且依赖该 + 归一化的进程内调用方。 + + `#5907` 落地的拒收信封(第 1 类 `INVALID_QUERY`/400、第 2 类 `NOT_IMPLEMENTED`/501、 + 按调用方原始拼写分类)与默认 alias 的拼法均未改动。 + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [29e28a3] +- Updated dependencies [259459d] +- Updated dependencies [6f6fec7] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d367f03] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [82397b6] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [3264516] +- Updated dependencies [2fdb36e] +- Updated dependencies [62159bd] +- Updated dependencies [d48aad5] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [939f579] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/driver-sql@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/drivers/driver-turso/package.json b/packages/drivers/driver-turso/package.json index 5eae07a3d7..fdad4bc265 100644 --- a/packages/drivers/driver-turso/package.json +++ b/packages/drivers/driver-turso/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-turso", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 c0ed3aaed1..29f51e14aa 100644 --- a/packages/formula/CHANGELOG.md +++ b/packages/formula/CHANGELOG.md @@ -1,5 +1,186 @@ # @objectstack/formula +## 17.0.0-rc.6 + +### Minor Changes + +- 6965160: feat(lint): view/page 可见性谓词的裸标识符构建期闸门 —— 坏谓词发不出去(#6128) + + 新增 **error 级** 规则 `visibility-bare-identifier`:view/page 的可见性谓词 + (`visibleWhen` 及其两个已弃用别名 `visibleOn` / `visibility`)里引用了任何绑定根都解析不到的 + 顶层标识符时,`os validate` / `os build` / `os lint` 一律拒收。写成 `status == 'active'` + 而不是 `record.status == 'active'` 的谓词,从此发不出去。 + + 按 #5149 维护者 2026-08-06 裁决的构建期半边落地(运行时 warn-once 半边已由 objectui#3541 合入)。 + 本仓传统的准确表述是:fail-open 或 fail-closed 都可以裁,**静默不可以**。谓词失败仍然 fail-open + (已发货 app 行为不变),但坏谓词不再能进入产物。 + + **为什么现有两道闸都放行**(#5149 Repro 1 实测,已写进规则注释,防后人误并): + ADR-0032 的标识符闸(`validate-expressions.ts`)解析 record 作用域的裸引用,但它的遍历只覆盖 + objects / flows / actions / sharingRules / hooks,**从不走 views 与 pages**;ADR-0089 D3b + 只判**有根**的谓词根错层(runtime 面的 `data.`、metadata 面的 `record.`),**无根**的谓词两边都不匹配。 + 两闸之间正好漏掉「作者按文档示例写了裸字段名 → 谓词永远解析失败 → 控制台 fail-open 静默显示」。 + + **判定由两个既有 oracle 合成,本包不自建 CEL 环境**(#4812 的教训):声明性判定取 + `@objectstack/formula` 的 `firstUndeclaredReference`(即 `validateExpression` 给 record 作用域 + 裸引用定罪的同一个严格环境),AST 取规范入口 `parseCelToAst`。AST 先收集所有处于**接收者位置** + 的标识符(`a.b` / `a?.b` / `a['b']` / `a.exists(…)`)并在检查前声明它们,于是只剩「当作裸值引用」 + 的标识符会被判 —— 未知**根**(`my_record.x`)交还给 ADR-0089 D3b,不在本规则射程内。 + + **与 #4953(全量 vs 稀疏绑定)的边界**:#4953 实测同一求值器在两种绑定下语义相反 + (`has(record.a)` 全量 true / 稀疏 false;`record.a != null` 全量 false / 稀疏 FAULT)。本规则 + **按构造与该分叉无关** —— 它从不追问某个 KEY 在已绑定的根上是否存在,只追问标识符有没有根, + 而无根标识符在两种绑定下都解析不到。`has(record.x)` / `record.x != null` 等守卫写法在本闸门下 + 一律绿,无论 #4953 最终怎么裁;已加测试钉住这条边界。 + + **遍历按实测修正,否则规则生来即死**:`os build` 跑 `examples/app-showcase` 得到的唯一一条 + view 表单谓词落在 `views[0].formViews.edit.sections[0].fields[6].visibleWhen` —— 运行时 app 形状下 + `views[]` 条目是**视图容器**(`ViewSchema` 声明的自有键就是 `list` / `form` / `listViews` / + `formViews`),`sections` 在下一层。原遍历只读 `views[].sections`,在这份 stack 上报告「干净」。 + 现在覆盖容器的 `form` 与每个 `formViews.`,以及仍然直接携带 `sections` 的 `defineForm` 形状; + pages 改走共享的 `walkPageComponents`(regions、slotted 页的 `slots`、以及 `properties` 里的 + `page:tabs` / `page:accordion` / `page:card` 子树都随之覆盖,source-authored 页按其既有语义跳过)。 + `objects[].views` 明确不读 —— 该键已被 schema 立碑拒绝,读它只会造出一条永不触发的幽灵检查。 + 两条既有 ADR-0089 D3b advisory 随遍历一并变得真正可达。 + + 注册表 tier `advisory` → `gating`(#5762 的先例):tier 声明并非自述, + `authoring-rule-wiring.test.ts` 会读规则源码核对。 + + 已知盲点(已钉测试、方向安全):字段名与 CEL **类型名**相同时(`type` / `int` / `string` / `list` + / `map` / `timestamp` …)不判 —— CEL 自身声明这些标识符,`type == 'grid'` 到检查器那里是类型 + overload 错误而非未知变量;改读 overload 消息会误杀合法的 `type(record.x) == string`。语法不通过 + 的谓词同样不判,交还给拥有该判定的闸门。两者都是漏判,永远不会变成误红。 + + 仓内 `app-todo` / `app-crm` / `app-showcase` 三个示例 `os validate` 全部通过、零 visibility finding, + 无需修改任何示例内容。 + + `@objectstack/formula` 侧:公开导出 `firstUndeclaredReference`(理由与既有的 + `collectCelRootIdentifiers` 一致 —— 绑定根集合不同的消费方需要的是同一个答案,替代方案是在消费方 + 自建严格 `Environment`,而那正是 #4812 从本包消费方手里拿掉的私有前端)。 + +### Patch Changes + +- 07c68b0: fix(formula): 括号/引号/转义等 parse 期错误不再被误报为 `runtime` + + `celEngine` 的错误分类此前完全靠**错误文案关键词**判定,而 cel-js 8.0.0 的 parse 期错误有约 19 种措辞,只有 3 种含 `parse` / `unexpected` / `syntax`。其余整类 —— 最典型的括号/方括号/花括号不配对(`Expected RPAREN, got EOF`)、未闭合字符串、非法转义、保留字 —— 全部落到默认值 `runtime`。 + + `kind` 不是内部字段:它被原样拼进作者可见的写入拒绝文案(`@objectstack/objectql` 的 `rule-validator` / `cel-fault`)与 REST 错误响应体的 `reason`。少写一个右括号的校验规则,作者读到的是 `(runtime: …)` —— 指向数据与求值期,而真正该改的是表达式本身,与 ADR-0032 D1d 的"消息面向自纠"相悖。 + + 改为按 cel-js 抛出的**错误类**判定:`ParseError` → `parse`(其中 `code: 'limit_exceeded'` 仍 → `bounds`,cel-js 的越界一律由 parser 抛出)。这一层不再读文案,因此也修掉了关键词方案无法修的一格:cel-js 会把**作者自己的源码行**嵌进 `message`(`formatErrorWithHighlight`),于是字段名能决定错误分类 —— 实测 `((record.type_id)` 这条普通的括号不配对,此前被判为 `type`,只因回显的源码里含子串 "type"。 + + `type` / `runtime` 两支暂仍走原关键词表:cel-js 的 `TypeChecker` 按**阶段**而非按故障选择错误类(`isEvaluating ? evaluationError : typeError`),同一个 `unknown_variable` 在 check 期是 `TypeError`、在 eval 期是 `EvaluationError`,整体结构化会改变这些既有判定。审计见 #6133。 + + kind 词表本身(`parse` / `type` / `runtime` / `bounds` / `dialect`)未变,消费方未改。 + +- e9b5265: fix(formula,lint): `current_user` becomes a declared root, and its field-level rejection becomes a real rule (#6290) + + `@objectstack/formula` told two stories about one root. `introspectScope` handed + `current_user` to authors as a legal namespace and `checkRoleCatalog`'s four + position-membership regexes all lead with it — both correct, because ADR-0068 D1 + makes `current_user` THE canonical spelling and `buildScope` really does mount + the same `EvalUser` under it. Only `cel-engine.ts`'s `SCOPE_ROOTS` disagreed, so + the strict environment read the blessed spelling as a BARE FIELD REFERENCE while + its two aliases (`user`, `ctx`) passed unremarked. + + Three things change. + + **1. `SCOPE_ROOTS` declares `current_user`.** That list is a "never faults" + baseline, not a per-surface contract, and it now advertises exactly what the + package advertises elsewhere. A new pin asserts the property directly: every + root `introspectScope` reports must resolve in the strict env. + + **2. The wrong prescription is gone.** Because the rejection used to fall out of + the baseline's omission, the author got the GENERIC bare-field diagnostic — + "Write `record.current_user`". That shape binds on no layer of the platform, so + an author who followed the message ended up with something strictly worse than + what they started with, still silent. The field-level verdict now comes from a + rule of its own in `@objectstack/lint`, which names the real failure (unbound ⇒ + fault ⇒ visibility falls back to `true` ⇒ the field a `current_user` test was + meant to hide stays visible for everyone, #6146) and prescribes surfaces that + exist: move the predicate to the option's own `visibleWhen`, declare field-level + security on a permission set (`fields: { '.': { readable: false } }`), + or rewrite it against `record`. It covers `visibleWhen`, `readonlyWhen` and + `requiredWhen`, which share the one evaluator. + + **3. Per-option `visibleWhen` is validated at all.** `validate-expressions.ts` + walked field-level conditional rules and stopped there, so `SelectOption.visibleWhen` + — an authorable CEL slot the client filters on AND the server enforces — reached + compile, validate and run time checked by nobody. A bare field reference, a + reference to a field that does not exist, a syntax error or a template-dialect + predicate in an option all shipped in silence, and the option simply never + offered itself. Options are now walked, located by option value, on the same + `record` scope as their host field. + + The two surfaces deliberately give opposite verdicts on `current_user`, because + their evaluators differ: field-level rules go through `evalFieldPredicate` + (`record` + `previous` + `parent`, never a user), options through + `resolveCascadingOptions` against the host's predicate scope, which does bind it + (ADR-0068 / objectui#2284). The showcase's role-gated option + (`'admin' in current_user.positions`) had never met this rule before and is now + pinned as the legal usage it is. + + Sweep: `objectstack validate` is clean on all three example apps + (`app-showcase`, `app-crm`, `app-todo`) with the option walk active — zero new + findings, including the showcase object that carries both a record-scoped + cascade and the role-gated option. + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/formula/package.json b/packages/formula/package.json index 9d57074d58..dc47775f18 100644 --- a/packages/formula/package.json +++ b/packages/formula/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/formula", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "ObjectStack canonical expression engine — CEL (cel-js) + ObjectStack stdlib + dialect registry", "main": "dist/index.js", diff --git a/packages/lint/CHANGELOG.md b/packages/lint/CHANGELOG.md index d457c08783..00f392c0bd 100644 --- a/packages/lint/CHANGELOG.md +++ b/packages/lint/CHANGELOG.md @@ -1,5 +1,537 @@ # @objectstack/lint +## 17.0.0-rc.6 + +### Minor Changes + +- 6965160: feat(lint): view/page 可见性谓词的裸标识符构建期闸门 —— 坏谓词发不出去(#6128) + + 新增 **error 级** 规则 `visibility-bare-identifier`:view/page 的可见性谓词 + (`visibleWhen` 及其两个已弃用别名 `visibleOn` / `visibility`)里引用了任何绑定根都解析不到的 + 顶层标识符时,`os validate` / `os build` / `os lint` 一律拒收。写成 `status == 'active'` + 而不是 `record.status == 'active'` 的谓词,从此发不出去。 + + 按 #5149 维护者 2026-08-06 裁决的构建期半边落地(运行时 warn-once 半边已由 objectui#3541 合入)。 + 本仓传统的准确表述是:fail-open 或 fail-closed 都可以裁,**静默不可以**。谓词失败仍然 fail-open + (已发货 app 行为不变),但坏谓词不再能进入产物。 + + **为什么现有两道闸都放行**(#5149 Repro 1 实测,已写进规则注释,防后人误并): + ADR-0032 的标识符闸(`validate-expressions.ts`)解析 record 作用域的裸引用,但它的遍历只覆盖 + objects / flows / actions / sharingRules / hooks,**从不走 views 与 pages**;ADR-0089 D3b + 只判**有根**的谓词根错层(runtime 面的 `data.`、metadata 面的 `record.`),**无根**的谓词两边都不匹配。 + 两闸之间正好漏掉「作者按文档示例写了裸字段名 → 谓词永远解析失败 → 控制台 fail-open 静默显示」。 + + **判定由两个既有 oracle 合成,本包不自建 CEL 环境**(#4812 的教训):声明性判定取 + `@objectstack/formula` 的 `firstUndeclaredReference`(即 `validateExpression` 给 record 作用域 + 裸引用定罪的同一个严格环境),AST 取规范入口 `parseCelToAst`。AST 先收集所有处于**接收者位置** + 的标识符(`a.b` / `a?.b` / `a['b']` / `a.exists(…)`)并在检查前声明它们,于是只剩「当作裸值引用」 + 的标识符会被判 —— 未知**根**(`my_record.x`)交还给 ADR-0089 D3b,不在本规则射程内。 + + **与 #4953(全量 vs 稀疏绑定)的边界**:#4953 实测同一求值器在两种绑定下语义相反 + (`has(record.a)` 全量 true / 稀疏 false;`record.a != null` 全量 false / 稀疏 FAULT)。本规则 + **按构造与该分叉无关** —— 它从不追问某个 KEY 在已绑定的根上是否存在,只追问标识符有没有根, + 而无根标识符在两种绑定下都解析不到。`has(record.x)` / `record.x != null` 等守卫写法在本闸门下 + 一律绿,无论 #4953 最终怎么裁;已加测试钉住这条边界。 + + **遍历按实测修正,否则规则生来即死**:`os build` 跑 `examples/app-showcase` 得到的唯一一条 + view 表单谓词落在 `views[0].formViews.edit.sections[0].fields[6].visibleWhen` —— 运行时 app 形状下 + `views[]` 条目是**视图容器**(`ViewSchema` 声明的自有键就是 `list` / `form` / `listViews` / + `formViews`),`sections` 在下一层。原遍历只读 `views[].sections`,在这份 stack 上报告「干净」。 + 现在覆盖容器的 `form` 与每个 `formViews.`,以及仍然直接携带 `sections` 的 `defineForm` 形状; + pages 改走共享的 `walkPageComponents`(regions、slotted 页的 `slots`、以及 `properties` 里的 + `page:tabs` / `page:accordion` / `page:card` 子树都随之覆盖,source-authored 页按其既有语义跳过)。 + `objects[].views` 明确不读 —— 该键已被 schema 立碑拒绝,读它只会造出一条永不触发的幽灵检查。 + 两条既有 ADR-0089 D3b advisory 随遍历一并变得真正可达。 + + 注册表 tier `advisory` → `gating`(#5762 的先例):tier 声明并非自述, + `authoring-rule-wiring.test.ts` 会读规则源码核对。 + + 已知盲点(已钉测试、方向安全):字段名与 CEL **类型名**相同时(`type` / `int` / `string` / `list` + / `map` / `timestamp` …)不判 —— CEL 自身声明这些标识符,`type == 'grid'` 到检查器那里是类型 + overload 错误而非未知变量;改读 overload 消息会误杀合法的 `type(record.x) == string`。语法不通过 + 的谓词同样不判,交还给拥有该判定的闸门。两者都是漏判,永远不会变成误红。 + + 仓内 `app-todo` / `app-crm` / `app-showcase` 三个示例 `os validate` 全部通过、零 visibility finding, + 无需修改任何示例内容。 + + `@objectstack/formula` 侧:公开导出 `firstUndeclaredReference`(理由与既有的 + `collectCelRootIdentifiers` 一致 —— 绑定根集合不同的消费方需要的是同一个答案,替代方案是在消费方 + 自建严格 `Environment`,而那正是 #4812 从本包消费方手里拿掉的私有前端)。 + +- ecff951: feat(lint): view/page 可见性谓词的 CEL 语法构建期闸门 —— `country === "USA"` 不再零诊断(#6253) + + 新增 **error 级** 规则 `visibility-predicate-syntax`:view/page 的可见性谓词 + (`visibleWhen` 及其两个已弃用别名 `visibleOn` / `visibility`)如果规范 CEL 前端根本 + 解析不了,`os validate` / `os build` / `os lint` 一律拒收。`===` 这类写法从此发不出去。 + + 按维护者 2026-08-07 对 #6253 的裁定落地:**判 blocking error**,与其它谓词面 + (validation rule / flow / action,ADR-0032)同级;不设 warning 档,也不为本面写豁免—— + warning 在 CI 里通常不拦,那只是「多绕几步的静默」。 + + **为什么这一面此前无人判**:`validate-expressions.ts`(ADR-0032)对它遍历到的每条谓词都跑 + `validateExpression`,语法错报 blocking error——但它的遍历面是 objects / flows / actions / + sharingRules / hooks,**从不走 `views` 与 `pages`**。走这一面的三条规则(ADR-0089 D3b 两条 + advisory,加 #6128 的裸标识符闸)都明确不判语法,理由是「不发明第二个语法判定」。那条政策 + 在它自己的调用点上成立(`validateExpression` 就在同一批调用点上跑),**在 view/page 面上不成立: + 那里没有第二个判定,沉默就是没人报**。后果与 #5149 同型:谓词求值失败 → `evalFieldPredicate` + 返回 fallback → 可见性 fallback 是 `true` → 元素无条件渲染,与「没写谓词」在屏幕上一模一样。 + `packages/spec/src/ui/view.test.ts` 的 fixture 就写着 `'country === "USA"'`,正说明这是作者 + (尤其 AI)会写出来的形状。 + + **判定仍然不是本包给的**——旧政策要保护的正是这一点,它完整保留:判定取 `parseCelToAst` + (规范前端,带 #3306 改写与 `DEFAULT_LIMITS`,#4812),本规则不自建 `Environment`、不手写 + tokenizer。#6253 加的是**对既有判定的上报**,外加原始报错缺的自纠措辞:cel-js 只说 + `Unexpected character: =` 并画一个 caret,既没点名作者写的运算符,也没给出 CEL 的写法。 + + **明确不走 `validateExpression` / `celEngine.compile`**,尽管那才是 ADR-0032 的入口: + `compile()` 是 parse **+ 类型检查**,差别不是理论上的——实测它会以 + `no such overload: type == string` 拒掉 `type == 'grid'`,而那正是本文件**已钉测试的既有盲点** + (字段名与 CEL 类型名相同时不判,因为改读 overload 消息会误杀合法的 `type(record.x) == string`)。 + 从语法分支绕过去会把那条决定悄悄推翻,并把一条 error 级闸门从「解析不了」扩张成「类型检查不过」—— + 而这一面的谓词绝大多数是 `dyn`。裁定说的是语法,parse 判定恰好就是语法。 + + **消息自纠**:实测过的非 CEL 拼法各自点名并给出 CEL 写法——`===`→`==`、`!==`→`!=`、 + `<>`→`!=`、`and`→`&&`、`or`→`||`、`not`→`!`、单个 `=`→`==`。扫描前先把字符串字面量抹平, + 所以 `record.msg == 'a === b' and record.n > 1` 归咎于 `and` 而不是字面量里的 `===`; + `record.msg == 'a === b'` 本身能解析,压根不报。`??` 与 SQL 的 `IN (…)` 故意不进表:两者 + 都会解析失败、都照报(带前端原话),但都没有「换一个 token」就能修好的等价写法,给半个修法 + 只会让作者多跑一趟。 + + **边界**(均已钉测试):空/纯空白谓词不是语法错(`parseCelToAst` 对空源也返回 `null`, + 没有这道 guard 会把「没写谓词」报成坏 CEL);`DEFAULT_LIMITS` 超限属于**边界**错而非语法错, + 照报但引用前端原话、不假装找到了 typo,与 ADR-0032 把两者一并归入「invalid CEL predicate」 + 的既有做法一致,且超长谓词在消息里省略,单条 runaway 表达式刷不满控制台;一条坏谓词**只出一个 + finding**——源码解析不出 AST 就没有标识符可判,裸标识符闸自动让位,该互斥性由「断言整个上报集合」 + 钉住而不是靠调用方内部实现。 + + 注册表无需改动:`validateVisibilityPredicates` 的 tier 在 #6128 已是 `gating`、commands 已是 + build/lint/validate,本规则的 `error` 直接沿用(已加测试复核该前提仍然成立)。 + + 仓内清扫:除 `packages/spec/src/ui/view.test.ts` 那几条**纯 schema 测试样本**(它们只跑 + `FormFieldSchema.parse`,不经 lint,属于本单援引的证据而非待修点)外,全仓 examples / apps / + packages 的 view/page 可见性谓词均能通过规范前端解析,无需修改任何示例内容。 + +- 92e13a0: refactor(lint)!: retire `visibility-alias-deprecated` — the rule could not fire on any real CLI input (#6318, ADR-0049) + + `@objectstack/lint` shipped a fourth conditional-visibility rule whose only job + was to report the deprecated predicate **key** (`visibleOn` on a view form + section/field, `visibility` on a page component) and steer the author to + `visibleWhen`. It never reported on anything a command actually loads. + + **Why it could not fire.** The rule is registered `input: 'normalized'`, so what + `os validate` / `os build` / `os lint` hand it is the output of + `normalizeStackInput`. The two ADR-0087 D2 conversions that fold the alias — + `view-visibleOn-to-visibleWhen` and `page-component-visibility-to-visibleWhen` — + run **inside** `normalizeStackInput`, one layer above. The key is therefore + already renamed by the time the rule sees the stack. Re-measured per site: + + | alias site | rule fed the raw authored object | rule fed the `normalized` tier | + | ----------------------------------- | -------------------------------- | ------------------------------ | + | `views[].form.sections[]` | 1 finding | **0** | + | `views[].formViews.edit.sections[]` | 1 finding | **0** | + | `pages[].regions[].components[]` | 1 finding | **0** | + + The one shape it did still fire on is a view **container** carrying top-level + `sections` — the shape its own unit tests used, and the shape strict + `ViewSchema` refuses outright (`Unrecognized key(s) on this view container: +\`sections\``). A green unit test over a fixture production can never send. + + **No working app loses a signal.** Authors were never hearing this rule, and + they do hear the conversion: the same D2 entry emits a `warnConversionNotice` + from `defineStack` that names the site, the conversion id and the retirement + window — wording the lint rule never had. + + ``` + defineStack: views[0].form.sections[0].visibleWhen: 'visibleOn' -> 'visibleWhen' + (converted at load; conversion 'view-visibleOn-to-visibleWhen', retires in protocol 16). + Update the source to the canonical shape — the conversion stops running then. + ``` + + **Authored metadata is unaffected.** `visibleOn` / `visibility` remain accepted + exactly as before, still fold to `visibleWhen`, and still retire with protocol 16. Nothing an app author writes has to change. + + **Consumer migration — one removed export.** The rule id constant leaves the + published barrel: + + - `VISIBILITY_ALIAS_DEPRECATED` (`'visibility-alias-deprecated'`) is removed from + `@objectstack/lint`. Delete the import; no finding carries that `rule` value + any more, so a `suppressWarnings: ['visibility-alias-deprecated']` entry or a + filter comparing against it is now dead code and can go with it. + + The other three rules in the same module are **unchanged** — they judge the + predicate's _value_, which crosses the fold into `visibleWhen` intact, and each + still reports normally on the `normalized` tier: + `visibility-root-mislayered`, `visibility-bare-identifier`, + `visibility-predicate-syntax`. `checkElement` also keeps reading the predicate + through the deprecated keys (canonical-first, so an alias can never override + `visibleWhen`), which is what lets those three still judge an alias-spelled + predicate handed to the exported function directly. + + Retired rather than re-anchored: making the rule read a genuine pre-normalize + value would have changed `runAuthoringRules`' external input contract, which is + a `packages/lint` public-API decision for the maintainer rather than a rule + file's to take. + + + +### Patch Changes + +- e9b5265: fix(formula,lint): `current_user` becomes a declared root, and its field-level rejection becomes a real rule (#6290) + + `@objectstack/formula` told two stories about one root. `introspectScope` handed + `current_user` to authors as a legal namespace and `checkRoleCatalog`'s four + position-membership regexes all lead with it — both correct, because ADR-0068 D1 + makes `current_user` THE canonical spelling and `buildScope` really does mount + the same `EvalUser` under it. Only `cel-engine.ts`'s `SCOPE_ROOTS` disagreed, so + the strict environment read the blessed spelling as a BARE FIELD REFERENCE while + its two aliases (`user`, `ctx`) passed unremarked. + + Three things change. + + **1. `SCOPE_ROOTS` declares `current_user`.** That list is a "never faults" + baseline, not a per-surface contract, and it now advertises exactly what the + package advertises elsewhere. A new pin asserts the property directly: every + root `introspectScope` reports must resolve in the strict env. + + **2. The wrong prescription is gone.** Because the rejection used to fall out of + the baseline's omission, the author got the GENERIC bare-field diagnostic — + "Write `record.current_user`". That shape binds on no layer of the platform, so + an author who followed the message ended up with something strictly worse than + what they started with, still silent. The field-level verdict now comes from a + rule of its own in `@objectstack/lint`, which names the real failure (unbound ⇒ + fault ⇒ visibility falls back to `true` ⇒ the field a `current_user` test was + meant to hide stays visible for everyone, #6146) and prescribes surfaces that + exist: move the predicate to the option's own `visibleWhen`, declare field-level + security on a permission set (`fields: { '.': { readable: false } }`), + or rewrite it against `record`. It covers `visibleWhen`, `readonlyWhen` and + `requiredWhen`, which share the one evaluator. + + **3. Per-option `visibleWhen` is validated at all.** `validate-expressions.ts` + walked field-level conditional rules and stopped there, so `SelectOption.visibleWhen` + — an authorable CEL slot the client filters on AND the server enforces — reached + compile, validate and run time checked by nobody. A bare field reference, a + reference to a field that does not exist, a syntax error or a template-dialect + predicate in an option all shipped in silence, and the option simply never + offered itself. Options are now walked, located by option value, on the same + `record` scope as their host field. + + The two surfaces deliberately give opposite verdicts on `current_user`, because + their evaluators differ: field-level rules go through `evalFieldPredicate` + (`record` + `previous` + `parent`, never a user), options through + `resolveCascadingOptions` against the host's predicate scope, which does bind it + (ADR-0068 / objectui#2284). The showcase's role-gated option + (`'admin' in current_user.positions`) had never met this rule before and is now + pinned as the legal usage it is. + + Sweep: `objectstack validate` is clean on all three example apps + (`app-showcase`, `app-crm`, `app-todo`) with the option walk active — zero new + findings, including the showcase object that carries both a record-scoped + cascade and the role-gated option. + +- 01fd9e1: fix(lint): `validateFormLayout` walks the view CONTAINER ladder, so both its rules stop reporting clean on every real app (#6251) + + `form-field-unknown` and `absolute-colspan-discouraged` read a `sections` array + off the **`views[]` entry itself** and skipped everything else. But a `views[]` + entry is a view CONTAINER, not a view: `ViewSchema` declares exactly `name` / + `label` / `object` / `list` / `form` / `listViews` / `formViews`, and form + sections live one level down, under `form` and each `formViews.`. So the + one shape the traversal read is the one shape strict `ViewSchema` **refuses** — + measured, `unrecognized_keys` naming `sections` — and the shapes every app + actually ships were never inspected at all. + + Measured on the three shipped example apps, before and after: `app-showcase`, + `app-crm` and `app-todo` carry **0** form sites at the entry root and **14** + under `form` / `formViews.`. The old traversal therefore had nothing to + read on any of them, and reported clean for that reason — the "ghost check" + shape (#4984 / #5009): a rule that is green because it never read anything is + worse than no rule, because it occupies the slot that would otherwise look + empty. + + One broken form, three placements, before → after: + + | placement | before | after | + | ---------------------------------------------------- | ------- | ------- | + | `views[0].sections` (entry IS a bare form view) | reports | reports | + | `views[0].form.sections` (container default form) | silent | reports | + | `views[0].formViews.edit.sections` (named form view) | silent | reports | + + What changed, precisely: + + - The traversal is the one `validate-visibility-predicates.ts` landed in #6248 + for the identical hole on the sibling rule — copied, not re-derived, so two + rules on one surface cannot drift apart about which forms exist. `list` / + `listViews.` are `ObjectListViewSchema` and carry no `sections`, so they + are deliberately not walked; `objects[].views` stays out because + `object.zod.ts` tombstones that key by name. + - The legacy `groups` bucket (`FormSectionSchema[]`, the documented alias of + `sections`) is read too. Measured: it is **not** folded into `sections` at + parse, so a `groups`-authored form was a second silent shape. + - A finding names its sub-container — `view "contact_views" · formViews.create` + — because an artifact-emitted container carries neither `name` nor `object`, + and without it two forms under one view were indistinguishable. + - A sub-container inherits the container's object binding when it declares no + `data.object` of its own, resolved through the same `objectName` → `object` → + `data.object` ladder the other view-walking rules in this package use. + - A map-shaped `views` reports at the key it sits at (`views.contact_views.…`) + rather than a synthetic index, so a finding stays usable as an edit target. + + Both rules remain advisory `warning`s and their messages, hints and severities + are unchanged. No new finding appeared on any example app, so nothing that was + green goes red on existing metadata — what changes is that a form defect in the + places apps actually put forms is now reported instead of silently passed. + +- a5ca08d: fix(lint): `object/missing-name-field` 认 `nameField`、不再把已退役的 `titleFormat` 当作 name 面(#6108) + + `object/missing-name-field` 的谓词从来不读 `obj.nameField`,却仍然采信 `obj.titleFormat`: + + ``` + hasNameField = !!obj.primaryField || !!obj.titleFormat || fields.some(name-like) + ``` + + 净效果是同一个包里两条规则互相矛盾。`validate-record-title.ts` 把每一处 `titleFormat` + 声明都报成 `title-format-retired`,并按 **ADR-0079** 指示作者迁移到 `nameField` + (`titleFormat` 是 render-only 模板,服务端既不能返回也不能查询);而共享的 + `objectTitleCompleteness`(`@objectstack/spec/data`)判定标题面时也从不读它。于是: + **照平台自己的迁移建议把 `titleFormat` 换成 `nameField` 的对象,反而多得一条 + "records will display as raw IDs" suggestion;守着已退役的键不动的对象反而干净。** + + 下游实测(hotcrm main,`@objectstack/* 17.0.0-rc.3`):6 处命中里 4 处是误报, + 四个对象——`crm_campaign_member` / `crm_event_attendee` / `crm_contract` / + `crm_forecast`——都显式声明了 `nameField`;只有两个 line-item 对象是真命中。 + + 本次修正: + + - 谓词补读 `nameField`(ADR-0079 的规范主标题指针),显式声明它的对象不再被告警; + - 摘掉 `titleFormat` 这一支。**只声明 `titleFormat`、没有 `nameField` 的对象因此会 + 新得一条本规则的 suggestion** —— 这是刻意的翻转,不是回归:这类对象正是 ADR-0079 + 要求迁移的那一批,`validate-record-title` 今天已经对它同时报 + `title-format-retired` 与 `title-unresolvable`。两条规则从此对同一个对象给出一致判断; + - `primaryField` 与 name-like 字段两支行为不变; + - 提示文案改为只点名作者真正能声明的面(`nameField` 与 name-like 字段),并新增 `fix` 提示 + 说明 `titleFormat` 不算标题面 —— 读到旧文案的作者很容易顺手再写一个 `titleFormat`, + 又掉回同一个矛盾里。旧文案里的 `primaryField` 同时不再出现:该键在 `packages/spec` 中 + 没有任何声明,`ObjectSchema.create()` 会以 `unrecognized_keys` 拒收它(实测,已立 #6326), + 提示不该向作者广告一个会被 schema 硬拒的键。谓词里的这一支保持不动。 + +- 6ce10bd: fix(lint): 摘掉 `primaryField` 这个幽灵键——两条规则不再把它当作标题面(#6326) + + `primaryField` 在 `packages/spec` 里**没有任何声明**。实测(`17.0.0-rc.5` dist): + + ``` + ObjectSchema.safeParse({ name: 'probe_obj', label: 'Probe', primaryField: 'code', + fields: { code: { type: 'text', label: 'Code' } } }) + // => success: false + // => issues: [{ code: 'unrecognized_keys', keys: ['primaryField'], path: [] }] + + ObjectSchema.create(/* 同上 */) + // => throws: ObjectSchema.create('probe_obj'): unknown key(s) — primaryField. + ``` + + 同一形状换成 `nameField: 'code'` 则 `safeParse` 通过。也就是说,这个键**从来不是可声明面**, + 而三处消费者没跟上——两面同源,却各自有一个可达面: + + - **文档面(作者会照做,当下活着的那一半)**:`skills/objectstack-data/SKILL.md` 是 AI 编写 + 元数据时读的技能文档,它把 `primaryField` 明说成 `object/missing-name-field` 的合法逃逸口。 + 照它写出来的对象在 `ObjectSchema.create()` 上被 ADR-0032「不静默丢弃未知键」的闸硬拒—— + **这是在教 AI 写出必然失败的元数据。** + - **规则面(判定永不成立)**:`data-model-rules.ts` 的 `!!obj.primaryField` 一支,以及 + `validate-semantic-roles.ts` 标题解析链里的那一项,对任何 schema 收得下的对象恒为 false, + 属于 #4984 家族的死支——看起来在保护什么,实际什么都判不到。 + + 本次按维护者裁定 **remove,不 declare**(`nameField` 已是 ADR-0079 的规范主标题指针, + 再立一个平行指针没有拉力,且与 Prime Directive #7「One Zod source per metadata type」相悖): + + - `data-model-rules.ts`:`object/missing-name-field` 的谓词收敛为 + `!!obj.nameField || fields.some(name-like)`; + - `validate-semantic-roles.ts`:规则 (d) 的标题解析链收敛为 + `[nameField, displayNameField]`(`displayNameField` 实测可声明,保留); + - `skills/objectstack-data/SKILL.md`:该规则的表述改为只点名作者真正能声明的面—— + `nameField` 与 name-like 字段(并列出这七个名字)。 + + **零 `packages/spec` 改动,不需要迁移:`primaryField` 从来不是可声明键,写了它的对象在 + schema 上本来就发布不了,所以没有任何能工作的 app 会因此回归。** 行为上唯一的变化是: + 一个只靠 `primaryField` 充当标题面的对象,现在会新得一条 `object/missing-name-field` + 的 suggestion(severity 为 suggestion,不失败命令)——而这类对象本就通不过 `ObjectSchema`。 + 真正的修法是改声明 `nameField`。 + +- 7618ee8: `translation-target-unknown` 按运行时视图身份判定容器默认 `list` 的 `_views` 键(#5164 第 2 棒 / lint 段) + + `validate-translation-references` 的 `collectViewRecord()` 过去读 `view.list.name` + 来决定容器默认列表贡献哪个 `_views` 名 —— 作者没写 `name` 时它什么也不注册,而组装器 + (`expandViewContainer`,`packages/spec/src/ui/view.zod.ts`)给同一个视图的身份是 + `.default`。第 1 棒(#6124)已把 i18n 提取器改为向组装器查询同一个键,于是 + **同一次 `os lint` 运行里**出现了一对自相矛盾的结论: + + - 要求方 `i18n/missing-view`:`objects.._views.default.label` 缺翻译; + - 否定方 `translation-target-unknown`:`_views.default` 是孤儿键,「no view of object + `` declares it」。 + + 作者补了译文被判孤儿,删了译文被判缺翻译,两条都躲不掉。本仓库自带示例上实测有 8 处 + (`examples/app-showcase` 6 处、`examples/app-todo` 2 处)。 + + 本规则现在同样**向组装器查询**这个键,而不是第三次自行推导,因此继承了组装器仅有的三条 + 规则: + + - 无 `name` 的默认列表键为 `default`;带 `name` 的沿用作者的 `name`; + - 结构上与某个 `listViews` 条目完全相同的默认列表被组装器按签名**折叠**进该条目,只有 + 存活的那个键合法 —— 被折叠掉的 `list.name` 不再是合法键(`examples/app-crm` 形状); + - 因命名冲突被改名的键(`default` → `default_2`)按**改名后**判定,因为改名后的名字才是 + 注册表键。 + + ## 判定变化(全是 warning,不改 `os lint` 退出码) + + | 形状 | 变化前 | 变化后 | + | ----------------------------------------------------------------------- | ------------ | --------------------------------------- | + | 默认 `list` 无 `name`,包里写 `_views.default.*` | 报孤儿(误报) | 通过 | + | 默认 `list` 无 `name`,包里写 `_views.list.*` | 报孤儿 | 报孤儿(不变;提示语现在会列出 `default`) | + | 默认 `list` 与某个 `listViews.` 同签名,包里写 `_views..*` | 通过(漏报) | 报孤儿 —— 该键运行时解析不到 | + | 默认 `list` 因冲突被改名 `default_2`,包里写 `_views.default_2.*` | 报孤儿(误报) | 通过 | + + 本仓库 12 个受棘轮覆盖的配置上实测:**新增 0 条**,消除 8 条误报; + `check:i18n-coverage` 基线不变(该棘轮只数 `i18n/` 前缀,本规则不在其内)。 + + 裁决依据:维护者 2026-08-06(#5164)—— `_views` 翻译键的 canonical 拼写 = 运行时身份的 + 裸键。第 3 棒 objectui `viewSuffixes` 去第二候选(objectui#3502)不在本次变更内。 + +- 9bc846b: fix(objectql,lint): 服务端为 `requiredWhen` 绑定 parent 作用域,并把构建期硬闸扩到同一格 + + `readonlyWhen` 的 parent 作用域洞在 #4889 已经补上;同一个字段上、由同一个求值器处理的 + `requiredWhen` 隔一个槽位还漏着。detail 对象上声明的 + `` requiredWhen: P`parent.status == 'sent'` `` ——「表头一旦 Sent,每一行都必须填写说明」—— + 只在内联表格里被求值,服务端从来只绑 `record` / `previous`,谓词直接 fault 走 fail-open + 分支,写入带着空字段落库,API 还回 200。 + + 注意它与 #4889 是**镜像**而不是同一种故障:`readonlyWhen` fail-open 是**写进了本该冻结的字段**, + `requiredWhen` fail-open 是**收下了本该被拒的记录**。两者都是同一处声明点上的 `declared ≠ enforced` + (PD #10)。 + + 本次按维护者 2026-08-06 的裁决落 A + C 两条,**刻意不对称于 #4889**: + + - **A —— 绑作用域,求值语义不动。** 引擎用 #4889 已经建好的 + `resolveMasterDetailParent(s)` 解析主表头行并传入求值器,insert / 单 id update / + bulk update 三个调用点都覆盖。**不可求值仍然 fail-open**(记日志、跳过、放行): + 表头此刻读不到就 422 掉一次本来合法的写入,比 `readonlyWhen` 那边「拒掉一个字段」响得多。 + 这是 issue 的 B 案,明确不做,留给 ADR-0058 D5 下一次复审。 + - **C —— 改在构建期拦。** `@objectstack/lint` 的 parent 作用域闸原本只盖 `readonlyWhen`, + 现在同样判 `requiredWhen`:对象没有恰好一个 `master_detail` 关系时,`parent` 不是元数据 + 陈述过的事实,声明直接判 error。两格共用同一个闸,但**报错文案不同** —— 两边运行时的失败 + 方向相反(`readonlyWhen` fail-closed ⇒ 字段永远写不进;`requiredWhen` fail-open ⇒ 要求 + 永远不生效),文案指错了就等于给了相反的修法。运行时敢保持 fail-open,正是因为这道闸 + 拦住了那条会无声烂掉的声明。 + + 同一次改动里补了 ADR-0113 非回归判定在 parent 作用域下的正确输入:「存量行本来就违规吗」问的是 + **写入前**那一行的状态,而它挂的是**旧**表头。改挂(repoint)到另一个主表时,若把落地表头也 + 喂给这个前置判定,就会把「移到 Sent 表头之下」读成既有违规而放行 —— 正是本 issue 要堵的那个 + 收下动作,只是换了个入口。因此求值器新增 `previousParent`,仅在载荷确实改挂时由引擎解析, + 其余情况沿用同一行、不多付一次读。 + + 对象级 `script` / `cross_field` 规则共用这个求值调用点,自 #4649 起对不可求值谓词是 + **fail-closed**,本次**没有**给它们绑新根 —— 绑了会把它们今天拒掉的写入翻成接受。这条由 pin + 测试钉住(#4972 当初把本改动挡在范围外,就是为了这个爆炸半径)。 + + 仓内暂无 app 声明 parent 作用域的 `requiredWhen`(showcase 的 invoice line 用的是行作用域的 + `record.quantity >= 100`),所以这是补潜伏缺口,不改变任何现有 app 的写入行为。 + +- 3510e4a: refactor(spec,drivers,lint): one implementation of the filter identity reduction (#5659) + + `{ $and: [] }` matches every row, `{ $or: [] }` matches none, `{}` is a TRUE + disjunct that absorbs its `$or`, `{ $not: {} }` is FALSE. That is a ruling + (#5322/#5134) pinned for every backend by the four identity cases in + `FILTER_LOGIC_CASES` — and it was implemented four times over: `reduceFilterNode` + in `driver-sql`, the same function again in `driver-mongodb`, the + `every`/`some`/truthiness algebra of `driver-memory`'s matcher, and nearly a + fifth hand-written copy inside `@objectstack/lint`, which declined to write one + and filed this issue instead. + + **New in `@objectstack/spec` (`@objectstack/spec/data`): `reduceFilterVerdict`**, + beside the case table that proves it. It answers `'true' | 'false' | 'clause'` + for a filter node and never throws on its own; each backend's own refusals — the + undeclared `$`-combinator and the `undefined` comparand in `driver-sql`, the + query-level keys and the `$null` comparand in `driver-mongodb` — are passed in as + `FilterVerdictHooks` and are invoked from exactly the positions they were invoked + from before. `reduceFilterKeyVerdict` answers the same question for one key, which + is what both SQL and MongoDB emitters consult while walking a node. + + **No behaviour changes in the three drivers.** The move is mechanical: the shared + algebra replaces each private copy, the refusals stay where they were, and the + `FILTER_LOGIC_CASES` conformance suites are green on both sides of the change — + including the SQL-inheriting `driver-sqlite-wasm` and `driver-turso`. + + **`@objectstack/lint` gains two warnings it was structurally blind to.** The + `multi: true` unbounded-bulk-write rule (#5482) asked "does this filter have zero + keys", so a `delete_record` bounded by `filter: { $and: [] }` or + `filter: { $or: [{}] }` — a whole-object write by the ruling every driver executes + — passed silently. It now asks the reduction, and it warns about both while + staying quiet on `{ $or: [] }` and `{ $not: {} }`, which match nothing. The + message names the shape it saw (`a filter that REDUCES TO TRUE ({"$and":[]})`) + rather than calling a non-empty filter "empty". + + If you have a flow declaring a bulk write bounded by one of those two shapes, the + lint will now tell you so — the write was already unbounded at run time; only the + feedback is new. + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [07c68b0] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [e9b5265] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [6965160] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/formula@17.0.0-rc.6 + - @objectstack/sdui-parser@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/lint/package.json b/packages/lint/package.json index 108976921b..125a1fa54d 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/lint", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 8090d9f230..cbe21b3aff 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,5 +1,104 @@ # @objectstack/plugin-mcp-server +## 17.0.0-rc.6 + +### Patch Changes + +- 5c2716b: mcp: a metadata outage stops being reported to MCP clients as `Agent "X" not found` + + The `agent_prompt` prompt resolved its body through `metadataService.get('agent', name)` + and answered the resulting `undefined` with `Error: Agent "X" not found`. That `undefined` + carries two opposite facts (#5840, ADR-0110 D3): the name was never declared, or every + loader behind the metadata service was down. So during a metadata outage an MCP client was + told, positively, what the author had declared — from a read that never happened. The same + shape sat one bridge over: the `objectstack://objects/{objectName}` resource answered + `getObject()`'s `undefined` with `Object "X" not found`. + + **Both surfaces now separate the two.** A degraded read answers `SERVICE_UNAVAILABLE` — + the same catalogued code and the same "whether it exists is unknown, retry once it is + reachable" sentence the `sys_metadata` half of this family already emits (#5532 / #5843) — + and a genuine miss keeps its not-found answer, byte for byte on the prompt surface. + MCP's `prompts/get` and `resources/read` results carry no error envelope, so the + classification travels in the payload each surface already had: the prompt's text, and the + resource's JSON body, which now names `code` and `status` on **both** answers + (`SERVICE_UNAVAILABLE`/503 vs `RESOURCE_NOT_FOUND`/404) so a client can tell them apart + without parsing prose. + + **This is a diagnosis fix, not an access change.** Both surfaces were already fail-closed: + no instructions and no schema were served during an outage before this, and none are now. + The defect was the description. + + Hosts whose `metadata` slot predates the optional `getDiagnosed` member report nothing + degraded — exactly what they could express before — so their behaviour is unchanged. The + object resource additionally keeps `getObject()` as its resolver and consults the + diagnosed read only as a verdict probe on the miss path, because `getObject` is its own + contract member with no documented equivalence to `get('object', name)` (and + `MetadataFacade.getObject` is not that). + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [07c68b0] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [e9b5265] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [6965160] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/formula@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 049d307129..beea6aa363 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/mcp", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 d3f800f993..8df700bd11 100644 --- a/packages/metadata-core/CHANGELOG.md +++ b/packages/metadata-core/CHANGELOG.md @@ -1,5 +1,139 @@ # @objectstack/metadata-core +## 17.0.0-rc.6 + +### Patch Changes + +- 1a53a02: fix(meta): `/meta` object reads stop reporting `readonly: false` on fields the write path refuses (#4513) + + `#4447` made the audit-provenance family (`created_at`, `created_by`, + `updated_at`, `updated_by`) engine-owned on the **write** path: the registry's + `applySystemFields` forces `{ readonly: true, system: true }` over a _declared_ + audit field, and `ObjectQL.update` strips a non-system caller's write to it. + + The **read** path never learned it. A `/meta` object read resolves through + `sys_metadata` overlay → MetadataService → SchemaRegistry, and only the last of + those three has been through `applySystemFields` — so an object whose built + artifact ships a materialized `created_at` carrying FieldSchema defaults + (`readonly: false`) reported that value to every client while writes to that + same field were being refused. Measured before the fix, all of the read exits + agreed with each other and disagreed with the engine: + + ``` + single read: {"type":"datetime","label":"Created At","readonly":false} + list read: {"type":"datetime","label":"Created At","readonly":false} + cached read: {"type":"datetime","label":"Created At","readonly":false} + layered read: {"type":"datetime","label":"Created At","readonly":false} + ``` + + One field, two answers — and the machine-readable one, the only face a client + or an AI author writing code off `/meta` can see, was the wrong one. + + **What changes.** Every `/meta` object read exit now reports the audit family + the way the engine enforces it. That covers the single-item read (both the + singular and plural type spelling), the list read, the cached/ETag branch, the + `?preview=draft` and `?state=draft` reads, and the layered read's `effective` + layer. `GET` bodies for objects that declare an audit field will show + `readonly: true, system: true` where they previously showed `readonly: false` + or omitted the keys; nothing else about the document changes, and the ETag for + such an object changes once. + + **What deliberately does not change.** + + - The layered read's `code` and `overlay` layers stay raw — showing the + package's declaration beside the governed `effective` value is the + diagnostic's whole point. + - `sys_metadata` still stores exactly what the author saved; the correction is + applied on the way out, so no phantom customization appears in the diff. + - An object that opts out of the audit family (`systemFields: false`, + `systemFields.audit: false`, `managedBy: 'better-auth'`) is untouched — the + engine enforces nothing there, so a read that claimed otherwise would be the + same lie pointing the other way. + - Only `readonly` and `system` are forced. Every other key an author writes — + `label`, `description`, `hidden`, `group`, and `type` for an external object + mapping a differently-typed remote column — stays theirs. + + The governance table moved from `packages/objectql/src/registry.ts` to + `@objectstack/metadata-core` (`AUDIT_FIELD_GOVERNANCE`, plus the + `applyAuditFieldGovernance` normalizer the read path applies), by the same + criterion and for the same cycle as the `#5619` engine-dispatch predicates: + `@objectstack/objectql` depends on `@objectstack/metadata-protocol`, so the + read path cannot import the table from the registry that enforces it, and a + second copy would agree only until someone edited one side. `objectql` + re-exports the symbol from its original path, so its public API is unchanged. + +- 3d4c545: fix(metadata): `sys_view_definition` 的「活跃行唯一」真正生效——归档视图不再占用 (name, organization_id, owner) 名额 + + `sys_view_definition` 的 `idx_sys_view_def_active` 索引注释一直承诺「among active rows」,但这个语义从未在任何一层交付:声明面的 `partial: "state = 'active'"` 没有任何 driver 消费者(`syncDeclaredIndexes` 走 knex 的 `table.unique()`,无法表达 `WHERE`),该键已随 #5248 / #4943 退役;而与 `sys_metadata` 不同,这张表背后**没有**任何等价的运行时迁移。结果是建出来的一直是无谓词的全量 UNIQUE 索引——用户归档(或软删、重置)一个视图后,**无法再新建同名视图**,被一条自己刚扔掉的记录挡住。 + + 现在补上运行时迁移 `ensureViewDefinitionActiveIndex`(照 `metadata-protocol` 既有的 `ensureOverlayIndex` 范式),在 `kernel:ready` 用 raw SQL 发 `CREATE UNIQUE INDEX idx_sys_view_def_active … WHERE state = 'active'`: + + - **名额可回收**——归档视图不再占用名额,同名视图可以重建; + - **唯一性不放宽**——两条 `state='active'` 的同名同域行仍然被拒; + - **复用声明的索引名**——`syncDeclaredIndexes` 按名跳过,后续每次启动都不会把全量 UNIQUE 索引重新加回来; + - **降级只会退回今天的行为,不会更低**——迁移先用一个临时探针索引验证当前方言与数据确实能建出部分索引,成功后才替换既有索引。因此 MySQL / MariaDB(无部分索引)上原有的全量 UNIQUE 索引原样保留(归档行在该方言上仍占名额,以 `info` 记录),不会出现「旧索引已删、新索引没建成」的无约束窗口。 + + `metadata-core` 侧只更新了 `sys-view-definition.object.ts` 的注释:该声明现在被明确记为**降级形态**(供无部分索引的方言与不跑该迁移的宿主使用),不应删除。 + + 已知未涵盖:`owner` 为 NULL 的共享视图与 `organization_id` 为 NULL 的环境级视图,因 SQL UNIQUE 的 NULL-distinct 语义本来就不受该索引约束。这是早于本次修复的既有缺口,本迁移只改变**行范围**(`WHERE state = 'active'`)而不动键的拼写——这也正是它严格弱于被替换的索引、因而不可能在存量数据上建失败的原因。该缺口已另单记录。 + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/metadata-core/package.json b/packages/metadata-core/package.json index 3dbcd7f78c..64b21a07d2 100644 --- a/packages/metadata-core/package.json +++ b/packages/metadata-core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-core", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 127b70efcc..3693e75d38 100644 --- a/packages/metadata-fs/CHANGELOG.md +++ b/packages/metadata-fs/CHANGELOG.md @@ -1,5 +1,13 @@ # @objectstack/metadata-fs +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [1a53a02] +- Updated dependencies [3d4c545] + - @objectstack/metadata-core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/metadata-fs/package.json b/packages/metadata-fs/package.json index e34bcf0b49..c21b81fd86 100644 --- a/packages/metadata-fs/package.json +++ b/packages/metadata-fs/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-fs", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 e113cf8aa1..77f7c070ea 100644 --- a/packages/metadata-protocol/CHANGELOG.md +++ b/packages/metadata-protocol/CHANGELOG.md @@ -1,5 +1,463 @@ # @objectstack/metadata-protocol +## 17.0.0-rc.6 + +### Minor Changes + +- 1818998: feat(spec,objectql,metadata-protocol): validate-only data operation — ask for the write's verdict instead of predicting it (#6037, #4633 ruling D) + + `import`'s dry run predicted the write path's verdict with a hand-copied mirror + of the engine's rules (`rest/src/import-coerce.ts`). A copy cannot structurally + keep up with the family it mirrors — ADR-0104 value shapes, `format` checks, + object-level `validations`, the state machine — so ruling D replaces prediction + with the verdict itself. + + **New:** `DataProtocol.validateData(request)` returns the write path's verdict + for candidate rows and persists nothing. + + ```ts + const verdict = await protocol.validateData({ + object: "lead", + mode: "insert", // or 'update', which judges only supplied keys + data: [{ first_name: "John", email: "not-an-email" }], + }); + // → { valid: false, + // results: [{ valid: false, errors: [{ field: 'email', code: 'invalid_email', … }], warnings: [] }], + // posture: { valueShapeStrict: true, mediaValueShapeStrict: false } } + ``` + + **Declaration and execution land together, deliberately.** `engine.validate()` + (objectql) calls the same `validateRecord` / `evaluateValidationRules` that + `insert()` calls, and `metadata-protocol` implements `validateData` on top of + it. Agreement between preview and write is therefore guaranteed by + construction, and a test asserts it directly by running both against one engine + in both postures. This is the ruling's own clause, not a style choice: + `BatchOptions.validateOnly` was retired in #4052 as a flag that promised a dry + run while the batch surfaces persisted regardless, so a caller previewing a + mutation had it EXECUTED. The new operation avoids that spelling too — the + tombstone still stands and still rejects `validateOnly`. + + **The verdict is the target deployment's, not an absolute.** The response + carries the ADR-0104 `posture` it was reached under. On a self-certified + deployment a bad value shape is an error; on a warn-first one the same row is + valid and the finding appears in `warnings` with the same `code` — one finding + that changed buckets, not two vocabularies. An unconditionally-strict preview + was considered and rejected (#4633 option B): it would fail rows on every + un-migrated deployment that the write would have accepted, which teaches + authors to distrust the one gate in front of a bulk import. + + Two boundaries worth knowing, both deliberate and both documented at the + implementation: + + - **No hooks run.** `beforeInsert` fires before validation on the real path, so + a hook deriving a _business_ field could change a verdict this does not + simulate. Firing arbitrary user hooks in a preview — mail, outbound calls, + writes to other objects — is the #4052 defect in a new spelling, so the gap is + documented rather than closed. Audit/ownership stamps are `system`/`readonly` + and validation skips them regardless. + - **Warn-first admissions are not recorded as certification evidence.** The + `#4769` sink exists so a boot cannot certify a contract it has just written + against; a preview writes nothing, so recording there would let a _preview_ + block a later migration. + + Additive: `validateData` is optional on `DataProtocol`, and nothing existing + changes shape. `valueShapeStrictEffective` / `mediaStrictEffective` are now + exported from objectql's record validator so the response reports the posture + that actually decided the verdict rather than the raw deployment flag. + + Unblocks #4633's consumption half (rest/import adopting the operation and + retiring the `import-coerce.ts` mirror). + +### Patch Changes + +- ae31a19: fix(spec,metadata-protocol): `capability` 补齐三处注册 —— 授权面不再接受任意 JSON (#5961) + + `capability` 是「enforced but undeclared」——#5271 给 `api` 关掉的那个 + `declared ≠ enforced` 的镜像。平台早就把它当成一个 metadata kind 在用: + `PLURAL_TO_SINGULAR` 从 #5870 起就有 `capabilities` → `capability`, + `AppPlugin` 用这个名字注册 stack 声明的 capability, + `bootstrapDeclaredCapabilities` 再读回来 seed `sys_capability`。但三处注册表 + 里都没有它:`MetadataTypeSchema`(kind 枚举)、`BUILTIN_METADATA_TYPE_SCHEMAS` + (schema 解析)、`DEFAULT_METADATA_TYPE_REGISTRY`(谁可以写、怎么加载)。 + + 后果有两条,第二条才是这个 issue 属于授权缺陷而非整洁度问题的原因: + + - `getMetadataTypeSchema('capability')` 返回 `undefined`,于是 `saveMetaItem` + 走了它自己文档化的「未注册类型 → 不校验直接存」分支, + `PUT /api/v1/meta/capability/:name` 接受**任意 JSON** 落进 `sys_metadata`。 + capability 是靠**名字字符串**被解析的——授予侧 `systemPermissions`、 + 要求侧 `requiredPermissions` 都是——所以一行任意 JSON 直接落在活的授权命名 + 空间里。 + - `isRuntimeCreateAllowed` 镜像 `getMetaTypes()` 的合成规则:没有静态注册表条目 + 的类型被当作可运行时创建。所以缺的那一行不只是「没关上门」,它**把门打开了**。 + `/meta/types` 同步发布了这个虚构:`allowRuntimeCreate: true` + 无 schema, + metadata-admin 引擎据此渲染成一个 raw-JSON 文本框。 + + ### 改了什么 + + - **`BUILTIN_METADATA_TYPE_SCHEMAS['capability'] = CapabilityDeclarationSchema`**。 + 既有的 422 `invalid_metadata` 路径就此覆盖 `capability`,`/meta/types` 发出真 + JSON Schema。 + - **`DEFAULT_METADATA_TYPE_REGISTRY` 新增 `capability` 条目, + `allowRuntimeCreate: false` + `allowOrgOverride: false`**。ADR-0066 D1:包 + DEFINE capability,权限集 GRANT,资源 REQUIRE。管理员在运行时凭空造一个 + capability 在这个三分里没有位置——代码里不会有任何地方 require 那个名字,这行 + 只是授权命名空间里一个无人引用的授予目标。这一对标志就是 #5086 的 CODE-ONLY + 声明,`saveMetaItem` 在**任何** kernel 上都以 403 `not_creatable` 拒绝,并从条 + 目自己的 `filePatterns[0]` 读回「该去哪儿声明」。`supportsOverlay: false`—— + capability 只是名字/标签/scope,没有 merge 语义,而允许租户 overlay 一个包发布 + 的声明等于允许把 `scope` 从 `org` 抬成 `platform`。`loadOrder: 12` 早于 + `permission`/`position`(15),使权限集的 `systemPermissions` 解析时 capability + 已经存在。 + - **`MetadataTypeSchema` 枚举补 `'capability'`**。 + - **`CapabilityDeclarationSchema` 声明 ADR-0010 保护信封并收紧为 `.strict()`**。 + 信封是必须的:loader 对每个已注册类型都调 `applyProtection`,不声明就会 422 掉 + loader 自己的输出(#4001 在 `permission`/`position` 上补过同一个洞)。收紧则与 + `api` 不同——`ApiEndpointSchema` 同时是**存储行**的解析器,所以它留在 + `STILL_STRIP`;而没有任何地方拿这个 schema 重新解析 `sys_capability` 行 + (`bootstrapDeclaredCapabilities` 通过 `capabilityRowFields` 按名读字段), + 所以收紧零成本,买到的是一个授权面本就该有的 declared = enforced 姿态。 + 改用 `strictObject` 书写,已知键从 shape 派生,不新增手抄键表。 + + **包声明通道完全没动。** `AppPlugin` 通过 `registerInMemory` 注册 stack 的 + `capabilities[]`,文件系统 loader 按 `filePatterns` glob——两条都不经过 + `saveMetaItem`,所以 `bootstrapDeclaredCapabilities` 依旧照常 seed。 + `OS_METADATA_WRITABLE=capability` 仍是 ADR-0005 那唯一一道运维逃生门,而在它后面 + 写入现在由 `CapabilityDeclarationSchema` 判定(422),不再原样落盘。 + + ⛔ `role` / `profile` / `policy` **不搭车**:它们没有 `PLURAL_TO_SINGULAR` 映射、 + 没有声明 schema、没有读回接缝,是另一个问题,另开单。这条以断言形式钉在 + `capability-metadata-kind.test.ts` 里,因为「capability 有了条目,邻居也该有」 + 正是下一个显而易见却错误的改动。 + +- 2a2a9fb: fix(spec,metadata-protocol,runtime): one place decides what an unset `NODE_ENV` advertises (#5936) + + A deployment whose operator never exported `NODE_ENV` must not describe itself as + `development` on `/discovery`: `environment` is a machine-readable field, a client + reads it to answer "am I talking to production?", and it may skip production warnings + or loosen a destructive action's confirmation on the answer. #5673 ruled that in and + fixed it — but only for one of the two producers, because that dispatch put + `packages/spec` out of scope. The other one, `MetadataProtocol.getDiscovery()` (served + by `@objectstack/rest`), went on answering `development` for exactly that input. + + The default now lives in the shared mapper, `resolveDiscoveryEnvironment`: an absent — + or blank — value resolves to `production`, and both producers pass the operator's value + through as they read it, neither carrying a default of its own. That is what makes it + one decision instead of two copies, and it means the next discovery producer inherits + the right answer without anyone remembering to copy a line. Patching only + metadata-protocol would have left a second copy of the default — precisely the drift the + shared table was created to prevent (#4828). + + "Unset" includes a blank value: `NODE_ENV=` exports an empty string, the runtime's + `getEnv` has always folded that into its default, and had the mapper treated blank as + "anything else" the two producers would have drifted again on that one input. + + **#4828's rule is untouched, and it points the other way on purpose.** A value that IS + set but is not a spelling this repo recognises (`qa`, `preview`) still degrades to + `development`, so nothing ever claims `production` on a guess. Absence is not a guess — + it is the host declining to say. + + Behaviour change to expect: a host that exports no `NODE_ENV` and serves `/discovery` + through `@objectstack/rest` now advertises `environment: "production"` where it + previously advertised `"development"`. A deployment that genuinely is development should + say so — `NODE_ENV=development` — which is what the runtime dispatcher has already + required since #5673. + + The mapping table above `NODE_ENV_TO_DISCOVERY_ENVIRONMENT` is corrected in the same + pass: its `unset / anything else -> development` row had been false for the runtime + caller since #5673 and is now two rows, one per rule. + +- dba7747: fix(metadata-protocol): `getUiView` 的响应体不再多发三个未声明键,与 `GetUiViewResponseSchema` 对齐 + + `GET /ui/view/:object/:type` 由 `getUiView` 产出、REST 层 `res.json(view)` 裸发(不套信封、不校验)。它的声明是 `GetUiViewResponseSchema`(= `ViewSchema`),但实发 body 里的 `list.object` / `form.object` / `form.label` 三个键,`ListViewSchema` / `FormViewSchema` 这两个 `strictObject` 从未声明,实测 `safeParse` 直接 `unrecognized_keys` 判红。因为 `GetUiViewResponseSchema` 在全仓没有任何运行时读者,这处分裂此前没有任何断言看得见。 + + **FROM → TO** + + ``` + FROM { list: { type, object, label, columns, sort, searchableFields } } + TO { object, list: { type, label, columns, sort, searchableFields } } + + FROM { form: { type, object, label, sections } } + TO { object, form: { type, sections } } + ``` + + - **迁移**:读 `object` 的消费者上移一层 —— `body.list.object` / `body.form.object` 改读 `body.object`。这是**相同的值换了层级**,不是删除:`ViewSchema` 一直在容器层声明 `object`(「Object this container binds to」),成员层那份本就是冗余副本。 + - `form.label`(原 `` `Edit ${…}` ``)**不上移、直接摘除**:它是渲染串而非元数据,任何 view schema 都没有声明过它;标题由 UI 自行拼(调用方本就知道自己请求的是哪个对象)。`list.label` **不受影响** —— `ListViewSchema` 正式声明了 `label`,保持原样。 + - 定级 **patch** 而非 minor/major:三键的消费面实测为零 —— `client-react` 的 `useView` 把 body 当 `any` 透传(`UseMetadataResult.data: any`),objectui 全仓 `meta.getView` 零命中(其 `getView(objectName, viewId)` 走的是 `client.meta.getItem('view', …)`,另一条通路)。无编译期破坏面,无类型改判。 + - `packages/spec` **零改动**:本次是把实现修正到既有声明,不是改声明迁就实现。 + + **未验面**:`cloud` 仓未在本次验证范围内(按 #5540 口径如实标注)。若该仓有直接读 `body.list.object` / `body.form.object` 的代码,需按上面的迁移上移一层;`form.label` 的读者需自行拼标题。 + + 常驻 pin:`packages/metadata-protocol/src/protocol.ui-view-response-conformance.test.ts` —— 用**生产端真实组装路径**(实调 `getUiView`)喂 `GetUiViewResponseSchema.safeParse`,而非手拼 fixture。反向验证已跑:恢复任一多发键 → pin 转红并点名该键。 + +- dbe92a7: fix(metadata-protocol): boot 重水合按行的真实 package 绑定登记对象归属(#4636 裁 B 收官) + + `loadMetaFromDb` 的 object 分支从 `engine.find` 返回的行上读 `record.packageId`,而 `sys_metadata` 的列是 snake_case 的 `package_id` —— 该表达式恒为 `undefined || 'sys_metadata'`,于是每次重启都把**绑定了包**的对象 overlay 登记在 `'sys_metadata'` 哨兵下。改为读 `package_id`,与写路径、`getMetaItems`、以及相邻的非 object 分支一致。 + + 用户可见的行为差异:归属键同时就是包过滤键(`getAllObjects(packageId)`),所以此前一个对象在**创建时**出现在自己所属包的侧边栏过滤里,**重启之后就消失**;更要紧的是重启后的第一次编辑——boot 登记 `'sys_metadata'`、保存登记 `app.`,`registerObject` 抛 `already owned by package …` 被 `applyObjectRegistryMutation` 吞成 `console.warn`,保存回 `success: true` 而内存 schema 停在重启时的版本,这一笔编辑被静默丢弃(cloud#970 的重启面)。两侧统一到真实 id 后,过滤与编辑都跨重启成立。 + + `@objectstack/objectql` 仅同步 `registry.ts` 中 `isTenantAuthored` 的契约注释:PR1 标注的「这半句描述的是契约,还不是代码」随本次落地摘除。 + +- 1a53a02: fix(meta): `/meta` object reads stop reporting `readonly: false` on fields the write path refuses (#4513) + + `#4447` made the audit-provenance family (`created_at`, `created_by`, + `updated_at`, `updated_by`) engine-owned on the **write** path: the registry's + `applySystemFields` forces `{ readonly: true, system: true }` over a _declared_ + audit field, and `ObjectQL.update` strips a non-system caller's write to it. + + The **read** path never learned it. A `/meta` object read resolves through + `sys_metadata` overlay → MetadataService → SchemaRegistry, and only the last of + those three has been through `applySystemFields` — so an object whose built + artifact ships a materialized `created_at` carrying FieldSchema defaults + (`readonly: false`) reported that value to every client while writes to that + same field were being refused. Measured before the fix, all of the read exits + agreed with each other and disagreed with the engine: + + ``` + single read: {"type":"datetime","label":"Created At","readonly":false} + list read: {"type":"datetime","label":"Created At","readonly":false} + cached read: {"type":"datetime","label":"Created At","readonly":false} + layered read: {"type":"datetime","label":"Created At","readonly":false} + ``` + + One field, two answers — and the machine-readable one, the only face a client + or an AI author writing code off `/meta` can see, was the wrong one. + + **What changes.** Every `/meta` object read exit now reports the audit family + the way the engine enforces it. That covers the single-item read (both the + singular and plural type spelling), the list read, the cached/ETag branch, the + `?preview=draft` and `?state=draft` reads, and the layered read's `effective` + layer. `GET` bodies for objects that declare an audit field will show + `readonly: true, system: true` where they previously showed `readonly: false` + or omitted the keys; nothing else about the document changes, and the ETag for + such an object changes once. + + **What deliberately does not change.** + + - The layered read's `code` and `overlay` layers stay raw — showing the + package's declaration beside the governed `effective` value is the + diagnostic's whole point. + - `sys_metadata` still stores exactly what the author saved; the correction is + applied on the way out, so no phantom customization appears in the diff. + - An object that opts out of the audit family (`systemFields: false`, + `systemFields.audit: false`, `managedBy: 'better-auth'`) is untouched — the + engine enforces nothing there, so a read that claimed otherwise would be the + same lie pointing the other way. + - Only `readonly` and `system` are forced. Every other key an author writes — + `label`, `description`, `hidden`, `group`, and `type` for an external object + mapping a differently-typed remote column — stays theirs. + + The governance table moved from `packages/objectql/src/registry.ts` to + `@objectstack/metadata-core` (`AUDIT_FIELD_GOVERNANCE`, plus the + `applyAuditFieldGovernance` normalizer the read path applies), by the same + criterion and for the same cycle as the `#5619` engine-dispatch predicates: + `@objectstack/objectql` depends on `@objectstack/metadata-protocol`, so the + read path cannot import the table from the registry that enforces it, and a + second copy would agree only until someone edited one side. `objectql` + re-exports the symbol from its original path, so its public API is unchanged. + +- 7e1b480: fix(metadata-protocol): 删除回执不再对 runtime-only 项谎称"已重置为 artifact 默认值" + + `deleteMetaItem` 的四句成功回执(repository 路径两句 + legacy raw-engine 路径两 + 句)原本无条件把每一次删除都叙述成"摘掉一层 overlay、回落到 artifact 默认值"。 + 但对一个 **runtime-only** 项 —— 管理员在 Studio 里新建的 `object` / `flow` / + `hook`,没有任何 code package 提供同名 artifact —— 底下根本没有默认值可回落:那 + 一行就是这个项的全部,删掉之后它在任何层都不复存在。回执却把管理员指向一个从未 + 存在过的基线。 + + 判据与 #5265 / PR #5926 在 save 侧用的是同一个:`isArtifactBacked` —— 也就是 + `intent: 'override-artifact' | 'runtime-only'` 的来源,本方法内早已算出。新增的 + 方法级绑定**替换**了 `intent` 原来的那次 inline 调用,所以分句后 registry 读取次 + 数不增反减。 + + | | FROM | TO | + | :---------------------------------- | :--------------------------------------------------------------------------- | :------------------------------------------------------- | + | 覆盖了 artifact,删除即回落 | `Customization overlay deleted — / reset to artifact default. [seq=N]` | 逐字不变 | + | runtime-only,删除即消失 | 同上 | `Deleted '' — it no longer exists. [seq=N]` | + | 覆盖了 artifact,本就没有 overlay 行 | `No customization overlay found for / — already at artifact default.` | 逐字不变 | + | runtime-only,本就不存在 | 同上 | `No '' found — nothing to delete.` | + + `success` / `reset` / `seq` 三个字段一字未动 —— `message` 没有任何消费方解析,仅 + 作展示。草稿两句(`Draft discarded — …` / `No pending draft for …`)本来就没有声 + 称过 overlay 或 reset,对两类项都为真,故逐字保留。legacy raw-engine 路径不写 + history、不发 watch 事件,两句因此本就不带 `[seq=…]`,该差异为既有设计,分句未 + 触碰。 + +- e39dd66: 冷启动跳过的 org 作用域元数据行不再无声消失 + + `loadMetaFromDb` 按 ADR-0005(2026-05 修订)只水合 `organization_id IS NULL` 的行, + per-org overlay 由 `getMetaItem`/`getMetaItems` 按需加载——对注册表里 + `allowOrgOverride: true` 的类型(`view`/`dashboard`/`report` 等)这是设计本身。但对 + **其余类型**,一条 org 作用域的行是平台根本没有 per-org 通道的行,而在此之前这个跳过 + 是**完全静默**的。 + + 实测标本是 `flow`:它是 `allowOrgOverride: false`(#6283 / PR #6478 按 ADR-0005:57 + 回滚),同时 `allowRuntimeCreate: true`,所以租户在 Studio 里新建一条 flow 仍会写出 + `sys_metadata.organization_id = ''`——运行时 `PUT /metadata/:type/:name` 把 + `resolveActiveOrganizationId` 透传给 `saveMetaItem`,而 `SysMetadataRepository.put` + 对任何类型都按 `organization_id: this.organizationId` 落库。该 flow 在本进程内一直正常 + 触发(发布时写穿进了进程级 registry),下一次重启后被这条过滤器丢掉,`kernel:ready` 的 + 绑定器读的是 `getMetaItems({ type: 'flow' })`(不带 org),于是它**再也不触发,且没有任何 + 日志说它消失了**——`kernel:bootstrapped` 的 unbound 审计也看不见它(它压根没注册)。 + + 现在冷启动会打一条聚合的 `warn`,按类型给出计数、抽样的 `name@org`,以及后果本身 + (「A 'flow' listed here will NOT bind its triggers in this process」)和处置建议。 + 查询默认为空:两个收窄谓词(`organization_id IS NOT NULL` + 类型清单,清单由 + `DEFAULT_METADATA_TYPE_REGISTRY` 派生而非手写)让健康部署读不到行、也不打印任何东西; + 驱动若无法下推其中一个谓词,退化为多读几行而不是打出误报(JS 侧会复核两个谓词)。 + + 加载行为**未改变**:这次只是把缺席变响亮。这类行到底该不该存在(写入侧拒绝 / 强制写成 + env-wide / 让绑定器按 org 读)是 #6190 上待裁决的契约问题。 + +- 2873eb9: fix(metadata-protocol): rolling back a package-bound overlay row no longer 409s (#6215) + + Every rollback of a metadata item authored inside a Studio package workspace + failed — and failed by blaming a concurrent edit that never happened: + + ``` + [metadata_conflict] object/myapp_invoice advanced during rollback. + Expected parent sha256:00ca6e72c... but current is null. + ``` + + Both user-facing paths were affected, because both are one call: + `rollbackMetaItem` (the per-item version-history revert) and `revertCommit` + (the package-commit revert) go through `SysMetadataRepository.restoreVersion`. + Only rows with **no** package binding — the legacy shape — rolled back at all, + while ADR-0070 pushes authoring toward always resolving a writable base + package, so the failing share was growing. + + **Cause.** `restoreVersion` read the current active row package-agnostically + and then re-put the historical body without saying which row it meant. `put` + scopes its optimistic-lock lookup by package, and an unstated `packageId` + resolves to the _unbound_ row (`package_id IS NULL`) rather than "any package" + — so for a row bound to `app.` the lock looked up a row that does not + exist, read its parent hash as `null`, compared that against the real hash the + first read had just returned, and threw `ConflictError`. The mismatch was + between two reads of the _same_ restore, not between two writers. + + **Fix.** `restoreVersion` now reads the raw active row once and takes BOTH + facts from it — the parent hash and the ADR-0048 `package_id` — then states + that binding on the write, the same way `promoteDraft` already did. The row the + lock is taken on is therefore, by construction, the row that gets written. + + This also closes the defect's second face: had the parent check ever passed, + `put` would have found no row in its `IS NULL` scope and **inserted a duplicate + unbound row** beside the bound one instead of updating it. `sys_metadata`'s + partial unique index keys on `COALESCE(package_id,'')`, so a real database + would have accepted that duplicate. + + Unchanged: package-less rows still roll back exactly as before, and a row that + _genuinely_ advanced between the rollback's read and its write is still refused + with `METADATA_CONFLICT` / 409. The refusal is narrowed to the case it always + claimed to report, not retired. + +- 3d4c545: fix(metadata): `sys_view_definition` 的「活跃行唯一」真正生效——归档视图不再占用 (name, organization_id, owner) 名额 + + `sys_view_definition` 的 `idx_sys_view_def_active` 索引注释一直承诺「among active rows」,但这个语义从未在任何一层交付:声明面的 `partial: "state = 'active'"` 没有任何 driver 消费者(`syncDeclaredIndexes` 走 knex 的 `table.unique()`,无法表达 `WHERE`),该键已随 #5248 / #4943 退役;而与 `sys_metadata` 不同,这张表背后**没有**任何等价的运行时迁移。结果是建出来的一直是无谓词的全量 UNIQUE 索引——用户归档(或软删、重置)一个视图后,**无法再新建同名视图**,被一条自己刚扔掉的记录挡住。 + + 现在补上运行时迁移 `ensureViewDefinitionActiveIndex`(照 `metadata-protocol` 既有的 `ensureOverlayIndex` 范式),在 `kernel:ready` 用 raw SQL 发 `CREATE UNIQUE INDEX idx_sys_view_def_active … WHERE state = 'active'`: + + - **名额可回收**——归档视图不再占用名额,同名视图可以重建; + - **唯一性不放宽**——两条 `state='active'` 的同名同域行仍然被拒; + - **复用声明的索引名**——`syncDeclaredIndexes` 按名跳过,后续每次启动都不会把全量 UNIQUE 索引重新加回来; + - **降级只会退回今天的行为,不会更低**——迁移先用一个临时探针索引验证当前方言与数据确实能建出部分索引,成功后才替换既有索引。因此 MySQL / MariaDB(无部分索引)上原有的全量 UNIQUE 索引原样保留(归档行在该方言上仍占名额,以 `info` 记录),不会出现「旧索引已删、新索引没建成」的无约束窗口。 + + `metadata-core` 侧只更新了 `sys-view-definition.object.ts` 的注释:该声明现在被明确记为**降级形态**(供无部分索引的方言与不跑该迁移的宿主使用),不应删除。 + + 已知未涵盖:`owner` 为 NULL 的共享视图与 `organization_id` 为 NULL 的环境级视图,因 SQL UNIQUE 的 NULL-distinct 语义本来就不受该索引约束。这是早于本次修复的既有缺口,本迁移只改变**行范围**(`WHERE state = 'active'`)而不动键的拼写——这也正是它严格弱于被替换的索引、因而不可能在存量数据上建失败的原因。该缺口已另单记录。 + +- c9bf940: fix(metadata-protocol): 对象 overlay 写路径按真实 package id 记录 registry 归属,并由服务端强制盖 `_provenance: 'org'` + + `applyObjectRegistryMutation` 此前把每一次对象写入都硬编码登记在 `'sys_metadata'` 哨兵下。 + 该归属键同时就是包过滤键(`SchemaRegistry.getAllObjects(packageId)` 匹配的是 + `contributor.packageId`),因此通过 Studio 包工作区新建的对象,在自己所属包的过滤结果里 + 一直是空的,直到有别的路径重新登记它。现在改为使用该行真实的 `package_id`;哨兵只保留 + 给「没有绑定任何包」的写入,`rollbackMetaItem` 则从行本身读出绑定(而不是从请求读)。 + + 同一次改动里,服务端在**副本**上无条件盖 `_provenance: 'org'`,不再采信请求体里的值: + 只搬归属键而不盖章会立刻复活 cloud#970 —— `applyProtection` 会把带包 id 且自身没有 + provenance 的 body 默认标成 `'package'`,`getArtifactItem` 据此认定它是代码制品, + `object` 又声明了 `allowOrgOverride: false`,于是用户刚建好的对象在下一次保存时收到 + `403 not_overridable`。`metadata-read-decorations.ts` 有意不剥离 `_provenance`, + Studio 的 GET → PUT 往返会把它原样送回,所以这个事实必须由服务端陈述。 + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [07c68b0] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [e9b5265] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [01fd9e1] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [a5ca08d] +- Updated dependencies [6ce10bd] +- Updated dependencies [7618ee8] +- Updated dependencies [6965160] +- Updated dependencies [ecff951] +- Updated dependencies [2f59da0] +- Updated dependencies [1a53a02] +- Updated dependencies [7c6261a] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [9bc846b] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [3d4c545] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] +- Updated dependencies [92e13a0] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/formula@17.0.0-rc.6 + - @objectstack/lint@17.0.0-rc.6 + - @objectstack/metadata-core@17.0.0-rc.6 + - @objectstack/metadata@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/metadata-protocol/package.json b/packages/metadata-protocol/package.json index 71c4ed2d45..c35efaf09f 100644 --- a/packages/metadata-protocol/package.json +++ b/packages/metadata-protocol/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-protocol", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 885b4bf45d..7d1b52016e 100644 --- a/packages/metadata/CHANGELOG.md +++ b/packages/metadata/CHANGELOG.md @@ -1,5 +1,113 @@ # @objectstack/metadata +## 17.0.0-rc.6 + +### Patch Changes + +- 7c6261a: refactor(metadata): peel the stored envelope before an `api` row is parsed as an endpoint (#5309) + + Internal refactor — no authored format changes, no observable acceptance change + for the shapes the platform stores today. + + A metadata _type name_ is worn by two different documents: the **authored + declaration** (exactly its spec vocabulary) and the **stored row** (that + declaration plus the metadata layer's own bookkeeping — `packageId`, `state`, + `version`, `publishedDefinition`, `publishedAt`, `publishedBy`, written by + `MetadataManager.register` / `publishPackage` and read back by `publishPackage`'s + package filter). Both `ApiEndpointSchema` parse sites — `buildEndpointIndex` (the + load-time backstop) and `gateApiItemsForPublish` (the publish gate) — used to hand + the whole stored row to the schema, and only its unknown-key _stripping_ kept the + bookkeeping from being judged as endpoint vocabulary. + + `peelStoredEnvelope` (`packages/metadata/src/stored-envelope.ts`) now takes the + envelope off first, so the schema sees the authored body and nothing else: + + - a row carrying a `metadata` value IS an envelope around it — the body is that + value, everything beside it is bookkeeping. This is the `data.metadata ?? data` + rule the publish gate, `publishedDefinition` and `getPublished` already shared; + - otherwise the body is the row minus the declared bookkeeping keys. + + The peel returns views and never mutates the row, so every existing envelope + reader (`publishPackage`'s `packageId` filter, `query`'s `state` / `packageId` + filters, `revertPackage`) is untouched, and `publishedDefinition` still snapshots + `data.metadata ?? data` verbatim. + + One consequence worth naming: `buildEndpointIndex` was the last reader that did + NOT follow the layer's body-selection rule, so a publish envelope + (`{ name, packageId, state, metadata: {…} }`) used to pass the publish gate and + then be excluded from the endpoint index — its route answered 404. The two doors + now read the same document. + + This is the prerequisite for tightening `ApiEndpointSchema` (#5384): with the + schema flipped to `strictObject` locally, `packages/metadata` went from 11 failing + tests to 1, and the one left is an authored non-vocabulary key being refused by + name — which is what that tightening is for. + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [1a53a02] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [3d4c545] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/metadata-core@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + - @objectstack/metadata-fs@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/metadata/package.json b/packages/metadata/package.json index f2e5f0da6c..2ff6a6b312 100644 --- a/packages/metadata/package.json +++ b/packages/metadata/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 f2309a8449..d454e0eed7 100644 --- a/packages/objectql/CHANGELOG.md +++ b/packages/objectql/CHANGELOG.md @@ -1,5 +1,805 @@ # @objectstack/objectql +## 17.0.0-rc.6 + +### Minor Changes + +- 259459d: refactor(spec)!: retire `array_agg` / `string_agg` from `AggregationFunction` — `count_distinct` deliberately kept (#6188, ADR-0049) + + `AggregationFunction` declared eight functions; the SQL family compiles five. + `SqlDriver.mapAggregateFunc` and the Turso `RemoteTransport.aggregate` each lower + `count`/`sum`/`avg`/`min`/`max` and route everything else to one refusal, so + three of the eight were declared-but-unenforced against the backends this + platform targets — and, worse, the _set_ each backend implemented was different, + so "which aggregations can I use" had no answer an author could read off the + schema. + + What makes these two sharper than an ordinary inert declaration is that another + package had to carry a denylist for them. `service-analytics` subtracted + `array_agg` and `string_agg` by name in `UNSUPPORTED_AGGREGATES`, because + without that subtraction they reached the Cube strategy's `default` and came + back as `COUNT(*)` — **a row count in place of the value the author asked for**, + with no error and no log (objectui#2945). + + **The three unlowered functions were SPLIT, not retired as a block** (maintainer + ruling, 2026-08-07): + + - **`count_distinct` STAYS** and takes ADR-0049's _enforce_ leg. It is a + dashboard staple with one portable lowering (`COUNT(DISTINCT x)`), and + `service-analytics` lowers it already; the SQL-driver implementation follows + on its own card. Its declaration leads its implementation here by decision, + not by drift. + - **`array_agg` / `string_agg` take the _remove_ leg.** Display conveniences + with no measured pull, and `string_agg` never had one shape to lower to at + all: the delimiter is a second argument in PostgreSQL, a `SEPARATOR` clause in + MySQL and a differently named function in SQL Server. + + FROM → TO, both authoring surfaces: + + | Was | Now | + | :-------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | + | `aggregations: [{ function: 'array_agg', field: 'tag', alias: 'tags' }]` | no replacement — read the rows with an ordinary `fields` query and shape them in the caller, or materialise the roll-up as a stored field | + | `aggregations: [{ function: 'string_agg', field: 'name', alias: 'names' }]` | as above | + | `measures: [{ name: 'tags', aggregate: 'array_agg', field: 'tag' }]` | delete the measure — `compileDataset` already refused it by name, so it never produced a number | + + The retirement kit: + + - This is an enum **VALUE** retirement, so there is no `retiredKey()` tombstone: + the enum's own error map carries the prescription, keyed on the received value + so that only the two spellings which used to be legal are told they "were + removed" (the `crypto.hash` / `HookBodyCapability` precedent, #4391). A + mis-spelling still gets zod's list of the legal functions. For the same reason + nothing lands in `RETIRED_KEYS_BY_MAJOR` and the four surface ratchets are + byte-identical — no def and no authorable key changed. + - **ADR-0087 D2 conversion + D3 chain step** + (`dataset-measure-array-string-agg-removed`): `os migrate meta --from 16` + drops any `dataset.measures[]` declaring a retired aggregate, plus any derived + measure the drop strands, with a notice each. The measure is dropped rather + than stripped down because one with neither `aggregate` nor `derived` fails + the dataset's own refinement — a conversion whose output cannot parse is worse + than none. + - **D3 semantic entry** (`query-array-string-agg-retired`) for + `QueryAST.aggregations[].function`: a request surface, never stored, so there + is no source for the chain to rewrite and callers move their own queries. + - The engine's in-memory fallback (`@objectstack/objectql`) drops its arms for + both functions — a `switch` case on a value the enum no longer has does not + type-check, and a dead arm is how a retired vocabulary returns by accident. + - `service-analytics`' `UNSUPPORTED_AGGREGATES` is now **empty and kept**: it is + half of an arithmetic the lockstep suite enforces (`SUPPORTED = spec +vocabulary − this`), which is what stops the next aggregate added to the spec + from silently reaching that `COUNT(*)` default. + + **Behaviour that actually changes** — this is the rare narrowing that removes + reachable behaviour, and it is worth stating plainly: on `driver-mongodb` and on + the engine's in-memory fallback these two DID compute. A raw QueryAST + aggregation against those backends returned an array or a joined string and will + now be refused at parse. That unpredictability is precisely what the ruling + ended — an aggregation that worked on one backend and failed on another is not a + capability — and both of those backends are inside the #5499 freeze. Their code + is untouched; it is simply no longer reachable through a spec-valid request. On + the dataset path nothing changes: `compileDataset` refused both by name already. + + + +- 69787f0: A hook registration can now express "global, EXCEPT these objects" — `registerHook(event, handler, { excludeObjects })`. + + `registerHook` carried one scope face: `object`, an allow list (absent = global, `'*'` = every object). An allow list and a deny list are interchangeable only over a closed universe of object names, and this one is open — a successful `/meta` PUT registers new objects into a running engine, and `SchemaRegistry.registerObject` emits no event a plugin could subscribe to. So a registrant wanting "everything except these platform tables" had two options, both wrong: keep the skip list inside the handler as an early return, which leaves the registration global and makes the per-object gates (`hasHooksFor`, the bulk-write row-set read) answer "hooks apply" for objects the handler is about to skip; or enumerate the complement into `object`, which freezes the list at boot so an object created afterwards is silently not covered — a compliance regression for the audit plugin, and a silent one. + + `excludeObjects?: string | string[]` is the deny half, subtracted from whatever `object` admits: `matches = allowMatches && !excludeMatches`. Absent means subtract nothing, so every registration that compiled before still behaves identically. Declared on the registration rather than left to a predicate callback, so the scope stays static, printable — the `Registered hook` debug record now reports it — and introspectable by diagnostics. + + Two shapes are refused at registration, following the same reasoning as the empty-target ruling: an empty name (`''`, `['']`, or a blank member) would subtract nothing while reading as though it subtracted something, and `'*'` would subtract every object and leave a hook that can never fire (ADR-0078: no silently inert declaration). Both throw, naming the fix. `excludeObjects: []` is accepted — it is the honest spelling of "subtract nothing", and the natural value of a spread whose source list is empty. + + `triggerHooks` (dispatch) and `hasHooksFor` (the bulk-write gate) were two hand-written copies of one matching semantic; adding a second scope dimension to two copies is how they drift, so both now call one shared matcher. A property test pins the direction that matters — the gate is never tighter than the dispatch, since a looser gate costs a wasted query while a tighter one silently drops hooks that were going to fire. + + The authorable `HookSchema` is deliberately untouched: the consumer is plugin code registering in TypeScript, and no metadata author needs "global minus a list" today. The key stays off the authoring surface until real pull appears. + +- 82397b6: feat(drivers,objectql): `$regex` / `$options` are refused everywhere, and `$icontains` is implemented on the SQL family (#5702) + + The driver half of the #4706 ruling. #5701 landed the contract (the vocabulary, + the `RETIRED_FILTER_OPERATORS` prescriptions, the shared text case-set) and + #5710 flipped the last live producer — `plugin-auth`'s ObjectQL adapter, which + emitted `$regex` on the authentication path — so the refusal can now land + without breaking sign-in. + + **BREAKING for anyone writing `$regex` or `$options` in a filter.** Both are + refused on every backend with `INVALID_FILTER` / 400 and a message that names + the replacement. `$regex` was never a declared operator: `driver-sql` compiled + it to a LIKE-escaped substring (so `a.b` matched only the literal `a.b`), + `driver-memory` ran it as a real `RegExp` (so the same filter also matched + `axb`, and an _invalid_ pattern was caught and answered `false` — zero rows, in + silence), and `objectql`'s `having` did the same. Write `$icontains` for the + case-insensitive substring search this was almost always used for, `$contains` + for a case-sensitive one; a pattern that genuinely needs a regex has no + filter-level replacement. + + **`$icontains` now runs on the SQL family** — `driver-sql`, `driver-sqlite-wasm`, + and both of `driver-turso`'s transports (the remote one does not go through + knex, so it needed its own). It compiles to `LOWER(col) LIKE LOWER(?) ESCAPE ?` + through the same `applyLike` / `pushLike` that carries the `%` / `_` / `\` + escaping, as a `fold` parameter rather than a second emitter — a copied emitter + is where the escape class would have been dropped, and an unescaped `%` matches + every row. An empty or non-string comparand is refused on the validating walk + (an empty one matches every row, which widens rather than narrows). On SQLite + `lower()` folds ASCII only, which IS the contract (#4706 Q1 = A): `$icontains: +'café'` does not match `CAFÉ`. + + + + `driver-mongodb`'s unknown-operator arm was throwing a bare `Error` with no + `code` and no `status`, three lines from the helper in its own file that sets + `INVALID_FILTER` / 400 — a 500-shaped body for a 400-class client mistake. It + now speaks the same envelope as its three siblings. + + Two parts of the ruling are deliberately NOT in this change and stay tracked in + `scripts/check-driver-conformance.mjs`'s ledger: the `$contains` family's + case-sensitivity (#4706 Q2 = A) needs SQLite's `LIKE` replaced by a case-exact + construct in the driver, the RLS lowering and the analytics lowering together, + or one permission rule compiles to two row sets (#6518); and `$icontains` on the + JS evaluation faces needs the spec vocabulary to take the operator, which cannot + happen before `driver-memory` has an arm for it (#6520). + +- 1818998: feat(spec,objectql,metadata-protocol): validate-only data operation — ask for the write's verdict instead of predicting it (#6037, #4633 ruling D) + + `import`'s dry run predicted the write path's verdict with a hand-copied mirror + of the engine's rules (`rest/src/import-coerce.ts`). A copy cannot structurally + keep up with the family it mirrors — ADR-0104 value shapes, `format` checks, + object-level `validations`, the state machine — so ruling D replaces prediction + with the verdict itself. + + **New:** `DataProtocol.validateData(request)` returns the write path's verdict + for candidate rows and persists nothing. + + ```ts + const verdict = await protocol.validateData({ + object: "lead", + mode: "insert", // or 'update', which judges only supplied keys + data: [{ first_name: "John", email: "not-an-email" }], + }); + // → { valid: false, + // results: [{ valid: false, errors: [{ field: 'email', code: 'invalid_email', … }], warnings: [] }], + // posture: { valueShapeStrict: true, mediaValueShapeStrict: false } } + ``` + + **Declaration and execution land together, deliberately.** `engine.validate()` + (objectql) calls the same `validateRecord` / `evaluateValidationRules` that + `insert()` calls, and `metadata-protocol` implements `validateData` on top of + it. Agreement between preview and write is therefore guaranteed by + construction, and a test asserts it directly by running both against one engine + in both postures. This is the ruling's own clause, not a style choice: + `BatchOptions.validateOnly` was retired in #4052 as a flag that promised a dry + run while the batch surfaces persisted regardless, so a caller previewing a + mutation had it EXECUTED. The new operation avoids that spelling too — the + tombstone still stands and still rejects `validateOnly`. + + **The verdict is the target deployment's, not an absolute.** The response + carries the ADR-0104 `posture` it was reached under. On a self-certified + deployment a bad value shape is an error; on a warn-first one the same row is + valid and the finding appears in `warnings` with the same `code` — one finding + that changed buckets, not two vocabularies. An unconditionally-strict preview + was considered and rejected (#4633 option B): it would fail rows on every + un-migrated deployment that the write would have accepted, which teaches + authors to distrust the one gate in front of a bulk import. + + Two boundaries worth knowing, both deliberate and both documented at the + implementation: + + - **No hooks run.** `beforeInsert` fires before validation on the real path, so + a hook deriving a _business_ field could change a verdict this does not + simulate. Firing arbitrary user hooks in a preview — mail, outbound calls, + writes to other objects — is the #4052 defect in a new spelling, so the gap is + documented rather than closed. Audit/ownership stamps are `system`/`readonly` + and validation skips them regardless. + - **Warn-first admissions are not recorded as certification evidence.** The + `#4769` sink exists so a boot cannot certify a contract it has just written + against; a preview writes nothing, so recording there would let a _preview_ + block a later migration. + + Additive: `validateData` is optional on `DataProtocol`, and nothing existing + changes shape. `valueShapeStrictEffective` / `mediaStrictEffective` are now + exported from objectql's record validator so the response reports the posture + that actually decided the verdict rather than the raw deployment flag. + + Unblocks #4633's consumption half (rest/import adopting the operation and + retiring the `import-coerce.ts` mirror). + +### Patch Changes + +- ad878e7: Lifecycle Archiver 的冷侧 `keep` prune 现在也尊重 #4747 的 teardown abort 位。 + + PR #5956 给 `archiveObject()` 的批循环补上了 abort 检查,但循环**之后**那条腿 —— `archive.keep` 保留期在归档库上的谓词 DELETE —— 没跟上:批循环刚因为读到 `aborted === true` 而 break,紧接着仍会向正在关闭的 cold datasource 发一次 `deleteMany`。teardown 落在最后一批的热删里时同样如此,那时循环是按短页正常退出的,连再读一次 abort 位的机会都没有。 + + 冷侧 prune 是纯保留期回收,不像循环内的 `upsert` → `bulkDelete` 那样受「归档成功才热删」的配对约束,推迟到下一轮 sweep 不留任何不一致 —— 下一轮按同一个 `keep` 推出同一个 cutoff,清同一批行。 + +- 6f6fec7: fix(objectql,driver-sql): 自增号播种按声明的 `suffix` 定位计数器,两侧收敛到同一答案 (#6468) + + `autonumberFormat` 允许序号槽 `{0..0}` **后面**还有 token —— `renderAutonumber` + 专门返回 `suffix`,其契约就是 `prefix + zero-padded(seq) + suffix`。这类格式渲染 + 出的值**序号不在串尾**:`{000}-{YYYY}` 渲染成 `001-2026`,是很常见的单号写法。 + + 两侧的播种解析却都假定「串尾的数字就是计数器」,而且各错各的: + + - 引擎兜底播种 `seedAutonumber()` 取整串的**最后一个**数字段 —— 读到的是年份。 + 库里三行 `001-2026`/`002-2026`/`003-2026`(真实计数器 3)把计数器播种成 **2026**, + 下一个发出的号直接跳到 `2027-2026`; + - driver-sql 的 `scanMaxNumericTail()` 把 tail 里**所有**数字拼接后 `parseInt` —— + 同样三行读成 **12026**,下一个号是 `12027-2026`。 + + 于是**同一份元数据、同一批行,换个驱动号段就不一样**;中间跳过的号已经烧掉,事后 + 无法回收。只修一侧会把「两个不同的错误答案」变成「一个对一个错」,跨驱动仍不一致, + 所以两侧同 PR 修。 + + **修法:两侧解析器尊重已声明的 `prefix`/`suffix`。** 两个字符串都由调用方从 + `renderAutonumber` 的返回值取得后传入 —— 两侧都不再自行理解格式,driver-sql 只收 + 参数(`getNextSequenceValue` 仅多转发一个位置参数,序列逻辑本身未动): + + - **prefix / suffix 任一非空 ⇒ 计数器「有锚」**:取 prefix 之后的**首个**数字段, + 并在该行确实带有声明的 suffix 时先把它去掉; + - **两者皆空 ⇒ 「无锚」**:各自的既有读法**逐字保留**(引擎取整串最后一个数字段, + driver-sql 拼接全部数字)—— 无 `{0..0}` 槽的格式渲染的就是串尾裸计数器,而早于 + 格式存在的历史值根本没有锚可依。 + + **suffix 只在匹配时剥离,绝不要求匹配。** `{000}-{YYYY}` 的计数器 scope 是渲染后的 + **prefix**(此处为空),即全局一个计数器、只有显示的年份在变,所以去年的 `007-2025` + 持有计数器 7,必须计入。把 suffix 下推成 `like '%-2026'` 会把这些行整批漏掉、播种 + **低于**真实 max —— 那正是 #6249 修掉的重复单号伤害,自己再造一遍。因此 SQL 谓词 + 保持 `like 'prefix%'`,suffix 只在 JS 侧逐行使用。 + + 无后缀格式(`D-{0000}`、`{0000}`)两侧本来就正确,行为不变并已 pin 住;#6467 的 + 播种扫描结构未触碰。 + +- 10c4ea9: fix(objectql): 集合算子的标量比较值答 400 INVALID_FILTER 并点名期望形状,不再 500 DATABASE_ERROR + + `FieldOperatorsSchema` 声明 `$in` / `$nin` 的比较值是数组、`$between` 是 `[min, max]` 二元组,但入口处没有任何一层强制这条声明:`isFilterAST` 只看算子,`parseFilterAST` 照单下降,于是 `['status', 'not_in', 'done']` 变成 `{ status: { $nin: 'done' } }` 一路走到驱动。 + + **行为变化(用户可见)**:此前 `driver-sql` 把标量交给 `whereIn(field, scalar)`,答 **500 `DATABASE_ERROR`** —— 用服务端故障码报告一个调用方能自己改好的过滤器,且不说明是哪个算子、哪个字段、该写成什么。现在引擎在唯一收口点拒收,答 **400 `INVALID_FILTER`**,信息点名算子(同时给出 `not_in` / `nin` / `notin` 这类作者实际书写的拼法)、字段、收到的值与位置、以及可直接粘贴的正确形状,并声明该过滤器**未被应用**。 + + 覆盖两道门:直接调用引擎(`FilterArray` 下降路径)与 HTTP 面(协议层已自行下降成 `FilterCondition` 对象后再交给引擎)—— 后者正是本问题实测到的那道门。`find` / `findOne` / `count` / `aggregate` / `update` / `delete` 六个入口一致。 + + `$between` 的非二元组比较值一并收在同一处:`driver-sql` 与 `driver-memory` 各自已经拒收(措辞保持逐字一致),`driver-mongodb` 的分支则直接落空、不发射区间谓词 —— 收在收口点后三家答案一致。 + + **不变的**:`$in: []` / `$nin: []` 仍是合法谓词(分别表示「不匹配任何行」与「匹配所有行」);列表**成员**的类型不在此处复判(那是 #5234,另一个面);非集合算子的标量比较值不受影响,包括 `$gt` 的 ISO 日期字符串这类 `FieldOperatorsSchema` 声明更严、而各后端一致接受的形状。 + +- 9c82b89: fix(objectql): the update-path `readonly` strip now drops the value the CALLER submitted, not whatever value the key holds when it runs + + The static-`readonly` write strip runs after `beforeUpdate`, but decided what to + delete from a snapshot of the caller's KEY NAMES. Those are different facts the + moment a hook writes to a read-only column: `delete data[name]` took the hook's + value with it whenever the caller's payload happened to carry the same key. + + Behaviour change — a whole-record write-back no longer erases hook writes. The + reported shape: a REST caller reads a record, flips `status` to `published`, and + PUTs the whole record back — `published_at: null` included, because that is what + it read. The publish hook stamped `published_at` on the transition; the strip + then deleted the stamp, and the row committed as `status = "published"` with + `published_at = null`, which every view sorting or filtering by `published_at` is + undefined on. The same hook's `last_reviewed_at` — equally read-only, but not + echoed by the caller — landed in that same write. Two hook-derived writes, one + alive and one dead, decided by nothing but a key name collision. + + The entry snapshot now carries the caller's values, and a read-only key is + stripped only while it still holds the caller's own value. A key a hook + overwrote is a platform write and survives — the same verdict the runtime + already gave a read-only key a hook ADDS. + + Not a relaxation of the read-only write rule: a caller-supplied read-only value + that no hook overwrote is dropped exactly as before, on both the single-id and + predicate update paths, and `isSystem` / `preserveAudit` are untouched. The + insert path is unchanged. + + Known limit, by design: the snapshot is shallow, so a hook that mutates a + caller-supplied object or array IN PLACE is indistinguishable from a hook that + did nothing, and the field is still stripped. A hook that means to write a + read-only column should assign to it. + +- b7d3be4: by-id 更新的 SET 载荷不再携带「已被判定不是主键」的 `id`——算子对象 / 数组 / `null` / 假值标量不会再覆盖被更新那一行的主键列 + + `update(o, { id: { $in: ['a','b'] }, title: 'x' }, { where: { id: 'rec_1' } })` 的**派发**自 #5748 裁 A / PR #5919 起就是对的:算子对象不是主键,判定顺着阶梯落到 `where.id`,绑定 `rec_1`(`ENGINE_UPDATE_DISPATCH_CASES` 里就写着这一行,`expect: 'by-id'` / `expectId: 'rec_1'`)。#6262 / PR #6433 收口的是 **multi 臂**的载荷;**by-id 臂**的同一半一直没做。实测(origin/main,记录型 driver 驱动真实引擎): + + ``` + driver.update('task', 'rec_1', { "id": { "$in": ["a","b"] }, "title": "x" }) + ^^^^^^^^^^^^^^^^^^^^^^^^ 这是 SET 子句 + ``` + + `driver-sql` 的 `update()` 用**整个** `data` 出 `formatted`(`applyWriteColumnMap(formatInput(object, data))`,`id` 不在任何跳过名单里),于是 SQL 形如 `UPDATE task SET id = '{"$in":["a","b"]}', title = 'x' WHERE id = 'rec_1'` —— rec_1 的行标识被一个序列化的算子对象不可逆地覆盖。 + + 修法与 #6262 同构:**剥离**,而且只剥「派发已经裁定不是主键」的那一份。成员资格不在这里重新推导,而是**调用派发本身**去问(`resolveEngineUpdateDispatch(data, undefined)` 为 `by-id` 当且仅当 `data.id` 是真值标量)——`asScalarId` 是**故意不导出**的,"给同一个问题添第三种公开写法,正是一条规则长出第二条的方式"。 + + - **零 verdict 变更**:`ENGINE_UPDATE_DISPATCH_CASES` 一行未动,同一个调用仍派发 `by-id`、仍绑 `rec_1`,`engine-update-dispatch.test.ts` 全绿。响亮拒收(路线 B)要反转这条 case,属对 #5748 裁 A 的部分回退,需要新裁决,不在本次范围。 + - **标量 `data.id` 刻意不动**:那里载荷的 `id` **就是**被绑定的主键(标量 `data.id` 压过 `where` 与 `multi`),写出来是 `SET id = 'rec_1' WHERE id = 'rec_1'`,同值空写,冗余而非破坏,且是长期行为;要不要一并剥是另一个决定,已按现状钉死(对照 pin)。 + - **`data: { id: null }` 的回写入口是可达的**(静态读取,非端到端 HTTP 复现):REST 的 `PATCH /data/:object/:id` 只剥 `expectedVersion`,`UpdateDataRequestSchema` 把 `data` 声明为 `z.record(z.string(), z.unknown())`(接受 `null`),协议层 `updateData` 再把请求体**原样**交给 `engine.update(object, data, { where: { id } })`。客户端 GET 一条记录、改两个字段、整体 PUT 回来而序列化把 `id` 写成 `null`,就落在这里。 + - **假值标量同判**:`{ id: 0 }` / `{ id: '' }` 的**判定语义**按 #5747 / #5748 原样不变(仍绑 `where.id`),载荷同样剥离——只剥算子对象而留下假值标量,等于对同一个事实立第二条规则。 + + 被剥离时按 `warn` 记一条日志,点明后果与两种正确写法。与 #6262 同样刻意**不**走 `onFieldsDropped`:`DroppedFieldsEvent.reason` 是 `readonly` / `readonly_when` 两值的闭合枚举(#3407 / #3042),扩这个词表是 `packages/spec` 的改动、另有消费者,已单独记为 #6437。 + +- 2a0d65e: `multi: true` 更新的 SET 载荷不再携带 `id`——算子对象不会再被写进每一行的主键列 + + `update(o, { id: { $in: ['a','b'] }, title: 'x' }, { multi: true })` 的**派发**自 #5748 裁 A / PR #5919 起就是对的:算子对象不是主键,不再遮蔽派发阶梯,声明的 bulk intent 照做,调用落到 `driver.updateMany`。#5919 没有做、#5922 也按 PD #10 明确留在范围外的,是**载荷**那一半。实测(origin/main,记录型 driver 驱动真实引擎): + + ``` + updateMany({ object: 'probe_task' }, { "id": { "$in": ["a","b"] }, "title": "x" }) + ^^^^^^^^^^^^^^^^^^^^^^^^ 这是 SET 子句 + ``` + + 即驱动被要求把一个序列化的算子对象写进**每一条命中行**的主键列。五个后端会对这件事各给一个答案(#5240 / #4434 家族),而在接受它的后端上,命中行的身份不可逆地丢失。 + + 修法是**剥离**:走到 multi 分支本身就意味着 `resolveEngineUpdateDispatch` 答了 `multi`,即它在**两个** id 来源里都没找到真值标量 id——所以此刻 `data.id` 里的任何东西(算子对象、数组、`null`、假值标量)都是引擎**已经裁定不是主键**的值。同一个问题的同一个答案,只是多用在一层上:不是主键的东西,也就不该坐在主键列上。 + + - **零 verdict 变更**:`ENGINE_UPDATE_DISPATCH_CASES` 一行未动,`operator object in data.id WITH multi:true` 仍是 `'multi'`,`engine-update-dispatch.test.ts` 全绿。响亮拒绝(#6262 的 B 案)要反转这条刚落地的 case,属对 #5748 裁 A 的部分回退,需要新裁决,不在本次范围。 + - **无可达的合法写入被吞掉**:真值标量 `data.id` 压过 `where` 与 `multi`,根本到不了这个分支;而 N 行也不可能共用一个主键。 + - **单 id 路径零变化**:`driver.update(object, id, data, …)` 的主键走的是独立参数,载荷里的 `id` 只是冗余而非破坏,本次不动(已按现状钉死)。 + - **假值标量同判**:`{ id: 0 }` / `{ id: '' }` 的**判定语义**按 #5747 / #5748 原样不变(仍是 `multi`),载荷同样剥离——把算子对象剥掉却把假值标量留下,等于对同一个事实立第二条规则,正是 `engine-update-dispatch.ts` 这一族被抽出来防止的事。 + + 被剥离时按 `warn` 记一条日志,点明后果与两种正确写法(单行按 id 更新 / 用 `where` 选行集)。刻意**不**走 `onFieldsDropped`:`DroppedFieldsEvent.reason` 是 `readonly` / `readonly_when` 两值的闭合枚举(#3407 / #3042),扩这个词表是 `packages/spec` 的改动、有 batch 与 REST 协议响应两处消费者,不该搭引擎修复的车。 + +- f09a2e7: HAVING 求值对齐 #5298 的 NULL-safe 裁决:聚合行上没有值的列现在满足 `$nin` 与 `$notContains`,与 driver-sql / formula / service-analytics 一致(此前 HAVING 是唯一仍判否的求值面)。 +- 5d022a1: Correct the `HookContext.input` contract table on `input.options`: during + `before*` the slot holds the CALLER's engine options bag (`where` and `multi` + included), not `DriverOptions` — the engine merges the driver-facing keys onto + it only after the handlers return. The table's two `before` rows said + `DriverOptions`, a type that declares neither key, which reads as "a hook can + see no predicate at all"; the composed `ast` is what hooks cannot reach, while + the caller's raw predicate is right there and is an upper-bound approximation of + the row set (middleware only narrows) — the safe direction for the fail-closed + guards built on it. Pinned with a positive assertion in + `hook-input-shape-contract.test.ts`. +- 53ef057: fix(rest,objectql): the import dry run asks the engine for its verdict instead of predicting it (#4633 ruling D) + + `POST /api/v1/data/:object/import?dryRun=true` green-lit rows the very same + endpoint then rejected. Measured on 17.0.0-rc.1: a CSV cell aimed at a + structured `address` field reported `{ ok: 1, created: 1 }` on the dry run and + `{ errors: 1, code: 'VALIDATION_FAILED' }` on the real write. + + The dry run predicted the write's verdict with a hand-copied mirror of a slice + of the engine's rules (`import-coerce.ts`'s `firstMissingRequiredField` and + `firstConstraintViolation`). A copy cannot structurally keep up with the family + it mirrors: ADR-0104 value shapes (`address` / `location` / references / media), + `format` checks, object-level `validations` and the state machine had no + counterpart, and `coerceFieldValue` routes structured shapes through its + pass-through catch-all, so no verdict was formed at all. + + **The mirror is retired.** The dry run now calls `DataProtocol.validateData` + (#6037), which runs the same `validateRecord` / `evaluateValidationRules` that + `insert()` runs, under the deployment's own ADR-0104 posture — so a bad value + shape is an error on a self-certified deployment and an admitted warning on a + warn-first one, exactly as on the write. Agreement is by construction, not by a + copy kept in step by hand. + + Also in this change: + + - **`engine.validate()` now resolves `defaultValue`s and seeds owned roll-up + `summary` fields before validating, on `insert` mode**, because `insert()` + does. Without it a required-but-defaulted column left unmapped was previewed + `failed` and written `created` — a false alarm on the row a preview is meant + to reassure you about. `update` mode still does not default (#2706). + - **A row report failed by validation now names the offending column.** The + engine's `ValidationError` carries `fields[]`, so the row's `field` is set and + its `code` is the field-level code (`required`, `min_value`, `max_length`, + `invalid_type`, …) rather than the wrapper's `VALIDATION_FAILED`. This is the + same vocabulary the dry run and the per-cell coercion failures already spoke; + before, a `min: 0` violation was `min_value` on the dry run and + `VALIDATION_FAILED` on the write. + - **Dry-run rows may carry `warnings[]`** — findings this deployment admits + rather than rejects (ADR-0104 warn-first). The row is `ok`, and the complaint + is visible instead of living only in a server log line. + + A protocol that does not implement `validateData` (plugin-auth's identity + import, whose write is better-auth rather than the engine) is not handed a + substitute: its dry run reports coercion and create/update/skip resolution only. + An engine-derived preview of a non-engine write would report findings that write + never produces. + +- c804f19: LifecycleService 的空间回收(VACUUM 级)与分片轮转(DROP 过期分片)现在也尊重 #4747 的 teardown abort 位。 + + PR #5956 / #5755 / PR #6397 依次给 reap 分页循环、Archiver 批循环和冷侧 `keep` prune 补上了 abort 判定,但 `sweep()` 里还剩两条同形的腿: + + - **空间回收**:abort 判定在对象循环的**头部**,teardown 落在**最后一个**声明对象的 reap 内时,`batchedReap` 因读到 `aborted === true` 而 break,对象循环随即自然结束(不再经过那个判定),控制流直接落到回收循环 —— 向正在关闭的 datasource 发一次 VACUUM 级操作。 + - **分片轮转**:对象同时声明 `ttl` 与 `storage.strategy: 'rotation'` 时,ttl reap 已经读过位并 break,返回后轮转仅由 strategy 与驱动能力把关,无判定地 DROP 过期物理分片。 + + 两条腿都是「已经拿到答案之后作出的决定」,而不是恰好横跨 teardown 的一次 await。推迟均无代价:回收是纯粹的页面归还,不删任何行;轮转是 O(1) 的窗口回收,下一轮 sweep 用同一个 `shards × unit` 推出同一个窗口、清同一批分片 —— 至多晚一个 sweep 间隔。 + + 仅声明 `rotation`(无 `ttl`)的一路行为不变:该形态下 `reapObject` 在轮转之前没有任何 await,位在结构上必为 false,而非「无人读过」。 + +- dbe92a7: fix(metadata-protocol): boot 重水合按行的真实 package 绑定登记对象归属(#4636 裁 B 收官) + + `loadMetaFromDb` 的 object 分支从 `engine.find` 返回的行上读 `record.packageId`,而 `sys_metadata` 的列是 snake_case 的 `package_id` —— 该表达式恒为 `undefined || 'sys_metadata'`,于是每次重启都把**绑定了包**的对象 overlay 登记在 `'sys_metadata'` 哨兵下。改为读 `package_id`,与写路径、`getMetaItems`、以及相邻的非 object 分支一致。 + + 用户可见的行为差异:归属键同时就是包过滤键(`getAllObjects(packageId)`),所以此前一个对象在**创建时**出现在自己所属包的侧边栏过滤里,**重启之后就消失**;更要紧的是重启后的第一次编辑——boot 登记 `'sys_metadata'`、保存登记 `app.`,`registerObject` 抛 `already owned by package …` 被 `applyObjectRegistryMutation` 吞成 `console.warn`,保存回 `success: true` 而内存 schema 停在重启时的版本,这一笔编辑被静默丢弃(cloud#970 的重启面)。两侧统一到真实 id 后,过滤与编辑都跨重启成立。 + + `@objectstack/objectql` 仅同步 `registry.ts` 中 `isTenantAuthored` 的契约注释:PR1 标注的「这半句描述的是契约,还不是代码」随本次落地摘除。 + +- 1a53a02: fix(meta): `/meta` object reads stop reporting `readonly: false` on fields the write path refuses (#4513) + + `#4447` made the audit-provenance family (`created_at`, `created_by`, + `updated_at`, `updated_by`) engine-owned on the **write** path: the registry's + `applySystemFields` forces `{ readonly: true, system: true }` over a _declared_ + audit field, and `ObjectQL.update` strips a non-system caller's write to it. + + The **read** path never learned it. A `/meta` object read resolves through + `sys_metadata` overlay → MetadataService → SchemaRegistry, and only the last of + those three has been through `applySystemFields` — so an object whose built + artifact ships a materialized `created_at` carrying FieldSchema defaults + (`readonly: false`) reported that value to every client while writes to that + same field were being refused. Measured before the fix, all of the read exits + agreed with each other and disagreed with the engine: + + ``` + single read: {"type":"datetime","label":"Created At","readonly":false} + list read: {"type":"datetime","label":"Created At","readonly":false} + cached read: {"type":"datetime","label":"Created At","readonly":false} + layered read: {"type":"datetime","label":"Created At","readonly":false} + ``` + + One field, two answers — and the machine-readable one, the only face a client + or an AI author writing code off `/meta` can see, was the wrong one. + + **What changes.** Every `/meta` object read exit now reports the audit family + the way the engine enforces it. That covers the single-item read (both the + singular and plural type spelling), the list read, the cached/ETag branch, the + `?preview=draft` and `?state=draft` reads, and the layered read's `effective` + layer. `GET` bodies for objects that declare an audit field will show + `readonly: true, system: true` where they previously showed `readonly: false` + or omitted the keys; nothing else about the document changes, and the ETag for + such an object changes once. + + **What deliberately does not change.** + + - The layered read's `code` and `overlay` layers stay raw — showing the + package's declaration beside the governed `effective` value is the + diagnostic's whole point. + - `sys_metadata` still stores exactly what the author saved; the correction is + applied on the way out, so no phantom customization appears in the diff. + - An object that opts out of the audit family (`systemFields: false`, + `systemFields.audit: false`, `managedBy: 'better-auth'`) is untouched — the + engine enforces nothing there, so a read that claimed otherwise would be the + same lie pointing the other way. + - Only `readonly` and `system` are forced. Every other key an author writes — + `label`, `description`, `hidden`, `group`, and `type` for an external object + mapping a differently-typed remote column — stays theirs. + + The governance table moved from `packages/objectql/src/registry.ts` to + `@objectstack/metadata-core` (`AUDIT_FIELD_GOVERNANCE`, plus the + `applyAuditFieldGovernance` normalizer the read path applies), by the same + criterion and for the same cycle as the `#5619` engine-dispatch predicates: + `@objectstack/objectql` depends on `@objectstack/metadata-protocol`, so the + read path cannot import the table from the registry that enforces it, and a + second copy would agree only until someone edited one side. `objectql` + re-exports the symbol from its original path, so its public API is unchanged. + +- bfe689b: fix(objectql): the insert-path runtime-owned strip now drops the value the CALLER submitted, not whatever value the key holds when it runs + + `stripRuntimeOwnedFields` runs after `beforeInsert`, but decided what to delete + from a snapshot of the caller's KEY NAMES. Those are different facts the moment a + hook writes to a runtime-owned column: `delete result[name]` took the hook's + value with it whenever the caller's payload happened to carry the same key. The + insert-side twin of the update-path defect fixed in the previous release, and + wrong for the identical reason. + + Measured, one object `{ title: text, code: autonumber }` and one `beforeInsert` + hook assigning `ctx.input.data.code`: + + - the caller omits `code` — the committed record holds the hook's value + - the caller sends `code` — the committed record holds `"1"`, the sequence value, + because the hook's write was deleted + + The two calls differ in nothing but whether the caller's payload happened to + carry a same-named key, and the first outcome is what the strip's own warning + text promises every hook author: "A beforeInsert/beforeUpdate hook does NOT need + either — hook-written keys are not caller-supplied." So this brings the code to + its own documented contract. Behaviour change — a whole-record POST (read a + template, edit fields, submit everything back) necessarily echoes the record + number it just read, so a hook that re-issues or normalizes that number no longer + loses its write to the sequence. + + The entry snapshot now carries the caller's values — as an explicit shallow copy + taken ahead of the hooks, so a hook mutating `ctx.input.data` in place cannot + rewrite the record of what the caller sent — and a runtime-owned key is stripped + only while it still holds the caller's own value. + + Not a relaxation of the runtime-owned write rule: a caller-seeded record number + that no hook overwrote is dropped exactly as before, on both the single-row and + batch insert paths, with the same warning, the same `onFieldsDropped` event and + the same `strictReadonlyWrites` refusal. `isSystem` and `preserveAudit` are + untouched. The comparison is `Object.is`, so a caller-forged `NaN` is still + recognised as the caller's own value and dropped. + +- 3fb42d2: fix(objectql): 字段 `readonlyWhen` 在服务端看到的记录改为「对象声明的全量形状」(#4953) + + `materializeDeclaredFields`(#1871 / #4649)此前只接在两个求值接缝上: + `evaluateValidationRules`(对象级校验规则、字段 `requiredWhen`、option + `visibleWhen`)与生命周期 hook 的 `condition`。**字段 `readonlyWhen` 不在其中** —— + 写入路径上的 `stripReadonlyWhenFields` / `stripReadonlyWhenFieldsMulti` 直接把 + `{ ...previous, ...data }` 交给 CEL 求值。 + + 后果是同一个字段上的两条谓词对「记录是什么」给出相反答案:`` requiredWhen: +P`record.approved_at == null` `` 是一条可用的守卫,而写在同一字段上的 + `` readonlyWhen: P`record.approved_at == null` `` 只要驱动没把 `approved_at` + 这一列回读出来就会 fault;**而 `readonlyWhen` fault 是 fail-open**,于是作者声明 + 为冻结的字段被照常写入。某次写入是否被拦,取决于驱动回读了哪些列 —— 作者既看不见 + 也控制不了的存储细节。 + + 本次把这两个 strip 的 `record` 与 `previous` 两个根都过 `materializeDeclaredFields`, + 按维护者 2026-08-06 裁决(#4953)统一**服务端**接缝。 + + **这是一次可见的行为变化,方向如下:** + + - 稀疏行上原本 fault→ 放行的谓词现在正常求值,谓词为真则改动被剥离(即恢复本应生效的 + 只读约束)。`record.x == null` / `!= null` / `previous.x == null` 都属此类。 + - 相应地,`has(record.<已声明字段>)` 在全量绑定下恒为 `true`(物化出的 `null` 是一个 + 「存在且值为 null」的键,这是 CEL 自身的规则),`!has(record.<已声明字段>)` 恒为 + `false`。因此以 `readonlyWhen: !has(record.x)` 表达「x 为空时冻结」的写法**不再锁住 + 字段** —— 它原本也不是一条保证(在回读全部列的驱动上它从来不锁),现在它变成确定的 + `false`。要表达「为空时冻结」请改写为 `record.x == null`(即 `@objectstack/lint` + null-guard 闸门一直建议的写法)。 + + 未改动的部分:`readonlyWhen` 的 fail-open 策略本身;#4889 的 `parent` 未绑定 ⇒ + **LOCKED** 判定(`parent` 是另一个对象的行,不做物化);对象级 `script` / + `cross_field` 自 #4649 起的 fail-closed;INSERT 仍不走 `readonlyWhen` 剥离。 + 未读到前序行时(引擎未取或行已不存在)**不做**物化 —— 那样不是补齐缺失值,而是 + 凭空捏造一个与库中行相矛盾的值。 + +- 9bc846b: fix(objectql,lint): 服务端为 `requiredWhen` 绑定 parent 作用域,并把构建期硬闸扩到同一格 + + `readonlyWhen` 的 parent 作用域洞在 #4889 已经补上;同一个字段上、由同一个求值器处理的 + `requiredWhen` 隔一个槽位还漏着。detail 对象上声明的 + `` requiredWhen: P`parent.status == 'sent'` `` ——「表头一旦 Sent,每一行都必须填写说明」—— + 只在内联表格里被求值,服务端从来只绑 `record` / `previous`,谓词直接 fault 走 fail-open + 分支,写入带着空字段落库,API 还回 200。 + + 注意它与 #4889 是**镜像**而不是同一种故障:`readonlyWhen` fail-open 是**写进了本该冻结的字段**, + `requiredWhen` fail-open 是**收下了本该被拒的记录**。两者都是同一处声明点上的 `declared ≠ enforced` + (PD #10)。 + + 本次按维护者 2026-08-06 的裁决落 A + C 两条,**刻意不对称于 #4889**: + + - **A —— 绑作用域,求值语义不动。** 引擎用 #4889 已经建好的 + `resolveMasterDetailParent(s)` 解析主表头行并传入求值器,insert / 单 id update / + bulk update 三个调用点都覆盖。**不可求值仍然 fail-open**(记日志、跳过、放行): + 表头此刻读不到就 422 掉一次本来合法的写入,比 `readonlyWhen` 那边「拒掉一个字段」响得多。 + 这是 issue 的 B 案,明确不做,留给 ADR-0058 D5 下一次复审。 + - **C —— 改在构建期拦。** `@objectstack/lint` 的 parent 作用域闸原本只盖 `readonlyWhen`, + 现在同样判 `requiredWhen`:对象没有恰好一个 `master_detail` 关系时,`parent` 不是元数据 + 陈述过的事实,声明直接判 error。两格共用同一个闸,但**报错文案不同** —— 两边运行时的失败 + 方向相反(`readonlyWhen` fail-closed ⇒ 字段永远写不进;`requiredWhen` fail-open ⇒ 要求 + 永远不生效),文案指错了就等于给了相反的修法。运行时敢保持 fail-open,正是因为这道闸 + 拦住了那条会无声烂掉的声明。 + + 同一次改动里补了 ADR-0113 非回归判定在 parent 作用域下的正确输入:「存量行本来就违规吗」问的是 + **写入前**那一行的状态,而它挂的是**旧**表头。改挂(repoint)到另一个主表时,若把落地表头也 + 喂给这个前置判定,就会把「移到 Sent 表头之下」读成既有违规而放行 —— 正是本 issue 要堵的那个 + 收下动作,只是换了个入口。因此求值器新增 `previousParent`,仅在载荷确实改挂时由引擎解析, + 其余情况沿用同一行、不多付一次读。 + + 对象级 `script` / `cross_field` 规则共用这个求值调用点,自 #4649 起对不可求值谓词是 + **fail-closed**,本次**没有**给它们绑新根 —— 绑了会把它们今天拒掉的写入翻成接受。这条由 pin + 测试钉住(#4972 当初把本改动挡在范围外,就是为了这个爆炸半径)。 + + 仓内暂无 app 声明 parent 作用域的 `requiredWhen`(showcase 的 invoice line 用的是行作用域的 + `record.quantity >= 100`),所以这是补潜伏缺口,不改变任何现有 app 的写入行为。 + +- a841151: fix(objectql): `ctx.api.transaction()` joins an open ambient transaction instead of opening a second one (#6168) + + `ObjectQL.transaction()` has always started with the ADR-0067 D2 join: if an + ambient transaction is already open, it runs the callback inside that one and + reports `owned: false` rather than beginning a nested driver transaction. + `ScopedContext.transaction()` — the second implementation of the same + primitive, reached as `ctx.api.transaction(fn)` from hook and action bodies — + did not. It went straight to the default driver and called `beginTransaction()` + unconditionally. + + Its own TSDoc called it "a second implementation of the same thing" and lined + up against ADR-0119 D1's caveats one by one; the join was the single point that + never got aligned. That is now fixed, with the same branch, in the same + position — before the driver lookup and before `opts.require`, because an + ambient transaction _is_ a transaction and a caller who declared they cannot run + without one is served by joining it. + + **Behaviour change — a nested sandbox/hook transaction now rolls back with the + outer one.** Previously a hook fired from inside an `engine.transaction()` + whose body called `ctx.api.transaction(fn)` got a **separate** driver + transaction. That transaction committed itself, so its writes **survived a + rollback of the outer one**: the caller was told the unit of work had been + undone while some of its rows were still there, with nothing failing and + nothing logged. It also took a second connection for the duration — the + deadlock ADR-0067 D2 exists to avoid on a single-connection pool such as + SQLite's. After this change the inner call joins, writes on the outer handle, + and is undone by the outer rollback. + + If you have a hook or action body that used `ctx.api.transaction()` inside a + larger transaction _specifically_ to get an independently-committing unit — + an audit trail that must outlive a rollback, say — it no longer does. The + supported way to have a write survive a rollback is the ADR-0057 §3.6 system + ledger carve-out (`lifecycle.class` of `audit` / `telemetry` / `event`), which + routes the row to its own datasource and executes it outside the transaction by + decision rather than by accident. + + The callback's `owned` signal (#5696) now reports `false` on this path, as it + already did on the engine surface. It was never wrong before — this surface + really did always open its own transaction — but what it honestly described was + the defect. + + Two limits stay as they are, and are now stated in the method's TSDoc. The join + reads the engine's ambient `AsyncLocalStorage` store only, so the discrete + `beginTransaction`/`commit`/`rollback` trio — which deliberately never + populates that store, because its handle is threaded explicitly across + `setImmediate` boundaries — is invisible to it and is not joined. That is what + keeps the branch from mistaking an explicitly-threaded handle for an ambient + one. The QuickJS sandbox drives its VM-side `ctx.api.transaction(fn)` through + that trio rather than through this method, so a VM-side body is outside this + join; unattributable handles are tracked separately in #6167. + +- d86815e: fix(objectql): a `sys_file` hydrate read fault is no longer indistinguishable from "this record has no file" (#6116) + + A file-field value stored as an opaque `sys_file` id is enriched on read into + `{ id, name, size, mimeType, url }`. That one batched lookup sat behind a bare + `catch { return records }`: **every** failure — connection drop, timeout, + permission denial, query error, and the benign "the table was never + provisioned" — was answered with the same silent pass-through of un-hydrated + ids. Consumers (UI, export) then receive a bare id where a file reference was + due and render it as _no attachment_, so a live outage looked exactly like a + record that genuinely holds no file. That is the ADR-0110 D3 shape — a fault + wearing the appearance of legitimate absent data — carried here on a functional + surface rather than a durability one. + + **Fail-open behaviour is unchanged, deliberately.** A file-metadata read that + fails must not take down the record read that asked for it, so the ids still + pass through un-hydrated and no read starts throwing. This is a + diagnosability fix: what changes is that the two reasons stop being the same + silence. + + The catch now discriminates by error **type**, through the shared + `isMissingTableError` predicate (`@objectstack/metadata/errors`) — the same + call the engine's autonumber seeding already makes, never a hand-rolled + `code === '42P01'` copy: + + - **table never provisioned** — the storage plugin is present but schema sync + has not run. There are genuinely no committed rows, so the un-hydrated + answer _is_ the truth: passed through in silence, exactly as before, so an + app whose storage schema is not yet synced gains no per-read noise. + - **every other read failure** — the rows may well exist and simply were not + seen. One `warn` now names the parent object, the fields left un-hydrated, + how many ids went unresolved, the driver's own error, the consequence (those + ids will render as "no file" for this read) and the fix (check + storage/database availability, then re-read). Said once per read, not once + per record or per id. + + `warn` rather than `error` per the repo's degradation-log-level rule: nothing + on this path claims to have persisted anything, the answer is visibly smaller + for this response only, and the next successful read repairs it. + + Note for operators reading logs: the generic read handler one frame up already + logged `Find operation failed` for the failed sub-read. That line is unchanged + and is not a substitute — it is emitted identically for the benign and the + non-benign failure and describes the `sys_file` sub-read only, never the parent + object, the fields, or the degraded answer that was nevertheless returned. + +- 2bd4e5e: 修复自增号播种扫描的 5000 行窗口:`seedAutonumber()` 现在读取计数器 scope 内**每一行**的最大值 + + 引擎兜底路径(驱动未声明 `supports.autonumber`,即 memory / mongodb)的自增号播种,此前是一次 `limit: 5000`、**无排序、无过滤**的 `find`,把「任意 5000 行窗口内的最大值」当成了全表 MAX。两种情况下会播种出低于真实 MAX 的号: + + - 对象行数超过 5000; + - 某个 scope(日期 / `{field}` 分组)的行被其他 scope 的行挤出窗口 —— 前缀过滤此前只在 JS 侧做,而窗口早已选定。 + + 计数器随后从一个已被占用的号段起号。对声明了 `unique` 的记录号字段,这就是直接发出重复的业务标识符 —— 一个写错了的值,重试和重启都修不回来。 + + 改为完整扫描:以 `keysetWalk` 按 `id` 游标分页(而非 `offset`,理由见 #4363),把前缀下推为 `$startsWith`,数值最大值在引擎侧逐值解析得出。这与 SQL 驱动自身的播种形状(`scanMaxNumericTail`,`like 'prefix%'` 无 limit)一致。 + + 数值最大值刻意**不**委托给 `orderBy desc + limit 1` 或聚合 `max`:两者都按文本排序,而字典序等于数值序仅当 scope 内所有值补零到同一定宽 —— 格式语言并不保证这一点(无 `{0..0}` 槽位的格式渲染裸计数器,`'9' > '10'`;任何定宽在计数器越过后都会溢出)。 + + 扫描无法走完时(行缺 `id` 游标、或驱动未执行游标谓词)拒绝播种并大声失败,而不是用「已读部分的最大值」这个下界起号 —— 与 #6114 对读故障的处置同族:不分配号,不写入。 + + 声明了 `supports.autonumber` 的驱动(`driver-sql` / `driver-turso` / `driver-sqlite-wasm`)走各自的 `_objectstack_sequences`,不受影响,改动前后均不产生播种扫描。 + +- c9bf940: fix(metadata-protocol): 对象 overlay 写路径按真实 package id 记录 registry 归属,并由服务端强制盖 `_provenance: 'org'` + + `applyObjectRegistryMutation` 此前把每一次对象写入都硬编码登记在 `'sys_metadata'` 哨兵下。 + 该归属键同时就是包过滤键(`SchemaRegistry.getAllObjects(packageId)` 匹配的是 + `contributor.packageId`),因此通过 Studio 包工作区新建的对象,在自己所属包的过滤结果里 + 一直是空的,直到有别的路径重新登记它。现在改为使用该行真实的 `package_id`;哨兵只保留 + 给「没有绑定任何包」的写入,`rollbackMetaItem` 则从行本身读出绑定(而不是从请求读)。 + + 同一次改动里,服务端在**副本**上无条件盖 `_provenance: 'org'`,不再采信请求体里的值: + 只搬归属键而不盖章会立刻复活 cloud#970 —— `applyProtection` 会把带包 id 且自身没有 + provenance 的 body 默认标成 `'package'`,`getArtifactItem` 据此认定它是代码制品, + `object` 又声明了 `allowOrgOverride: false`,于是用户刚建好的对象在下一次保存时收到 + `403 not_overridable`。`metadata-read-decorations.ts` 有意不剥离 `_provenance`, + Studio 的 GET → PUT 往返会把它原样送回,所以这个事实必须由服务端陈述。 + +- a682670: 写入载荷里的算子对象在标量字段上被响亮拒收(#5922) + + **行为变化**:此前静默入库的算子对象现在被拒绝。`update('task', { title: { $in: ['a','b'] } }, …)` 会抛 + `VALIDATION_FAILED`(字段码 `invalid_type`),而不再把 `{"$in":["a","b"]}` 原样交给驱动写进 `title` 列。 + + 原本这条错误的命运取决于字段类型,而不取决于错误本身:`number` 会立刻响亮拒绝(`n must be a number`), + `text` 则零告警落库,之后以「这行的 title 变成了乱码」的形态在读路径上出现,离原因很远。实测(15 种字段类型, + 记录型 driver 驱动真实引擎)显示放行的远不止 `text`:`textarea`、未声明 `options` 的 `select`、以及 + `lookup` 等引用类(ADR-0104 warn-first)同样放行;而 `select`(有 options)/ `url` / `email` / `phone` + 之所以拒绝,只是因为 `String({ $in: […] })` 是 `"[object Object]"`,恰好过不了它们的正则或选项表 —— 一条 + 在 4 种类型上偶然成立、在另外 11 种上不成立的规则,作者无法从元数据预测。 + + 现在的规则只有一条:**声明值是标量的字段,一律不接受算子对象**。判定复用 spec 已导出的算子词表 + (`ALL_OPERATORS` + `RETIRED_FILTER_OPERATORS`),不是第六份手抄的 `startsWith('$')`,所以协议新增算子当天即 + 自动收口。消息与 ADR-0104 的形状拒绝同族(同一 `invalid_value_shape` 文案,四语言均已本地化),点名字段、 + 点名算子、点名声明类型。 + + 刻意不动的两处:`json` 等结构化 JSON 类继续放行(`{ "$in": [...] }` 存在 `json` 列里是用户数据,不是写错的 + filter);多值字段保留既有的 `invalid_type_array` 拒绝。`insert` 与 `update`(单行与 multi)三个校验入口均已覆盖。 + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [07c68b0] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [e9b5265] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [dba7747] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [6965160] +- Updated dependencies [dbe92a7] +- Updated dependencies [2f59da0] +- Updated dependencies [1a53a02] +- Updated dependencies [7c6261a] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [7e1b480] +- Updated dependencies [e39dd66] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [2873eb9] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [3d4c545] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] +- Updated dependencies [c9bf940] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/metadata-protocol@17.0.0-rc.6 + - @objectstack/formula@17.0.0-rc.6 + - @objectstack/metadata-core@17.0.0-rc.6 + - @objectstack/metadata@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Minor Changes diff --git a/packages/objectql/package.json b/packages/objectql/package.json index 7a47625ad6..bb55b4634a 100644 --- a/packages/objectql/package.json +++ b/packages/objectql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/objectql", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "Isomorphic ObjectQL Engine for ObjectStack", "main": "dist/index.js", diff --git a/packages/observability/CHANGELOG.md b/packages/observability/CHANGELOG.md index 0c6a35ef6b..f10d3f419c 100644 --- a/packages/observability/CHANGELOG.md +++ b/packages/observability/CHANGELOG.md @@ -1,5 +1,66 @@ # @objectstack/observability +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/observability/package.json b/packages/observability/package.json index a9c2b551ec..2b77fbb13b 100644 --- a/packages/observability/package.json +++ b/packages/observability/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/observability", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 dec6aa5f6f..d30028f819 100644 --- a/packages/platform-objects/CHANGELOG.md +++ b/packages/platform-objects/CHANGELOG.md @@ -1,5 +1,69 @@ # @objectstack/platform-objects +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [1a53a02] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [3d4c545] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/metadata-core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/platform-objects/package.json b/packages/platform-objects/package.json index 615ec372b5..6eb5242a74 100644 --- a/packages/platform-objects/package.json +++ b/packages/platform-objects/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/platform-objects", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "Core platform object schemas for ObjectStack — identity, security, audit, tenant, and metadata objects", "main": "dist/index.js", diff --git a/packages/plugins/embedder-openai/CHANGELOG.md b/packages/plugins/embedder-openai/CHANGELOG.md index 17f841ce5b..dc655015bc 100644 --- a/packages/plugins/embedder-openai/CHANGELOG.md +++ b/packages/plugins/embedder-openai/CHANGELOG.md @@ -1,5 +1,66 @@ # @objectstack/embedder-openai +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/plugins/embedder-openai/package.json b/packages/plugins/embedder-openai/package.json index 855082e0c3..b73c5c797f 100644 --- a/packages/plugins/embedder-openai/package.json +++ b/packages/plugins/embedder-openai/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/embedder-openai", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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.", "main": "dist/index.js", diff --git a/packages/plugins/knowledge-memory/CHANGELOG.md b/packages/plugins/knowledge-memory/CHANGELOG.md index 07c9dd8f76..4edb5bc9c4 100644 --- a/packages/plugins/knowledge-memory/CHANGELOG.md +++ b/packages/plugins/knowledge-memory/CHANGELOG.md @@ -1,5 +1,68 @@ # @objectstack/knowledge-memory +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/service-knowledge@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/plugins/knowledge-memory/package.json b/packages/plugins/knowledge-memory/package.json index 2e10958ab8..c36a32df47 100644 --- a/packages/plugins/knowledge-memory/package.json +++ b/packages/plugins/knowledge-memory/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/knowledge-memory", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "In-memory knowledge adapter for ObjectStack (dev / test reference implementation).", "main": "dist/index.js", diff --git a/packages/plugins/knowledge-ragflow/CHANGELOG.md b/packages/plugins/knowledge-ragflow/CHANGELOG.md index e9ae376ccf..cc20949560 100644 --- a/packages/plugins/knowledge-ragflow/CHANGELOG.md +++ b/packages/plugins/knowledge-ragflow/CHANGELOG.md @@ -1,5 +1,68 @@ # @objectstack/knowledge-ragflow +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/service-knowledge@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/plugins/knowledge-ragflow/package.json b/packages/plugins/knowledge-ragflow/package.json index e28e18b1e4..0c27775d30 100644 --- a/packages/plugins/knowledge-ragflow/package.json +++ b/packages/plugins/knowledge-ragflow/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/knowledge-ragflow", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "RAGFlow knowledge adapter for ObjectStack — production-grade RAG via the Apache 2.0 RAGFlow REST API.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-approvals/CHANGELOG.md b/packages/plugins/plugin-approvals/CHANGELOG.md index 48ac6a591a..79c51323fc 100644 --- a/packages/plugins/plugin-approvals/CHANGELOG.md +++ b/packages/plugins/plugin-approvals/CHANGELOG.md @@ -1,5 +1,77 @@ # @objectstack/plugin-approvals +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [07c68b0] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [e9b5265] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [6965160] +- Updated dependencies [2f59da0] +- Updated dependencies [1a53a02] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [3d4c545] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/formula@17.0.0-rc.6 + - @objectstack/metadata-core@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/plugins/plugin-approvals/package.json b/packages/plugins/plugin-approvals/package.json index b45d00bc91..96c355ef25 100644 --- a/packages/plugins/plugin-approvals/package.json +++ b/packages/plugins/plugin-approvals/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-approvals", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "Multi-step approval engine for ObjectStack — sys_approval_process + sys_approval_request + sys_approval_action + IApprovalService.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-audit/CHANGELOG.md b/packages/plugins/plugin-audit/CHANGELOG.md index 399e9157b4..28704b5a7b 100644 --- a/packages/plugins/plugin-audit/CHANGELOG.md +++ b/packages/plugins/plugin-audit/CHANGELOG.md @@ -1,5 +1,94 @@ # @objectstack/plugin-audit +## 17.0.0-rc.6 + +### Patch Changes + +- 0f8d16a: perf(plugin-audit): 审计跳过名单上到注册面,平台内部表不再为白读买单 (#5860) + + plugin-audit 的五个写入注册(`captureBefore` 的 `beforeUpdate`/`beforeDelete`, + `writeAudit` 的 `afterInsert`/`afterUpdate`/`afterDelete`)此前**不带任何对象范围**, + 因而在引擎眼里全部是全局 hook。"哪些对象要审计"这个知识一直存在 —— `SKIP_OBJECTS` + —— 但它停在 handler 内部的早退里,注册面上看不见。于是按对象计算需求的两道门只能保守 + 判真:#5284 的单 id `update()` 前置行门、#5038 的批量门,对 `sys_job_queue`、 + `sys_job_run`、`sys_upload_session`、`ai_traces` 这些表同样判"需要",每次写入白读一遍 + 行集,而 handler 的第一行就返回了。放大倍数最刺眼的是 `sys_job_queue`:每条队列消息 + 至少三次写入(publish / lease / terminal),自 #5160 起每封邮件都走它。 + + 现在这五个注册带上 `excludeObjects`(#5928 / PR #6575 落地的声明式排除面),名单由 + `SKIP_OBJECTS` **派生**而非重抄,两个面不可能各自漂移。handler 内的早退**保留**为纵深 + 防御 —— 它护住的是每一个非 hook 调用方 —— 所以审计写入的行为逐位守恒,变的只是引擎 + 能看见的范围。 + + **为什么是减法而不是允许列表**:对象全集在运行期是开放的。`/meta` PUT 会把新对象注册进 + 运行中的引擎,而 `SchemaRegistry.registerObject` 不发任何事件,插件侧没有可订阅的通道去 + 追平一份枚举出来的名单 —— 那样的名单会在启动时冻结,此后新建的对象**静默**不被审计,对 + 合规插件是无声的倒退。排除面没有这个失败模式:安装时没人听说过的对象默认被审计。这条性质 + 已单独钉在测试里。 + + 顺带,`writeCommentMentions` 收为 `{ object: 'sys_comment' }` —— 它的 handler 第一行本就 + 拒绝其他对象,这是一个封闭的单名允许列表,现有契约一直表达得了。行为不变,但它不再出现在 + 其他任何对象的 `afterInsert` 需求里。 + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/plugins/plugin-audit/package.json b/packages/plugins/plugin-audit/package.json index f2f1ae10ed..db56273a0c 100644 --- a/packages/plugins/plugin-audit/package.json +++ b/packages/plugins/plugin-audit/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-audit", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "Audit Plugin for ObjectStack — System audit log object and audit trail", "main": "dist/index.js", diff --git a/packages/plugins/plugin-auth/CHANGELOG.md b/packages/plugins/plugin-auth/CHANGELOG.md index 33033f9859..5f411295c2 100644 --- a/packages/plugins/plugin-auth/CHANGELOG.md +++ b/packages/plugins/plugin-auth/CHANGELOG.md @@ -1,5 +1,174 @@ # Changelog +## 17.0.0-rc.6 + +### Minor Changes + +- 1fa224a: feat(plugin-auth): the fixed-window counter gets its own `./rate-limit-storage` entry (#6040) + + `rate-limit-storage.ts` is the repo's ONE fixed-window counter — + `incrementFixedWindow` / `createLazyCounterStore` / `InProcessCounterStore`, + ADR-0069 D2 — and #4790's cross-reference asks later arrivals to reuse it + rather than write a third copy. They did, and from outside auth: + `@objectstack/runtime` counts inbound requests and endpoint policy through it, + and `@objectstack/service-sms` counts its daily SMS budget through it (#2814). + + `@objectstack/plugin-auth` published exactly one entry, `"."`, whose `export *` + chain takes **value** imports on `better-auth/adapters` + (`objectql-adapter.ts`) and `@better-auth/core/db` (`backfill-account-issuer.ts`). + Value imports are evaluated eagerly, so reaching those ~90 lines of counting + loaded `better-auth` + `@better-auth/{core,oauth-provider,scim,sso}` + `jose` + + `@noble/hashes` + `@objectstack/rest` + `@objectstack/platform-objects` first. + Measured against the built package: `require('@objectstack/plugin-auth')` puts + 109 modules in `require.cache`; the counter needs one. + + So the counter is now published on its own: + + ```ts + // before — 109 modules, the whole better-auth family + import { incrementFixedWindow } from "@objectstack/plugin-auth"; + // after — 1 module, 3.7 KB + import { incrementFixedWindow } from "@objectstack/plugin-auth/rate-limit-storage"; + ``` + + `tsup` emits the second entry with `splitting: false`, so it is a self-contained + bundle rather than a nominal split: `dist/rate-limit-storage.mjs` is 3.71 KB + against `dist/index.mjs`'s 330.28 KB, contains zero top-level imports and zero + occurrences of the string `better-auth`. The one better-auth reference that + survives is `import type { BetterAuthRateLimitStorage }`, which is erased at + build and costs a consumer nothing at runtime. + + **Nothing is removed.** The root still re-exports every one of these symbols, so + existing `@objectstack/plugin-auth` imports keep working unchanged — this is a + new entry point, which is why it is `minor` rather than breaking. The `patch` on + `runtime` and `service-sms` is the import-specifier switch in those packages; + their behaviour is identical. + + `src/rate-limit-storage-isolation.test.ts` pins the invariant from both sides, + in the shape `packages/types/src/node-isolation.test.ts` (#4700) established for + the `./node` split: it walks the real import graph from the subpath entry and + fails on any better-auth **value** import or any undeclared external package, + it fails if a consumer reaches the counter through the package root again, and + it fails if the root ever _stops_ pulling better-auth eagerly — because at that + point the split stopped buying anything and deserves re-measuring rather than a + suite that passes for the wrong reason. + +### Patch Changes + +- f8fe47e: feat(runtime,rest,plugin-auth,service-i18n,service-storage): route-ledger 条目类型加可选 `responseSchema` (#5791) + + #3877 的「最小首步」,维护者 2026-08-06 已批。**纯增量、零行为变更**:五个 route + ledger 的现有条目一行未改,字段缺省即「未声明」。 + + ## 为什么是这一步 + + #3877 量到的洞不是「发出的和声明的不一致」,而是**大多数路由根本没有可对账的声明**: + 237 条已挂载路由里 215 条是 `sdk` 面,而携带 schema 引用的是 **0 条**。于是同一单 + 里裁定了两件事——Stage C(批量补 ~190 条响应 schema)**永不排期**(一条响应 schema + 是「这个端点承诺什么」的产品决定,批量生产正是 #3676 / #3833 / #3847 / #3870 四个 + 缺陷的成因),以及先把「这条路由声明了什么」变成**可查询数据**,让 Stage D 的棘轮 + 将来有东西可棘。本次落地的就是后者。 + + ## 字段语义 + + `responseSchema` 是 `@objectstack/spec/api` 导出名,指向该路由**响应载荷**的声明: + 路由套 `{ success, data }` 信封时指 `data`,不套时指整个 body。信封本身不归它管, + 由 `pnpm check:route-envelope` 结构化守住——一个字段无法同时诚实地描述两层。 + + 五个 ledger 是五个各自独立声明、按约定同形的 interface,因此是五处同名同措辞的可选 + 字段,**不是**新建共享类型包。三个 ledger 明确要求保持 import-free(客户端守卫按 + 相对**源文件**编译它们),且 `zod` 并非每个持有 ledger 的包的依赖,故字段存的是 + **名字**而非 live schema 对象,解析放在能 import spec 的守卫里。 + + ## 已填的两条(实证,不是批量) + + 只填 #5682 已给出双断言覆盖(safeParse 判**值** + 键集判**键**)的 discovery 族两条, + 且刻意分处两个 ledger,以证明一个字段形状确实服务五个独立声明的条目类型: + + - `packages/runtime` `GET /discovery` → `DiscoverySchema`(走信封,指 `data`) + - `packages/rest` `GET /api/v1/discovery` → `DiscoverySchema`(裸发,指整个 body) + + `GET /api/v1` 这条 bare-base 别名**故意不填**:它与上面那条共用同一个 + `discoveryHandler` 闭包,但 #5682 的测试只驱动 `/api/v1/discovery`,「同一个 handler + 所以同一个形状」是对代码的论证而非对代码的测量。没有覆盖就不填。 + + ## 新增守卫 + + - `packages/client/src/route-ledger-response-schema.test.ts` —— 五个 ledger 的并集里 + 每一个 `responseSchema` 都到**活的** `@objectstack/spec/api` 导出里解析,并且真的 + 调用一次 `safeParse`(spec 的 schema 是 `lazySchema()` 代理,只查属性存在会被代理 + 陷阱满足)。含否定对照(少一个字母的名字、空串、导出了但不是 schema)与反空转下界。 + - `discovery-schema-conformance.test.ts`(runtime / rest 各一)—— 钉住 ledger 报的 + schema 就是该套件实际解析用的**同一个对象**,并各自测量了载荷所在的层级。 + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [de6b7f1] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [53ef057] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [465c5fc] +- Updated dependencies [2f59da0] +- Updated dependencies [83a3b1f] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [773f80a] +- Updated dependencies [f8fe47e] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/rest@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/plugins/plugin-auth/package.json b/packages/plugins/plugin-auth/package.json index 8aaabc33fa..6cccbd78bc 100644 --- a/packages/plugins/plugin-auth/package.json +++ b/packages/plugins/plugin-auth/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-auth", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "Authentication & Identity Plugin for ObjectStack", "main": "dist/index.js", diff --git a/packages/plugins/plugin-dev/CHANGELOG.md b/packages/plugins/plugin-dev/CHANGELOG.md index 8dd4d4498c..ac844d7f30 100644 --- a/packages/plugins/plugin-dev/CHANGELOG.md +++ b/packages/plugins/plugin-dev/CHANGELOG.md @@ -1,5 +1,115 @@ # @objectstack/plugin-dev +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [ad878e7] +- Updated dependencies [6f6fec7] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [f7d80f4] +- Updated dependencies [ae31a19] +- Updated dependencies [10c4ea9] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [de6b7f1] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [262e40d] +- Updated dependencies [d367f03] +- Updated dependencies [9c82b89] +- Updated dependencies [b7d3be4] +- Updated dependencies [2a0d65e] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [f09a2e7] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [1fe436d] +- Updated dependencies [59b794f] +- Updated dependencies [db59e9c] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [53ef057] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [465c5fc] +- Updated dependencies [c804f19] +- Updated dependencies [c51ffa5] +- Updated dependencies [dbe92a7] +- Updated dependencies [2f59da0] +- Updated dependencies [83a3b1f] +- Updated dependencies [1a53a02] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [bfe689b] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [1fa224a] +- Updated dependencies [3fb42d2] +- Updated dependencies [35f7fb4] +- Updated dependencies [82397b6] +- Updated dependencies [9bc846b] +- Updated dependencies [773f80a] +- Updated dependencies [f8fe47e] +- Updated dependencies [a841151] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [cca11e9] +- Updated dependencies [cfb549d] +- Updated dependencies [761a0ba] +- Updated dependencies [d86815e] +- Updated dependencies [be87153] +- Updated dependencies [2bd4e5e] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] +- Updated dependencies [c9bf940] +- Updated dependencies [a682670] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/objectql@17.0.0-rc.6 + - @objectstack/runtime@17.0.0-rc.6 + - @objectstack/rest@17.0.0-rc.6 + - @objectstack/driver-memory@17.0.0-rc.6 + - @objectstack/plugin-hono-server@17.0.0-rc.6 + - @objectstack/service-storage@17.0.0-rc.6 + - @objectstack/plugin-auth@17.0.0-rc.6 + - @objectstack/service-i18n@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/account@17.0.0-rc.6 + - @objectstack/setup@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/plugin-security@17.0.0-rc.6 + - @objectstack/service-realtime@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/plugins/plugin-dev/package.json b/packages/plugins/plugin-dev/package.json index 8e9ac57ade..e147e25457 100644 --- a/packages/plugins/plugin-dev/package.json +++ b/packages/plugins/plugin-dev/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-dev", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "Development Assembly Plugin for ObjectStack — wires the real platform stack for zero-config local development", "main": "dist/index.js", diff --git a/packages/plugins/plugin-email/CHANGELOG.md b/packages/plugins/plugin-email/CHANGELOG.md index bb050f3fb6..513a439f07 100644 --- a/packages/plugins/plugin-email/CHANGELOG.md +++ b/packages/plugins/plugin-email/CHANGELOG.md @@ -1,5 +1,72 @@ # @objectstack/plugin-email +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [07c68b0] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [e9b5265] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [6965160] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/formula@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/plugins/plugin-email/package.json b/packages/plugins/plugin-email/package.json index afff5a7e6e..0882c76680 100644 --- a/packages/plugins/plugin-email/package.json +++ b/packages/plugins/plugin-email/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-email", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "Email service plugin for ObjectStack — IEmailService + transport-pluggable outbound delivery with sys_email persistence.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-hono-server/CHANGELOG.md b/packages/plugins/plugin-hono-server/CHANGELOG.md index 076182fdcd..29b62fa463 100644 --- a/packages/plugins/plugin-hono-server/CHANGELOG.md +++ b/packages/plugins/plugin-hono-server/CHANGELOG.md @@ -1,5 +1,108 @@ # @objectstack/plugin-hono-server +## 17.0.0-rc.6 + +### Patch Changes + +- 1fe436d: fix(plugin-hono-server): `/auth/me/permissions` resolves position-bound grants through the canonical resolver (#6334) + + On a hono host, `/api/v1/auth/me/permissions` and `/me/apps` resolved the caller + through a standalone resolver in `current-user-endpoints.ts` that read + `sys_member` + `sys_user_permission_set` — and **nothing else**. It never read + `sys_user_position` / `sys_position_permission_set`, so a permission set bound to + a **position** — the ADR-0090 D3 distribution mechanism, and how the showcase app + grants every persona — was invisible to these endpoints: the response carried + `positions: []`, omitted the set from `permissionSets`, and withheld its + `systemPermissions`. + + That is the surface objectui's four `useCapabilityGate` gates read (toolbar, row + kebab, record header, bulk bar — ADR-0066 D4), while the data plane resolves + through SecurityPlugin's middleware on the canonical chain. So the server + **granted** the action and the UI **hid the button** from a user who genuinely + held the capability — the failure direction the fail-open design names as the + worse one. + + A second, quieter half of the same divergence: the hand-rolled envelope published + membership roles under `roles`, while `ExecutionContext` — and every reader in + that file — calls the field `positions` (ADR-0090 D3, "formerly `roles`"). The + endpoint's `positions` was therefore always `[]` and those names never reached + `resolvePermissionSets` either, independently of the position tables. + + The session lookup (the genuinely transport-specific part) stays where it is; all + grant aggregation now delegates to `resolveUserAuthzGrants`, the canonical + resolver's userId-driven core, which `@objectstack/core` exports for exactly this + caller shape — a surface that already knows who the principal is and needs the + same envelope with no HTTP request to resolve it from. Arriving with it, none of + it re-implemented: `sys_user_position` (null org = global, active-org match, + ADR-0091 validity windows), the implicit `everyone` audience anchor (ADR-0090 D5), + `sys_position_permission_set`, `mapMembershipRole` normalization, the + platform-admin derivation and posture rung, and the `ai_seat` synthesis. + + No response-envelope change: `positions` / `permissionSets` / `systemPermissions` + / `tabPermissions` keep their names and shapes, and now carry the grants the + server was already enforcing. + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/observability@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/plugins/plugin-hono-server/package.json b/packages/plugins/plugin-hono-server/package.json index 7f364e13ec..38418423ff 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": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "Standard Hono Server Adapter for ObjectStack Runtime", "main": "dist/index.js", diff --git a/packages/plugins/plugin-pinyin-search/CHANGELOG.md b/packages/plugins/plugin-pinyin-search/CHANGELOG.md index fb10a7948d..7ebbaaca75 100644 --- a/packages/plugins/plugin-pinyin-search/CHANGELOG.md +++ b/packages/plugins/plugin-pinyin-search/CHANGELOG.md @@ -1,5 +1,38 @@ # @objectstack/plugin-pinyin-search +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [259459d] +- Updated dependencies [ad878e7] +- Updated dependencies [6f6fec7] +- Updated dependencies [10c4ea9] +- Updated dependencies [9c82b89] +- Updated dependencies [b7d3be4] +- Updated dependencies [2a0d65e] +- Updated dependencies [f09a2e7] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [53ef057] +- Updated dependencies [c804f19] +- Updated dependencies [dbe92a7] +- Updated dependencies [1a53a02] +- Updated dependencies [bfe689b] +- Updated dependencies [3fb42d2] +- Updated dependencies [82397b6] +- Updated dependencies [9bc846b] +- Updated dependencies [a841151] +- Updated dependencies [d86815e] +- Updated dependencies [2bd4e5e] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [c9bf940] +- Updated dependencies [a682670] + - @objectstack/objectql@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/plugins/plugin-pinyin-search/package.json b/packages/plugins/plugin-pinyin-search/package.json index 96503728fd..830c06a3c8 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": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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).", "main": "dist/index.js", diff --git a/packages/plugins/plugin-reports/CHANGELOG.md b/packages/plugins/plugin-reports/CHANGELOG.md index 09201c3ea1..c1f9001e69 100644 --- a/packages/plugins/plugin-reports/CHANGELOG.md +++ b/packages/plugins/plugin-reports/CHANGELOG.md @@ -1,5 +1,68 @@ # @objectstack/plugin-reports +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/plugins/plugin-reports/package.json b/packages/plugins/plugin-reports/package.json index bff958763d..ea3f628730 100644 --- a/packages/plugins/plugin-reports/package.json +++ b/packages/plugins/plugin-reports/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-reports", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "Saved reports + scheduled email digests for ObjectStack — sys_saved_report + sys_report_schedule + IReportService.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-security/CHANGELOG.md b/packages/plugins/plugin-security/CHANGELOG.md index 1f1e63f757..3f0e4574bd 100644 --- a/packages/plugins/plugin-security/CHANGELOG.md +++ b/packages/plugins/plugin-security/CHANGELOG.md @@ -1,5 +1,72 @@ # @objectstack/plugin-security +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [07c68b0] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [e9b5265] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [6965160] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/formula@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/plugins/plugin-security/package.json b/packages/plugins/plugin-security/package.json index 1ff10b679c..c2540ddc8c 100644 --- a/packages/plugins/plugin-security/package.json +++ b/packages/plugins/plugin-security/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-security", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "Security Plugin for ObjectStack — RBAC, RLS, and Field-Level Security Runtime", "main": "dist/index.js", diff --git a/packages/plugins/plugin-sharing/CHANGELOG.md b/packages/plugins/plugin-sharing/CHANGELOG.md index 4ba8f59110..4356b75f10 100644 --- a/packages/plugins/plugin-sharing/CHANGELOG.md +++ b/packages/plugins/plugin-sharing/CHANGELOG.md @@ -1,5 +1,182 @@ # @objectstack/plugin-sharing +## 17.0.0-rc.6 + +### Minor Changes + +- 54299ca: feat(sharing): `ISharingService` 的每行写判定补三态 —— 放行 / 不表态 / 拒绝(#6428) + + #5492 的维护者裁决(2026-08-07,B 案)分两步兑现两种已声明的写扩权,本次是 **step 1: + 契约与默认实现**。plugin-security 前像门的 provenance 分层合成是 step 2,本次一行未动。 + + **为什么二态不够(实测,不是推演)。** `canEdit()` 用同一个 `true` 表达了两件事 —— + 「我有依据放行」与「本服务对这一行根本不设门」。对只**追加**一道门的调用方(sharing + 中间件、`sys_attachment` 父记录门、ADR-0055 master 判定)这没问题:`true` = 「我不拦 + 你」。对让这个答案去**顶替另一个权威的地板**的调用方就是 fail-open —— #5492 的 E2 实验 + 把前像写门委托给 `canEdit()` 后,在**没有 `owner_id` 列**的对象上,普通成员跨 creator + 的 UPDATE 变成 `ok: true`(main 上是 403),因为平台的 `created_by` 所有权地板正是这类 + 对象唯一的行级写门,而一个「不表态」的 `true` 把它盖掉了。 + + **新增契约面**(`@objectstack/spec/contracts`): + + - `SharingWriteVerdict = 'allow' | 'abstain' | 'deny'` —— 闭合联合,普通 TS 类型 + (非 zod 派生,不进 ADR-0122 的 pin 计数)。 + - `ISharingService.checkEdit()` / `checkDelete()` —— 三态主形态,动作边界照 ADR-0111 D3 + 继承:`edit` 级共享让 `checkEdit` 答 `allow`、同一行 `checkDelete` 仍答 `deny`;两者 + 的 `abstain` 集合完全相同(两道门对「哪些对象由共享设门」意见一致,只在动词上分歧)。 + + **兼容:`canEdit()` / `canDelete()` 原样保留,语义零漂移。** 它们被定义为三态的 + **投影** `verdict !== 'deny'` —— 从前对 public / 无 owner 字段 / bypass 对象返回的那个 + `true`,现在落在 `abstain` 上,投影回来仍是 `true`。真值表逐分支被测试钉住(9 个分支 + × 两个动词),因为 `resolveSharingCanEdit`(plugin-security)与 `sys_attachment` 父记录 + 门读的正是这一列,翻掉任何一格都是本 PR 未触及的包里的静默权限变更。 + + **fail-closed 落点:查询失败是 `deny`,永远不是 `abstain`。** 两者对合成方是相反的指令 + (`abstain` 把这一行交给另一个权威,`deny` 就地终结),把失败读成「没有意见」正是造出上述 + fail-open 的那个混淆。默认实现把所有权查询与共享查询整段包在 fail-closed 分支里,并 + `logger.error` 记名,不静默吞。 + + **行为变化(一处,方向收紧)**:引擎查询抛错时,`canEdit`/`canDelete` 从**向外抛**改为 + 返回 `false`。两个既有调用点本来就在自己那侧 catch 成 `false`(`resolveSharingCanEdit` + 的 #5386 fail-closed、attachment hook 的降级读),所以对它们是同一结果;其余调用点由 + 「异常中止写入」变成「403 拒绝写入」,严格不更宽松。 + + **解锁**:#5492 step 2 的前像门可以按 provenance 分层合成 —— `abstain` 回落平台所有权 + 地板、`allow` 按声明顶替地板、`deny` 维持拒绝 —— 而不必在 security 侧重算一份 + owner/depth/share/bypass(那会是同一契约的第二份实现)。#5491 与 #5492 同批落地。 + +### Patch Changes + +- db59e9c: hooks: drop the last three `doc` / `previousDoc` alias reads on a hook context — read the engine's own keys only + + Behaviour is unchanged: every one of these limbs guarded against a producer that + has never existed, so none of them could be reached. + + - `service-storage` attachment lifecycle read `ctx.result ?? ctx.input.doc ?? ctx.input.data` + - `plugin-sharing` primary-BU projection read `(ctx.input.data ?? ctx.input.doc).user_id` + - `runtime`'s hook sandbox read `engineCtx.input ?? engineCtx.doc` and `engineCtx.previous ?? engineCtx.previousDoc` + + Every ObjectQL write context spells the payload `data` — measured and pinned by + `hook-input-shape-contract.test.ts` in `@objectstack/objectql` ("insert carries + `data` — never `doc`", #5273). The top-level pair is the same family one level + up: `HookContextSchema` declares `input` / `result` / `previous` and neither a + `doc` nor a `previousDoc`, and `engine.ts` — the sole producer of a HookContext + — builds neither. The limbs survived only because the old `HookContext.input` + contract table documented insert as `{ doc, options }`; that table was corrected + in #5668, and the same alias was removed from `trigger-record-change` in #5671. + These are the remainder (#5906), removed rather than left as a second de-facto + contract (PD #12). + +- 8e13ca8: fix(plugin-sharing): share-link 路由把完整授权信封交给 enforcement,修复 `group` 姿态下建链恒 403(#6206,裁决 A 案的消费半边) + + `SharingServicePlugin` 的 share-link 路由此前在 `resolveAuthzContext` 之后重新 + 拼一个四字段对象(`userId` / `tenantId` / `positions` / `permissions`),而这个 + 对象被原样当作 enforcement context 喂进 `engine.find` —— 即 [Finding-2] + 「只能为你自己看得见的记录建链接」那道可见性校验。被丢在半路的是 + `accessible_org_ids`、`org_user_ids`、`systemPermissions`、`posture`、 + `tabPermissions`。 + + 实害(已复现,非仅代码读出):`group` 租户姿态下 `accessible_org_ids` 就是 + Layer 0 那堵墙(ADR-0105 D2),集合缺席即判否(fail closed)。于是可见性校验 + 查不到任何行,建链接对**调用方本来读得到的记录**返回 + `403 FORBIDDEN: Not permitted to share /` —— 一个已发布姿态上, + 已发布功能完全不可用。`single` 姿态(默认)不读该字段,行为不变。 + + 改法按维护者 2026-08-07 的 A 案裁决(契约半边 #6430 / PR #6511 已落):信封 + **整个**透传(`{ ...authz, isSystem: false }`),不再逐字段挑选 —— 逐字段挑选正是 + 这条缝出问题的方式,也是下一个新增授权维度会再次漏掉的地方。`posture` 随上下文 + 流动、不在 enforcement 处重推(ADR-0095 D2)。窄类型 `ShareLinkExecutionContext` + 保留,但只服务路由自己的 401 判定(认证与否),不再出现在任何裁决路径上。 + + `ShareLinkService.createLink` / `revokeLink` / `listLinks` 与 `canManageShares` + 探针的参数类型随之收成完整 `ExecutionContext`,与 #6511 落地的契约一致。 + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [ad878e7] +- Updated dependencies [6f6fec7] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [07c68b0] +- Updated dependencies [10c4ea9] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [e9b5265] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [9c82b89] +- Updated dependencies [b7d3be4] +- Updated dependencies [2a0d65e] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [f09a2e7] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [53ef057] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [c804f19] +- Updated dependencies [6965160] +- Updated dependencies [dbe92a7] +- Updated dependencies [2f59da0] +- Updated dependencies [1a53a02] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [bfe689b] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [3fb42d2] +- Updated dependencies [35f7fb4] +- Updated dependencies [82397b6] +- Updated dependencies [9bc846b] +- Updated dependencies [a841151] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [d86815e] +- Updated dependencies [be87153] +- Updated dependencies [2bd4e5e] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] +- Updated dependencies [c9bf940] +- Updated dependencies [a682670] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/objectql@17.0.0-rc.6 + - @objectstack/formula@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/plugins/plugin-sharing/package.json b/packages/plugins/plugin-sharing/package.json index 366113db2c..1bd2de3e9a 100644 --- a/packages/plugins/plugin-sharing/package.json +++ b/packages/plugins/plugin-sharing/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-sharing", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "Record-level sharing for ObjectStack — sys_record_share + middleware that enforces sharingModel + ISharingService.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-webhooks/CHANGELOG.md b/packages/plugins/plugin-webhooks/CHANGELOG.md index 039defc314..164a8fe055 100644 --- a/packages/plugins/plugin-webhooks/CHANGELOG.md +++ b/packages/plugins/plugin-webhooks/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/plugin-webhooks +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [f1850d8] +- Updated dependencies [eb91eba] +- Updated dependencies [17d0954] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/service-messaging@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/plugins/plugin-webhooks/package.json b/packages/plugins/plugin-webhooks/package.json index f5e52bb7b6..11708f27ba 100644 --- a/packages/plugins/plugin-webhooks/package.json +++ b/packages/plugins/plugin-webhooks/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-webhooks", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 d15820e0ef..9d8e074cfe 100644 --- a/packages/qa/dogfood/CHANGELOG.md +++ b/packages/qa/dogfood/CHANGELOG.md @@ -1,5 +1,125 @@ # @objectstack/dogfood +## 0.0.40-rc.5 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [2bc1876] +- Updated dependencies [1d0faa7] +- Updated dependencies [ad878e7] +- Updated dependencies [0f8d16a] +- Updated dependencies [6f6fec7] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [10c4ea9] +- Updated dependencies [8e2bbba] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [9c82b89] +- Updated dependencies [b7d3be4] +- Updated dependencies [2a0d65e] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [f09a2e7] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [db59e9c] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [53ef057] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [c804f19] +- Updated dependencies [dbe92a7] +- Updated dependencies [49f208b] +- Updated dependencies [5c2716b] +- Updated dependencies [2f59da0] +- Updated dependencies [1a53a02] +- Updated dependencies [7c6261a] +- Updated dependencies [2604d34] +- Updated dependencies [f1850d8] +- Updated dependencies [eb91eba] +- Updated dependencies [17d0954] +- Updated dependencies [643b7c7] +- Updated dependencies [bfe689b] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [1fa224a] +- Updated dependencies [3cc8676] +- Updated dependencies [e15bf7e] +- Updated dependencies [3fb42d2] +- Updated dependencies [35f7fb4] +- Updated dependencies [82397b6] +- Updated dependencies [9bc846b] +- Updated dependencies [f8fe47e] +- Updated dependencies [a841151] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [8e13ca8] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [3264516] +- Updated dependencies [2fdb36e] +- Updated dependencies [d48aad5] +- Updated dependencies [761a0ba] +- Updated dependencies [d86815e] +- Updated dependencies [be87153] +- Updated dependencies [2bd4e5e] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] +- Updated dependencies [c9bf940] +- Updated dependencies [a682670] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/objectql@17.0.0-rc.6 + - @objectstack/service-analytics@17.0.0-rc.6 + - @objectstack/plugin-audit@17.0.0-rc.6 + - @objectstack/service-storage@17.0.0-rc.6 + - @objectstack/plugin-sharing@17.0.0-rc.6 + - @objectstack/mcp@17.0.0-rc.6 + - @objectstack/metadata@17.0.0-rc.6 + - @objectstack/service-messaging@17.0.0-rc.6 + - @objectstack/plugin-auth@17.0.0-rc.6 + - @objectstack/verify@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/example-crm@4.0.92-rc.5 + - @objectstack/example-showcase@0.3.14-rc.5 + - @objectstack/connector-mcp@17.0.0-rc.6 + - @objectstack/connector-openapi@17.0.0-rc.6 + - @objectstack/connector-rest@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + - @objectstack/plugin-email@17.0.0-rc.6 + - @objectstack/plugin-security@17.0.0-rc.6 + - @objectstack/plugin-webhooks@17.0.0-rc.6 + ## 0.0.40-rc.4 ### Patch Changes diff --git a/packages/qa/dogfood/package.json b/packages/qa/dogfood/package.json index c76ce25258..6c2877dc8d 100644 --- a/packages/qa/dogfood/package.json +++ b/packages/qa/dogfood/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/dogfood", - "version": "0.0.40-rc.4", + "version": "0.0.40-rc.5", "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 41064a1d1b..11351dec4a 100644 --- a/packages/qa/downstream-contract/CHANGELOG.md +++ b/packages/qa/downstream-contract/CHANGELOG.md @@ -1,5 +1,66 @@ # @objectstack/downstream-contract +## 0.0.38-rc.5 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + ## 0.0.38-rc.4 ### Patch Changes diff --git a/packages/qa/downstream-contract/package.json b/packages/qa/downstream-contract/package.json index d1b1ca40df..95ec9c2113 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.38-rc.4", + "version": "0.0.38-rc.5", "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 0e1a7985a3..3a45f50029 100644 --- a/packages/qa/http-conformance/CHANGELOG.md +++ b/packages/qa/http-conformance/CHANGELOG.md @@ -1,5 +1,11 @@ # @objectstack/http-conformance +## 0.0.6-rc.5 + +### Patch Changes + +- @objectstack/core@17.0.0-rc.6 + ## 0.0.6-rc.4 ### Patch Changes diff --git a/packages/qa/http-conformance/package.json b/packages/qa/http-conformance/package.json index 4830df005b..27cc28472f 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.0.6-rc.4", + "version": "0.0.6-rc.5", "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/rest/CHANGELOG.md b/packages/rest/CHANGELOG.md index fa0bd6b5a8..ca4da24d04 100644 --- a/packages/rest/CHANGELOG.md +++ b/packages/rest/CHANGELOG.md @@ -1,5 +1,395 @@ # @objectstack/rest +## 17.0.0-rc.6 + +### Minor Changes + +- 465c5fc: REST 的 9 条 direct-mount 路由现在对 `RestServer` 可枚举,并随之进入 `GET {apiPath}/openapi.json` + + `package-routes.ts`(4 条 `packages.*`)与 `external-datasource-routes.ts`(5 条 + `datasources/:name/external/*`)一直绕过 `RouteManager`、直接挂在宿主 `IHttpServer` 上, + `RestServer` 因此不持有「这 9 条本次 boot 是否挂载」的事实。#5588(PR #5821)把 + `/openapi.json` 的 built-in 段改成服务器自身路由表的投影之后,这 9 条(其中 8 条在 + `rest-route-ledger.ts` 里是 `disposition: 'sdk'` 的真实能力)就不在生成的文档里 —— + 用 `/openapi.json` 生成客户端的 consumer 拿不到它们,任何基于 `getRoutes()` 的自省也看不见。 + + 现在两个 registrar 各自把「实际挂载的那一个数组」原样返回,由组合步骤 + (`mountAndRecordDirectRoutes`,`rest-api-plugin.ts` 调用)登记到 `RestServer` 上: + + - `RestServer.getRoutes()` 返回本次 boot 的**全部**已挂载路由,每条带 `source` + (`'route-manager' | 'direct-mount'`),类型为新导出的 `MountedRoute`; + - `/openapi.json` 的 built-in 段随之覆盖这 9 条,带各自的 summary / tags / 路径参数; + - 描述与挂载**同源**:返回的数组就是用来挂载的那个数组,不存在第二份手工清单。 + + 诚实性两个方向都保持不变:某次 boot 没有 `package` 服务 ⇒ `packages.*` 既没挂载、 + 也不出现在 `getRoutes()` 与文档里;federation 那 5 条无条件挂载(服务缺席时按请求答 503), + 所以它们始终出现 —— 文档说的仍然只是「什么被挂载了」。 + + 对使用者的影响:`getRoutes()` 的返回值多了 9 条(服务在场时)以及每条上的 `source` + 字段;既有的 `method` / `path` / `handler` / `metadata` 读法不变。 + +### Patch Changes + +- de6b7f1: fix(rest): dashboard 组件门禁在默认配置下真正执行 (#5881) + + ADR-0057 D10 的 `requiresService` 组件门禁 —— 剔除指向未注册可选服务的 dashboard + 磁贴 —— 在默认部署里一次都没跑过。`GET /meta/:type/:name` 的单条读取有一条缓存分支, + 它排除了 `app`(per-user RBAC 过滤)与 `doc` / `book`(per-caller audience),唯独没有 + 排除 `dashboard`;而 `enableCache` 默认为 `true`。门禁写在非缓存分支里,于是只有显式 + 关掉缓存的部署才会执行到它。 + + 后果正是该 ADR 点名要防的那一幕:在没有某个可选服务的部署里(比如单租户运行时里的 + Organizations KPI,其 `org-scoping` 服务不存在),console 会渲染一块绑定到缺失服务的 + 死磁贴 —— 尽管服务端的门禁代码在、测试也在。 + + **修复**:`dashboard` 与 `app` 同款,从缓存分支排除,两种拼写(`/meta/dashboard/x` + 与规范复数 `/meta/dashboards/x`)都覆盖。其它元数据类型的 ETag 快路径不受影响。 + + **为什么不是"把门禁提到分支之外、两条路径共用"** —— 那读起来更整齐,但 ETag 无法承载 + 门禁结论:validator 是**未过滤文档**的哈希,而 `notModified` 在 protocol 内部就已判定, + REST 层没有机会重判。共用之后送出的就是"过滤过的正文 + 指向未过滤正文的 validator"。 + 一次 boot 之内这没有危害(已注册服务集在 bootstrap 之后不可变),但 `Cache-Control: +private, no-cache` 意味着客户端**存下正文**、之后只做重验证,而存下的正文比进程活得久: + 一次关掉该可选服务的重新部署并不改变文档,ETag 不变 ⇒ 每次重验证都回 304 ⇒ 那块死磁贴 + 恰好在移除其服务的那次部署之后被永久缓存下来。放弃快路径的代价则接近于零: + `getMetaItemCached` 本就委托给 `getMetaItem`,服务端两条路做的是同样的工作,失去的只是 + 304 省下的正文字节。 + + 对调用方的可见变化:dashboard 的单条读取不再返回 ETag / 304,每次都是完整的 200。 + +- d9bef45: fix(spec,rest): `OVERLAY_PERSISTENCE_FAILED` leaves the error-code ledger — it lost its only producer (#5783) + + `ERROR_CODE_LEDGER` registered `OVERLAY_PERSISTENCE_FAILED` under + `@objectstack/metadata-protocol`, but nothing in the repository can emit it any + more. Its one emission point was the `catch` inside `saveMetaItem`'s legacy + raw-engine branch, and #5264 (PR #5782) deleted that branch. A registered code + with no producer is ADR-0112's "no silent fourth state" read backwards: the + vocabulary promises a client a code no response can carry, and the ledger's own + admission test cannot notice, because it checks casing, duplication and + shadowing — never whether anyone still throws the code. + + Verified before removing: a declaration-and-emission search over `origin/main` + finds the name only in the ledger row itself, two generated reference pages, one + `rest-server.ts` comment, one historical changeset plus its CHANGELOG entry, and + two `packages/rest` tests that construct the error themselves. No producer, and + no consumer — including `objectui` and `cloud`, both searched at their + `origin/main` — reads the literal. Removal only shrinks a dead row: nothing + gates an emission on ledger membership, so no runtime or gate starts rejecting + anything it accepted before. + + **Wire impact: none.** No response carried this code, so no client can lose one. + The narrowing is type-level: `ErrorCode` (`StandardErrorCode` ∪ the ledger, what + `ApiErrorSchema.code` validates) no longer admits the string, so TypeScript + would now reject `code: 'OVERLAY_PERSISTENCE_FAILED'` at a call site — and there + is no such call site left to reject. + + Note for whoever compiles the release: #5437's changeset + (`rest-5xx-message-withheld.md`) names this code as one of two examples of a + `code` that "still rides on the response". That sentence was accurate when it + was written; the other example, `NOT_IMPLEMENTED`, is unaffected and still + demonstrates the same behaviour. + + The two `packages/rest` tests that asserted `resolveErrorResponse`'s handling of + a declared 5xx keep their substance and switch to a producer that still exists — + `metadata-protocol`'s `batchData` atomic refusal (`501` / `NOT_IMPLEMENTED`) and + the surviving overlay-delete `500`. Three stale comments are corrected in the + same pass: the `agent` entry in `metadata-plugin.zod.ts` (which described a + routing mechanism replaced by #5086's 403 refusal), the reachability argument in + `rest-5xx-message-sanitization.test.ts`, and `resolveErrorResponse`'s own + docblock in `rest-server.ts`. + +- 53ef057: fix(rest,objectql): the import dry run asks the engine for its verdict instead of predicting it (#4633 ruling D) + + `POST /api/v1/data/:object/import?dryRun=true` green-lit rows the very same + endpoint then rejected. Measured on 17.0.0-rc.1: a CSV cell aimed at a + structured `address` field reported `{ ok: 1, created: 1 }` on the dry run and + `{ errors: 1, code: 'VALIDATION_FAILED' }` on the real write. + + The dry run predicted the write's verdict with a hand-copied mirror of a slice + of the engine's rules (`import-coerce.ts`'s `firstMissingRequiredField` and + `firstConstraintViolation`). A copy cannot structurally keep up with the family + it mirrors: ADR-0104 value shapes (`address` / `location` / references / media), + `format` checks, object-level `validations` and the state machine had no + counterpart, and `coerceFieldValue` routes structured shapes through its + pass-through catch-all, so no verdict was formed at all. + + **The mirror is retired.** The dry run now calls `DataProtocol.validateData` + (#6037), which runs the same `validateRecord` / `evaluateValidationRules` that + `insert()` runs, under the deployment's own ADR-0104 posture — so a bad value + shape is an error on a self-certified deployment and an admitted warning on a + warn-first one, exactly as on the write. Agreement is by construction, not by a + copy kept in step by hand. + + Also in this change: + + - **`engine.validate()` now resolves `defaultValue`s and seeds owned roll-up + `summary` fields before validating, on `insert` mode**, because `insert()` + does. Without it a required-but-defaulted column left unmapped was previewed + `failed` and written `created` — a false alarm on the row a preview is meant + to reassure you about. `update` mode still does not default (#2706). + - **A row report failed by validation now names the offending column.** The + engine's `ValidationError` carries `fields[]`, so the row's `field` is set and + its `code` is the field-level code (`required`, `min_value`, `max_length`, + `invalid_type`, …) rather than the wrapper's `VALIDATION_FAILED`. This is the + same vocabulary the dry run and the per-cell coercion failures already spoke; + before, a `min: 0` violation was `min_value` on the dry run and + `VALIDATION_FAILED` on the write. + - **Dry-run rows may carry `warnings[]`** — findings this deployment admits + rather than rejects (ADR-0104 warn-first). The row is `ok`, and the complaint + is visible instead of living only in a server log line. + + A protocol that does not implement `validateData` (plugin-auth's identity + import, whose write is better-auth rather than the engine) is not handed a + substitute: its dry run reports coercion and create/update/skip resolution only. + An engine-derived preview of a non-engine write would report findings that write + never produces. + +- 83a3b1f: fix(rest): `GET /meta/books/:name` no longer bypasses the ADR-0046 §6.7 audience gate (#6241) + + The single-item metadata read has a cached branch and an uncached one, and the + ADR-0046 §6.7 audience gate lives in the uncached one. The comment above the + cached branch's entry condition has always stated why `doc` and `book` must skip + it: + + > `doc` and `book` bypass the shared cache: their §6.7 audience gate is + > per-caller, and a shared ETag would leak gated content across viewers. + + The condition beneath that sentence compared the **raw** `:type` path segment + against the literals `'doc'` / `'book'`. The route serves both spellings, and + Prime Directive #3 makes the **plural** one canonical — so + `GET /api/v1/meta/books/:name` did not match the exclusion, took the cached + branch, and the audience gate never ran. `enableCache` defaults to `true`, which + made the failing path the default one. + + Measured against a real `RestServer` — one book declaring + `audience: { permissionSet: … }`, one signed-in caller holding no permission + set: + + ``` + singular "book" :: cachedCalls=0 status=[403] PERMISSION_DENIED + plural "books" :: cachedCalls=1 status=[] full gated body served + ``` + + Same book, same caller, two spellings of one route. `GET /meta/docs/:name` took + the same path. This was **fail-open**: the wrong outcome is disclosure of gated + documentation, not an availability error. + + **The fix is structural, not two corrected literals.** This is #3984 recurring + in the same file eight days later, so the handler now normalizes the type + **once** at the top (`RestServer.metaTypeSingular`) and every gate below reads + that local — a per-type gate added later has no raw param in scope to compare + against by accident. The cache exclusion and the §6.7 gate now read one shared + predicate, so "which types bypass the cache" and "which types are audience + gated" can no longer drift apart. A repository guard + (`pnpm check:meta-type-normalized`, AST-based, zero exemptions) refuses the next + raw comparison in `packages/rest/src`. + + **Behaviour change worth knowing:** `GET /meta/docs/:name` and + `GET /meta/books/:name` now take the uncached branch, as their singular + spellings always did, so those two responses no longer carry an `ETag` / + `Cache-Control` validator and a conditional request no longer answers `304`. No + other metadata type is affected. The cost is only the 304's saved bytes — + `getMetaItemCached` delegates to `getMetaItem`, so the server does identical + work either way — and the ETag it gave up was a hash of the **unfiltered** + document, which is the cross-viewer leak the exclusion exists to prevent. + +- 773f80a: fix(rest): REST 面的执行上下文补齐 ADR-0090 D9/D10 的 principal 分类(#6071) + + `resolveAuthzContext`(`@objectstack/core`)被提取出来,正是为了让两个 HTTP 入口 + 不再在**授权**上漂移。但它之后的一步 —— 把授权信封组装成 `ExecutionContext` —— + 仍是两份手写副本,而两份的字段集已经不一致:runtime / dispatcher 那份 + (`packages/runtime/src/security/resolve-execution-context.ts`)按 ADR-0090 D9/D10 + 设置 `principalKind`(必要时连同 `onBehalfOf`),`rest-server.ts` 的 `computeExecCtx` + 两个都不设。 + + 后果不在装饰面而在 enforcement 面:`plugin-security/explain-engine.ts` 的 + posture 下限、`security-plugin.ts` 的 agent 基线、`observability/perf-timing.ts` + 的披露闸门都读 `principalKind`,于是同一个请求走 dispatcher 与走 REST 会拿到不同 + 的上下文,读这个字段的判断在 `os serve` / `dev` 的数据与元数据路由上**从不成立**。 + 问题由 #5859 实施时的 dogfood 全栈 boot 插桩测得:到达消费方的键集里 `__kernel` + 在(自证是 rest-server 这条组装路径)、`principalKind` 不在。 + + 本次改动只补这一个传输上缺的字段,口径与 runtime 侧完全一致: + + - 会话(cookie)或 API key 背书的主体 ⇒ `principalKind: 'human'` —— 与 runtime + 侧「an authenticated (API-key) request resolves as a human principal, never + guest」的钉子同一判定。 + - `'agent'` 与随之而来的 `onBehalfOf` **在本传输上不可表达**:它需要一个指明已授权 + 客户端的 OAuth access token,而该凭据只在 dispatcher 的 `/mcp` 门上被接受 + (`acceptOAuthAccessToken`),正是为了不让粗粒度的工具族 scope 溜进 REST。 + - `'guest'` 同样不可表达:`computeExecCtx` 在信封没有 `userId` 时就返回 + `undefined`,匿名 REST 调用者本来就拿不到任何上下文(随后被 `enforceAuth` 401)。 + **匿名面零变化** —— 不给匿名调用者凭空发一个 guest 上下文。 + + 行为差量(逐条核过,无一条改变授权结果):`explain-engine.ts` 的 guest ⇒ `EXTERNAL` + 与 `security-plugin.ts` 的 agent 分支在 REST 面仍不成立(前者的 `!context?.userId` + 前肢本就恒真,后者读 `'agent'` 标签、且真正的兜底是委托 LINK);`perf-timing.ts` + 只认 `'service'` / `'system'`,`'human'` 不开闸。唯一可观测的新增是 explain 输出里 + 多回显一个 `principalKind: 'human'`(该字段在 explain schema 中本就是 optional)。 + +- f8fe47e: feat(runtime,rest,plugin-auth,service-i18n,service-storage): route-ledger 条目类型加可选 `responseSchema` (#5791) + + #3877 的「最小首步」,维护者 2026-08-06 已批。**纯增量、零行为变更**:五个 route + ledger 的现有条目一行未改,字段缺省即「未声明」。 + + ## 为什么是这一步 + + #3877 量到的洞不是「发出的和声明的不一致」,而是**大多数路由根本没有可对账的声明**: + 237 条已挂载路由里 215 条是 `sdk` 面,而携带 schema 引用的是 **0 条**。于是同一单 + 里裁定了两件事——Stage C(批量补 ~190 条响应 schema)**永不排期**(一条响应 schema + 是「这个端点承诺什么」的产品决定,批量生产正是 #3676 / #3833 / #3847 / #3870 四个 + 缺陷的成因),以及先把「这条路由声明了什么」变成**可查询数据**,让 Stage D 的棘轮 + 将来有东西可棘。本次落地的就是后者。 + + ## 字段语义 + + `responseSchema` 是 `@objectstack/spec/api` 导出名,指向该路由**响应载荷**的声明: + 路由套 `{ success, data }` 信封时指 `data`,不套时指整个 body。信封本身不归它管, + 由 `pnpm check:route-envelope` 结构化守住——一个字段无法同时诚实地描述两层。 + + 五个 ledger 是五个各自独立声明、按约定同形的 interface,因此是五处同名同措辞的可选 + 字段,**不是**新建共享类型包。三个 ledger 明确要求保持 import-free(客户端守卫按 + 相对**源文件**编译它们),且 `zod` 并非每个持有 ledger 的包的依赖,故字段存的是 + **名字**而非 live schema 对象,解析放在能 import spec 的守卫里。 + + ## 已填的两条(实证,不是批量) + + 只填 #5682 已给出双断言覆盖(safeParse 判**值** + 键集判**键**)的 discovery 族两条, + 且刻意分处两个 ledger,以证明一个字段形状确实服务五个独立声明的条目类型: + + - `packages/runtime` `GET /discovery` → `DiscoverySchema`(走信封,指 `data`) + - `packages/rest` `GET /api/v1/discovery` → `DiscoverySchema`(裸发,指整个 body) + + `GET /api/v1` 这条 bare-base 别名**故意不填**:它与上面那条共用同一个 + `discoveryHandler` 闭包,但 #5682 的测试只驱动 `/api/v1/discovery`,「同一个 handler + 所以同一个形状」是对代码的论证而非对代码的测量。没有覆盖就不填。 + + ## 新增守卫 + + - `packages/client/src/route-ledger-response-schema.test.ts` —— 五个 ledger 的并集里 + 每一个 `responseSchema` 都到**活的** `@objectstack/spec/api` 导出里解析,并且真的 + 调用一次 `safeParse`(spec 的 schema 是 `lazySchema()` 代理,只查属性存在会被代理 + 陷阱满足)。含否定对照(少一个字母的名字、空串、导出了但不是 schema)与反空转下界。 + - `discovery-schema-conformance.test.ts`(runtime / rest 各一)—— 钉住 ledger 报的 + schema 就是该套件实际解析用的**同一个对象**,并各自测量了载荷所在的层级。 + +- 88f9d94: fix(types,rest): one named unique-violation predicate — a MySQL conflict is 409 UNIQUE_VIOLATION, not 500 (#6250) + + **On MySQL, every unique-constraint conflict came back as `500 INTERNAL_ERROR`.** + The API contract registers `UNIQUE_VIOLATION` as a 409 code + (`packages/spec/src/api/error-code-ledger.zod.ts`), so a front end had no way to + tell "this email is already taken" from "the server fell over" — no retry advice, + no field to point at, and a 5xx in the operator's dashboards for what is an + ordinary client outcome. SQLite and Postgres deployments never saw it, which is + why it survived: their conflict prose happens to contain the words the mapping + looked for. + + **Cause: the conflict verdict was nested inside a leak heuristic.** REST's 409 + branch lived inside the true-branch of `looksLikeInternalErrorLeak()`, keyed on + the substrings `unique constraint` / `unique violation`. MySQL says + `ER_DUP_ENTRY: Duplicate entry '…' for key '…'`, which matches no limb of that + heuristic, so the conflict never reached the `if` at all and fell out of the + terminal `UNCLASSIFIED_FAULT`. Two unrelated questions — "is this a conflict?" + and "would echoing this text leak internals?" — had been fused into one, and + MySQL is where they disagree. + + Measured on the previous release, through the real error mapper: + + ``` + mysql, bare message 500 INTERNAL_ERROR → 409 UNIQUE_VIOLATION + mysql, knex-wrapped SQL 500 DATABASE_ERROR → 409 UNIQUE_VIOLATION + postgres, SQLSTATE only 500 INTERNAL_ERROR → 409 UNIQUE_VIOLATION + sqlite, message 409 UNIQUE_VIOLATION (unchanged) + postgres, message 409 UNIQUE_VIOLATION (unchanged) + ``` + + So the hole was never MySQL-only: the mapping read one of the two channels + drivers use. A Postgres error carrying SQLSTATE `23505` with unremarkable prose + was a 500 as well. + + **New: `isUniqueViolationError(error)`, exported from `@objectstack/types`.** One + named predicate replaces the substring test, reading every channel a driver + uses — `code` (`23505` / `ER_DUP_ENTRY` / `SQLITE_CONSTRAINT_UNIQUE`), `errno` + (`1062`), the message, and one step down the `cause` chain that pool and + query-builder layers wrap with. Its vocabulary is the union of the four + hand-written copies the repo already carried, so routing REST through it cannot + narrow any verdict clients rely on today; an unrecognised error is never a + conflict, because a false 409 tells an SDK not to retry and points the user at a + value that is fine. + + **The internal-leak classifier is byte-identical.** The fix hoists the conflict + question out of it rather than widening its criteria, so nothing else it guards + is reclassified as safe-to-expose. And the 409 body is fixed text: MySQL embeds + the offending user data in its message (`Duplicate entry 'a@b.com' …`) and + Postgres the index and column names, none of which reaches the client. The full + driver text still reaches the server log. + + No action needed. Clients that already handled `409 UNIQUE_VIOLATION` on SQLite + and Postgres now receive it on MySQL too. + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/observability@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + - @objectstack/service-package@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/rest/package.json b/packages/rest/package.json index 2bb0c629cd..05639c752d 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/rest", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 226264da16..83067260b2 100644 --- a/packages/runtime/CHANGELOG.md +++ b/packages/runtime/CHANGELOG.md @@ -1,5 +1,368 @@ # @objectstack/runtime +## 17.0.0-rc.6 + +### Minor Changes + +- cca11e9: **`createStandaloneStack` now dispatches `libsql://` / Turso URLs** instead of refusing them as an unsupported scheme (#5820). + + `detectDriverFromUrl()` recognised `memory://`, `postgres://`, `mongodb://` and `file:`, and threw on everything else — while `resolveDatabaseUrl()` listed `TURSO_DATABASE_URL` as one of its URL sources. A host that set it got the URL read in and then rejected on the way out. Since the CLI wired `libsql://` for `os serve` / `os start` (#5602), the same `OS_DATABASE_URL=libsql://…` booted under `os start` and failed under `os migrate`, which comes through this stack. + + What changed: + + - `libsql://…` and `http(s)://*.turso.…` resolve to the `turso` driver kind — the same two spellings the CLI classifies, kept identical on purpose. + - `databaseDriver: 'turso'` (and `OS_DATABASE_DRIVER=turso`) is accepted by the config schema. + - The driver comes from `@objectstack/driver-turso`, an **optional** install: it drags `@libsql/client` and its native bindings, so it is not a dependency of `@objectstack/runtime`. It is loaded lazily, only for a selection that asks for libSQL, and injected through the driver-factory seam `DefaultDatasourcePlugin` already exposes — so the connect path, the `bootCritical` fail-fast verdict, `OS_ALLOW_DRIVER_CONNECT_FAILURE` and the retained Setup → Datasources status are identical to every other kind. + - Package missing? The boot fails **loudly**, carrying the exact install command (`npm install @objectstack/driver-turso`) as data as well as prose. There is no SQLite fallback: a silent step-down would open an empty local database while your libSQL data stays untouched, and every write — including an `os migrate` DDL — would land in the wrong place (#3276). + - `databaseAuthToken` is no longer declared-and-ignored: the `turso` kind reads it, falling back to `OS_DATABASE_AUTH_TOKEN` and then the vendor's own `TURSO_AUTH_TOKEN` — the same precedence `os serve` uses. + + Unknown schemes still throw, and the message now lists `libsql://` among the supported ones. + +- cfb549d: **`createStandaloneStack` now dispatches `mysql://`, and an unknown `OS_DATABASE_DRIVER` value is refused instead of silently becoming SQLite** (#6265). + + Two halves of one defect family: a driver selection this stack could not dispatch. + + **`mysql://` — the #5820 split with a different scheme.** The CLI has classified `mysql://` / `mysql2://` as the `mysql` kind since forever (`inferDriverTypeFromUrl`), the shared datasource factory has always been able to build it (`SqlDriver` on the `mysql2` client), and `content/docs/data-modeling/drivers.mdx` lists it in the URL-inference table — only `detectDriverFromUrl()` in this package had no arm. So one `OS_DATABASE_URL=mysql://…` booted under `os start` and hard-failed under `os migrate` (which boots through this stack) with `Unsupported database URL scheme`. + + - `mysql://…` and `mysql2://…` resolve to the `mysql` kind, matched by character-for-character the same regex the CLI uses — the two functions answer the same question about the same URL, so a divergence between them _is_ the bug. + - The stack declares `{ driver: 'mysql', config: { url } }` and the shared factory builds it, exactly like `postgres`. No optional package and no new dependency: `mysql2` is already an optional peer of `@objectstack/driver-sql`, the same posture `pg` has, so a missing client surfaces at connect like it always did. + - `databaseDriver: 'mysql'` and `OS_DATABASE_DRIVER=mysql` are accepted; `sqliteFile` stays `null` for a MySQL target, so `os migrate`'s occupancy probe does not read a DSN as a file path. + + **`OS_DATABASE_DRIVER` is validated now.** `databaseDriver` in config was parsed by a zod enum (loud rejection) while the env var was a bare `as` cast — an assertion that checks nothing at runtime. An unrecognised value matched no dispatch arm and landed in the chain's trailing `else`: SQLite, in silence. `OS_DATABASE_DRIVER=mysql` with no URL therefore created a local `standalone.db` while the operator believed they were talking to MySQL, and a typo (`mysq1`, `postgress`) did the same; with a URL set it surfaced as the doubly-misleading "sqlite driver was selected but the URL does not look like a file path" for someone who never selected sqlite. This is the #3276 class. + + - Both paths now read **one** declaration (`StandaloneDatabaseDriverSchema`): the config key parses it, the env value parses it, the `ResolvedDriverKind` union is inferred from it, and the refusal enumerates its options rather than repeating them in a hand-written list. + - An unknown value throws, naming the value and every legal driver: `sqlite, sqlite-wasm, memory, postgres, mysql, mongodb, turso`. The env value is lower-cased first, matching the CLI's reader of the same variable; the accepted vocabulary is the enum and nothing else. + - The dispatch chain's trailing `else` is no longer "sqlite" — it is a `never` guard, so the _next_ kind added to the enum without a dispatch arm is a compile error rather than a wrong database. + + Unknown URL schemes still throw (the message now lists `mysql://`), and the "unknown driver" and "unknown URL scheme" refusals stay distinguishable. + +### Patch Changes + +- f7d80f4: fix(runtime): `callData` no longer has a `batch` arm that answers a silent, empty success (#5856) + + `callData`'s `action === 'batch'` arm returned `{ object, results: [] }` — an + HTTP 200 whose body a consumer cannot tell apart from "the batch ran and matched + nothing" — while opening no transaction and writing nothing. It was the only arm + in that function answering an unimplemented action with **success**: every other + unhandled action throws `400 Unknown data action: …`, and `aggregate` throws + `503` when the engine cannot serve it. Retry, idempotency and audit logic all + read a 200 + empty result set as one successful empty operation. + + Nothing could reach it, and that is the point: its safety lived **upstream**, in + a route table that happens not to spell `batch`, not in any guard of its own — + the ADR-0115 Evidence 5 / #4451 shape, where one route-table extension silently + turns a dormant branch into a live "successfully did nothing". Every entry point + was enumerated before removal (`/data` compares `parts[1]` against the literal + `'query'` and otherwise reads it as a record id; the MCP bridge, the actions + domain and `invokeBusinessAction` pass literals; the declarative endpoint + executor is bounded by `ApiEndpointSchema.objectParams.operation`, a closed enum + of find/get/create/update/delete; and `callData` is not part of this package's + export surface), so the arm is removed under ADR-0049 enforce-or-remove rather + than converted to a 501 nobody would ever receive. + + **Behaviour on every live path is unchanged** — no reachable request produced + that response. What changed is the answer waiting for the first caller who ever + does spell `batch`: a loud `400 Unknown data action: batch`, identical to any + other unknown action, instead of a silent success. Batching itself is untouched + and keeps its single owner: `@objectstack/rest`'s `registerBatchEndpoints` + mounts both `POST /batch` (atomic, cross-object) and `POST /data/:object/batch` + (per-object, ADR-0119) — which is exactly why a host serving only the + dispatcher reports `capabilities.transactionalBatch: false` (#5672). + +- 2a2a9fb: fix(spec,metadata-protocol,runtime): one place decides what an unset `NODE_ENV` advertises (#5936) + + A deployment whose operator never exported `NODE_ENV` must not describe itself as + `development` on `/discovery`: `environment` is a machine-readable field, a client + reads it to answer "am I talking to production?", and it may skip production warnings + or loosen a destructive action's confirmation on the answer. #5673 ruled that in and + fixed it — but only for one of the two producers, because that dispatch put + `packages/spec` out of scope. The other one, `MetadataProtocol.getDiscovery()` (served + by `@objectstack/rest`), went on answering `development` for exactly that input. + + The default now lives in the shared mapper, `resolveDiscoveryEnvironment`: an absent — + or blank — value resolves to `production`, and both producers pass the operator's value + through as they read it, neither carrying a default of its own. That is what makes it + one decision instead of two copies, and it means the next discovery producer inherits + the right answer without anyone remembering to copy a line. Patching only + metadata-protocol would have left a second copy of the default — precisely the drift the + shared table was created to prevent (#4828). + + "Unset" includes a blank value: `NODE_ENV=` exports an empty string, the runtime's + `getEnv` has always folded that into its default, and had the mapper treated blank as + "anything else" the two producers would have drifted again on that one input. + + **#4828's rule is untouched, and it points the other way on purpose.** A value that IS + set but is not a spelling this repo recognises (`qa`, `preview`) still degrades to + `development`, so nothing ever claims `production` on a guess. Absence is not a guess — + it is the host declining to say. + + Behaviour change to expect: a host that exports no `NODE_ENV` and serves `/discovery` + through `@objectstack/rest` now advertises `environment: "production"` where it + previously advertised `"development"`. A deployment that genuinely is development should + say so — `NODE_ENV=development` — which is what the runtime dispatcher has already + required since #5673. + + The mapping table above `NODE_ENV_TO_DISCOVERY_ENVIRONMENT` is corrected in the same + pass: its `unset / anything else -> development` row had been false for the runtime + caller since #5673 and is now two rows, one per rule. + +- db59e9c: hooks: drop the last three `doc` / `previousDoc` alias reads on a hook context — read the engine's own keys only + + Behaviour is unchanged: every one of these limbs guarded against a producer that + has never existed, so none of them could be reached. + + - `service-storage` attachment lifecycle read `ctx.result ?? ctx.input.doc ?? ctx.input.data` + - `plugin-sharing` primary-BU projection read `(ctx.input.data ?? ctx.input.doc).user_id` + - `runtime`'s hook sandbox read `engineCtx.input ?? engineCtx.doc` and `engineCtx.previous ?? engineCtx.previousDoc` + + Every ObjectQL write context spells the payload `data` — measured and pinned by + `hook-input-shape-contract.test.ts` in `@objectstack/objectql` ("insert carries + `data` — never `doc`", #5273). The top-level pair is the same family one level + up: `HookContextSchema` declares `input` / `result` / `previous` and neither a + `doc` nor a `previousDoc`, and `engine.ts` — the sole producer of a HookContext + — builds neither. The limbs survived only because the old `HookContext.input` + contract table documented insert as `{ doc, options }`; that table was corrected + in #5668, and the same alias was removed from `trigger-record-change` in #5671. + These are the remainder (#5906), removed rather than left as a second de-facto + contract (PD #12). + +- c51ffa5: sandbox: `ScriptContext.user` 由 `unknown` 收窄为命名联合 `ScriptUser`(#5521) + + 沙箱接缝 `ScriptContext`(`packages/runtime/src/sandbox/script-runner.ts`)把交给 hook / + action body 的调用者声明为 `user?: unknown`,类型系统对这个字段一无所知 —— 第四个 + dispatch 面明天再手搓一个 user 字面量,编译器不会说一句话。而"三个 dispatcher 手搓出三种 + 形状"正是 #5372 的成因:它能存在几个版本,部分原因就是没有任何声明可以违背。 + + 现在它是 `user?: ScriptUser`,`ScriptUser = ActorUser | HookContext['user']` —— 两个**实测 + 的真实生产者形状**的联合,与 33 行外的姊妹字段 `ScriptSession`(#5613 / #5991)同构: + + - action body 收 `ActorUser`(`security/actor-user.ts`,#5372 起的唯一生产者,#6011 后 + `positions` 为唯一拼法); + - hook body 收 `HookContext['user']`(ObjectQL `buildUser()` 的 `session.userId` 快捷方式: + `id` / `name` / `email` / `organizationId`,全部可选)。 + + 刻意**不**收成单一类型:hook 快捷方式不带 `positions` / `permissions` / `systemPermissions`, + 收成 `ActorUser` 会在 hook 面断言一套它从未生产过的授权词汇;也**不**收成 spec 的 + `EvalUser`(issue 选项 1)—— 实测 `buildUser()` 根本不产 `positions`,而 `EvalUser` 要求它, + 那是套着 spec 外衣的同一种过度声明。 + + 行为零变化:两个写入方从 `any` 引擎上下文赋值,唯一的 VM 侧读取方收 `unknown`。TS 消费者 + 可见,故走 patch。`ActorUser` 同时作为**类型**从包入口导出,使联合的两支都可被消费者命名。 + +- 1fa224a: feat(plugin-auth): the fixed-window counter gets its own `./rate-limit-storage` entry (#6040) + + `rate-limit-storage.ts` is the repo's ONE fixed-window counter — + `incrementFixedWindow` / `createLazyCounterStore` / `InProcessCounterStore`, + ADR-0069 D2 — and #4790's cross-reference asks later arrivals to reuse it + rather than write a third copy. They did, and from outside auth: + `@objectstack/runtime` counts inbound requests and endpoint policy through it, + and `@objectstack/service-sms` counts its daily SMS budget through it (#2814). + + `@objectstack/plugin-auth` published exactly one entry, `"."`, whose `export *` + chain takes **value** imports on `better-auth/adapters` + (`objectql-adapter.ts`) and `@better-auth/core/db` (`backfill-account-issuer.ts`). + Value imports are evaluated eagerly, so reaching those ~90 lines of counting + loaded `better-auth` + `@better-auth/{core,oauth-provider,scim,sso}` + `jose` + + `@noble/hashes` + `@objectstack/rest` + `@objectstack/platform-objects` first. + Measured against the built package: `require('@objectstack/plugin-auth')` puts + 109 modules in `require.cache`; the counter needs one. + + So the counter is now published on its own: + + ```ts + // before — 109 modules, the whole better-auth family + import { incrementFixedWindow } from "@objectstack/plugin-auth"; + // after — 1 module, 3.7 KB + import { incrementFixedWindow } from "@objectstack/plugin-auth/rate-limit-storage"; + ``` + + `tsup` emits the second entry with `splitting: false`, so it is a self-contained + bundle rather than a nominal split: `dist/rate-limit-storage.mjs` is 3.71 KB + against `dist/index.mjs`'s 330.28 KB, contains zero top-level imports and zero + occurrences of the string `better-auth`. The one better-auth reference that + survives is `import type { BetterAuthRateLimitStorage }`, which is erased at + build and costs a consumer nothing at runtime. + + **Nothing is removed.** The root still re-exports every one of these symbols, so + existing `@objectstack/plugin-auth` imports keep working unchanged — this is a + new entry point, which is why it is `minor` rather than breaking. The `patch` on + `runtime` and `service-sms` is the import-specifier switch in those packages; + their behaviour is identical. + + `src/rate-limit-storage-isolation.test.ts` pins the invariant from both sides, + in the shape `packages/types/src/node-isolation.test.ts` (#4700) established for + the `./node` split: it walks the real import graph from the subpath entry and + fails on any better-auth **value** import or any undeclared external package, + it fails if a consumer reaches the counter through the package root again, and + it fails if the root ever _stops_ pulling better-auth eagerly — because at that + point the split stopped buying anything and deserves re-measuring rather than a + suite that passes for the wrong reason. + +- f8fe47e: feat(runtime,rest,plugin-auth,service-i18n,service-storage): route-ledger 条目类型加可选 `responseSchema` (#5791) + + #3877 的「最小首步」,维护者 2026-08-06 已批。**纯增量、零行为变更**:五个 route + ledger 的现有条目一行未改,字段缺省即「未声明」。 + + ## 为什么是这一步 + + #3877 量到的洞不是「发出的和声明的不一致」,而是**大多数路由根本没有可对账的声明**: + 237 条已挂载路由里 215 条是 `sdk` 面,而携带 schema 引用的是 **0 条**。于是同一单 + 里裁定了两件事——Stage C(批量补 ~190 条响应 schema)**永不排期**(一条响应 schema + 是「这个端点承诺什么」的产品决定,批量生产正是 #3676 / #3833 / #3847 / #3870 四个 + 缺陷的成因),以及先把「这条路由声明了什么」变成**可查询数据**,让 Stage D 的棘轮 + 将来有东西可棘。本次落地的就是后者。 + + ## 字段语义 + + `responseSchema` 是 `@objectstack/spec/api` 导出名,指向该路由**响应载荷**的声明: + 路由套 `{ success, data }` 信封时指 `data`,不套时指整个 body。信封本身不归它管, + 由 `pnpm check:route-envelope` 结构化守住——一个字段无法同时诚实地描述两层。 + + 五个 ledger 是五个各自独立声明、按约定同形的 interface,因此是五处同名同措辞的可选 + 字段,**不是**新建共享类型包。三个 ledger 明确要求保持 import-free(客户端守卫按 + 相对**源文件**编译它们),且 `zod` 并非每个持有 ledger 的包的依赖,故字段存的是 + **名字**而非 live schema 对象,解析放在能 import spec 的守卫里。 + + ## 已填的两条(实证,不是批量) + + 只填 #5682 已给出双断言覆盖(safeParse 判**值** + 键集判**键**)的 discovery 族两条, + 且刻意分处两个 ledger,以证明一个字段形状确实服务五个独立声明的条目类型: + + - `packages/runtime` `GET /discovery` → `DiscoverySchema`(走信封,指 `data`) + - `packages/rest` `GET /api/v1/discovery` → `DiscoverySchema`(裸发,指整个 body) + + `GET /api/v1` 这条 bare-base 别名**故意不填**:它与上面那条共用同一个 + `discoveryHandler` 闭包,但 #5682 的测试只驱动 `/api/v1/discovery`,「同一个 handler + 所以同一个形状」是对代码的论证而非对代码的测量。没有覆盖就不填。 + + ## 新增守卫 + + - `packages/client/src/route-ledger-response-schema.test.ts` —— 五个 ledger 的并集里 + 每一个 `responseSchema` 都到**活的** `@objectstack/spec/api` 导出里解析,并且真的 + 调用一次 `safeParse`(spec 的 schema 是 `lazySchema()` 代理,只查属性存在会被代理 + 陷阱满足)。含否定对照(少一个字母的名字、空串、导出了但不是 schema)与反空转下界。 + - `discovery-schema-conformance.test.ts`(runtime / rest 各一)—— 钉住 ledger 报的 + schema 就是该套件实际解析用的**同一个对象**,并各自测量了载荷所在的层级。 + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [29e28a3] +- Updated dependencies [259459d] +- Updated dependencies [ad878e7] +- Updated dependencies [6f6fec7] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [07c68b0] +- Updated dependencies [10c4ea9] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [e9b5265] +- Updated dependencies [de6b7f1] +- Updated dependencies [01faeb1] +- Updated dependencies [d92ed03] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [262e40d] +- Updated dependencies [d367f03] +- Updated dependencies [9c82b89] +- Updated dependencies [b7d3be4] +- Updated dependencies [2a0d65e] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [dba7747] +- Updated dependencies [bf0ae99] +- Updated dependencies [f09a2e7] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [53ef057] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [465c5fc] +- Updated dependencies [c804f19] +- Updated dependencies [6965160] +- Updated dependencies [dbe92a7] +- Updated dependencies [2f59da0] +- Updated dependencies [83a3b1f] +- Updated dependencies [1a53a02] +- Updated dependencies [7c6261a] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [7e1b480] +- Updated dependencies [bfe689b] +- Updated dependencies [e39dd66] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [1fa224a] +- Updated dependencies [3fb42d2] +- Updated dependencies [35f7fb4] +- Updated dependencies [82397b6] +- Updated dependencies [9bc846b] +- Updated dependencies [773f80a] +- Updated dependencies [2873eb9] +- Updated dependencies [f8fe47e] +- Updated dependencies [a841151] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [3264516] +- Updated dependencies [2fdb36e] +- Updated dependencies [62159bd] +- Updated dependencies [d48aad5] +- Updated dependencies [761a0ba] +- Updated dependencies [d86815e] +- Updated dependencies [be87153] +- Updated dependencies [939f579] +- Updated dependencies [2bd4e5e] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [3d4c545] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] +- Updated dependencies [c9bf940] +- Updated dependencies [a682670] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/driver-sql@17.0.0-rc.6 + - @objectstack/objectql@17.0.0-rc.6 + - @objectstack/metadata-protocol@17.0.0-rc.6 + - @objectstack/formula@17.0.0-rc.6 + - @objectstack/rest@17.0.0-rc.6 + - @objectstack/service-datasource@17.0.0-rc.6 + - @objectstack/driver-memory@17.0.0-rc.6 + - @objectstack/driver-sqlite-wasm@17.0.0-rc.6 + - @objectstack/metadata-core@17.0.0-rc.6 + - @objectstack/metadata@17.0.0-rc.6 + - @objectstack/plugin-auth@17.0.0-rc.6 + - @objectstack/service-i18n@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/observability@17.0.0-rc.6 + - @objectstack/plugin-security@17.0.0-rc.6 + - @objectstack/service-cluster@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 13c11c0021..31de47e340 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/runtime", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 4f75a8887c..b4163686dc 100644 --- a/packages/sdui-parser/CHANGELOG.md +++ b/packages/sdui-parser/CHANGELOG.md @@ -1,5 +1,7 @@ # @objectstack/sdui-parser +## 17.0.0-rc.6 + ## 17.0.0-rc.5 ## 17.0.0-rc.4 diff --git a/packages/sdui-parser/package.json b/packages/sdui-parser/package.json index 57206bb66c..bd0138c53a 100644 --- a/packages/sdui-parser/package.json +++ b/packages/sdui-parser/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/sdui-parser", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "ObjectStack constrained JSX-source → SDUI SchemaNode tree compiler (parse, never execute). Isomorphic, zero React. ADR-0080.", "main": "dist/index.js", diff --git a/packages/services/service-analytics/CHANGELOG.md b/packages/services/service-analytics/CHANGELOG.md index 86b316361e..83ece1c45a 100644 --- a/packages/services/service-analytics/CHANGELOG.md +++ b/packages/services/service-analytics/CHANGELOG.md @@ -1,5 +1,510 @@ # Changelog — @objectstack/service-analytics +## 17.0.0-rc.6 + +### Minor Changes + +- 3264516: fix(driver-sql,service-analytics)!: 两类无意义比较对象不再编译成「静默空谓词」——`$in`/`$nin` 的对象成员与 LIKE 族的对象比较值一律拒收 (#5234) + + 两个形状此前都**编译通过、执行、并给出一个作者没写过的答案**,而且没有任何东西记录这件事: + + | filter | 改前 | 改后 | + | ---------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------- | + | `{status: {$in: ['a', {foo: 1}]}}` | 该成员绑不上任何行,查询答得**就像第二个成员从没被写过** | `INVALID_FILTER` / 400,点名 `index 1` | + | `{status: {$nin: [{foo: 1}]}}` | `NOT IN ('[object Object]')` —— **一行都没排除**,作者写下的排除悄悄没发生 | 同上 | + | `{name: {$contains: {}}}` | `LIKE '%[object Object]%'` —— 对一行文本恰好是 `[object Object]` 的记录,**真的命中了** | `INVALID_FILTER` / 400,点名 `StringOperatorSchema` | + | `{name: {$notContains: {}}}` | 反过来:为一个没人记录的理由**排除了一条真实记录** | 同上 | + + #5041(PR #5223)在 `assertCompilableComparand` 的头注释里把这两个形状写为 "Deliberately NOT + extended",理由是它们 fail-closed(只收窄结果集)、比 #5041 实测的裸 `TypeError` 低一级。**实测下来这 + 两条理由都不成立**:`$nin` / `$notContains` 方向是**放宽**(该排除的没排除,在 read-scope 下即 #5347 / + #5324 判过的 over-reach);而 `$contains: {}` 给的从来不是「零行」,是**错行**。 + + ## 三份实现一起动,否则修完仍是方言 + + 同一个 `String()` 宽容在本仓有多份;只收紧 `driver-sql` 会变成「哪个面接的就是哪个答案」—— + #5146 / #5332 / #5567 各花一轮消掉的那类分叉。守卫因此落在**每个包自己的收口点**,而不是三个发射器: + + - **`driver-sql`** —— `assertCompilableComparand`,#5041 已有的那一个门。 + - **`service-analytics` 的 `where` 门** —— `filter-normalizer.ts` 的 `fieldLeaves`。它是本包**唯一**的 + leaf 生产者,所以一处拒收同时覆盖三个消费方:`NativeSQLStrategy`(真正执行的语句)、 + `ObjectQLStrategy.generateSql`(`/analytics/sql` 回显)与 `ObjectQLStrategy.convertFilter`(引擎路径)。 + 这个顺序是关键而非顺手:`convertFilter` 是**生产者**,在那里 `String()` 会把对象洗成一个类型完全正确 + 的 `'[object Object]'` 字符串交给驱动,下游再严格的驱动也永远看不到它该严格的那个形状。 + - **`service-analytics` 的 read-scope 门** —— `read-scope-sql.ts` 的 `compileOperator`,它编译的 + `FilterCondition` 不经过上面那个门。 + + `like-pattern.ts` 与 `applyLike` 里的 `String(value)` **原样保留**:它们不再是缺陷所在,因为门前已经没有 + 渲染不出来的值能到达。两包的谓词由 `like-metacharacter-escape.test.ts` 逐值互锁——正是该文件已经用来锁 + 转义表达式的同一套办法。 + + ## 围栏是 allow-list,而且每一条都是实测后决定的 + + 抄 `driver-turso` `RemoteTransport` 的形状(cloud#1004 / #1058):deny-list 会把下一个被发明出来的值形状 + 悄悄放进来,这正是那个 bug 熬过第一次修复的原因。顺带说明,**turso 自 #1058 起就已经拒收这两个形状**, + 所以本地 SQLite 与远程 SQLite 此前对同一条查询给的是不同答案;本次改动把它们收敛到一起。 + + 留在围栏内的(逐条实测,不是假设): + + - **数字 / 布尔 / `null`**:`{$contains: 5}` → `%5%`、`{$contains: null}` → `%null%` 在 `driver-sql`、 + `driver-memory` 与 analytics 两个面上**今天答案一致**,#5526 还专门把 `null` 这条钉住了。拒收它们是在 + **破坏**一致,不是建立一致——所以只拒**对象**。 + - **`Date`**:turso 的 allow-list 把它作为唯一的对象转换保留,拒收会重新叉开本地与远程。 + - **binary**:`$in` 成员照收(`isBindableComparand` 与写路径 `formatInput` 同一套分类),LIKE 拒收——它 + 绑得上但渲染不出作者想要的东西。这就是两个谓词而不是一个带 flag 的原因。 + - **`undefined`**:不可授权(JSON 没有 `undefined`),analytics 门按 #5526 / #5332 归一为 `null` 而非拒收; + 在 `driver-sql` 拒收它会**造出**一个分歧而不是消除一个,故照旧。 + + 被拒的**数组**是本次唯一一个「拒收即消分叉」的形状:`{name: {$contains: ['al','be']}}` 在 `read-scope-sql` + (与 `driver-sql`)绑 `%al,be%`,在 analytics 的 `where` 门却绑 `%al%`(它读 `values[0]`,后面的成员被 + 静默丢弃)。同一个包对同一条 filter 有两个答案,两个门现在都拒。 + + ## 作者需要知道的迁移 + + 这两个形状本来就没有能用的读法——`filter.zod.ts` 的 `StringOperatorSchema` 早就把 LIKE 族比较数声明为 + `z.string()`,本次只是让声明变成强制(Prime Directive #12,declared = enforced)。改后它们答 400 而不是 + 一个错答案;把比较数换成字面值即可。`{$eq: {…}}` **不在本次范围**,仍按 `toSqlBindValue` 绑 JSON(#5526 + 钉住的行为)。 + +### Patch Changes + +- 259459d: refactor(spec)!: retire `array_agg` / `string_agg` from `AggregationFunction` — `count_distinct` deliberately kept (#6188, ADR-0049) + + `AggregationFunction` declared eight functions; the SQL family compiles five. + `SqlDriver.mapAggregateFunc` and the Turso `RemoteTransport.aggregate` each lower + `count`/`sum`/`avg`/`min`/`max` and route everything else to one refusal, so + three of the eight were declared-but-unenforced against the backends this + platform targets — and, worse, the _set_ each backend implemented was different, + so "which aggregations can I use" had no answer an author could read off the + schema. + + What makes these two sharper than an ordinary inert declaration is that another + package had to carry a denylist for them. `service-analytics` subtracted + `array_agg` and `string_agg` by name in `UNSUPPORTED_AGGREGATES`, because + without that subtraction they reached the Cube strategy's `default` and came + back as `COUNT(*)` — **a row count in place of the value the author asked for**, + with no error and no log (objectui#2945). + + **The three unlowered functions were SPLIT, not retired as a block** (maintainer + ruling, 2026-08-07): + + - **`count_distinct` STAYS** and takes ADR-0049's _enforce_ leg. It is a + dashboard staple with one portable lowering (`COUNT(DISTINCT x)`), and + `service-analytics` lowers it already; the SQL-driver implementation follows + on its own card. Its declaration leads its implementation here by decision, + not by drift. + - **`array_agg` / `string_agg` take the _remove_ leg.** Display conveniences + with no measured pull, and `string_agg` never had one shape to lower to at + all: the delimiter is a second argument in PostgreSQL, a `SEPARATOR` clause in + MySQL and a differently named function in SQL Server. + + FROM → TO, both authoring surfaces: + + | Was | Now | + | :-------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | + | `aggregations: [{ function: 'array_agg', field: 'tag', alias: 'tags' }]` | no replacement — read the rows with an ordinary `fields` query and shape them in the caller, or materialise the roll-up as a stored field | + | `aggregations: [{ function: 'string_agg', field: 'name', alias: 'names' }]` | as above | + | `measures: [{ name: 'tags', aggregate: 'array_agg', field: 'tag' }]` | delete the measure — `compileDataset` already refused it by name, so it never produced a number | + + The retirement kit: + + - This is an enum **VALUE** retirement, so there is no `retiredKey()` tombstone: + the enum's own error map carries the prescription, keyed on the received value + so that only the two spellings which used to be legal are told they "were + removed" (the `crypto.hash` / `HookBodyCapability` precedent, #4391). A + mis-spelling still gets zod's list of the legal functions. For the same reason + nothing lands in `RETIRED_KEYS_BY_MAJOR` and the four surface ratchets are + byte-identical — no def and no authorable key changed. + - **ADR-0087 D2 conversion + D3 chain step** + (`dataset-measure-array-string-agg-removed`): `os migrate meta --from 16` + drops any `dataset.measures[]` declaring a retired aggregate, plus any derived + measure the drop strands, with a notice each. The measure is dropped rather + than stripped down because one with neither `aggregate` nor `derived` fails + the dataset's own refinement — a conversion whose output cannot parse is worse + than none. + - **D3 semantic entry** (`query-array-string-agg-retired`) for + `QueryAST.aggregations[].function`: a request surface, never stored, so there + is no source for the chain to rewrite and callers move their own queries. + - The engine's in-memory fallback (`@objectstack/objectql`) drops its arms for + both functions — a `switch` case on a value the enum no longer has does not + type-check, and a dead arm is how a retired vocabulary returns by accident. + - `service-analytics`' `UNSUPPORTED_AGGREGATES` is now **empty and kept**: it is + half of an arithmetic the lockstep suite enforces (`SUPPORTED = spec +vocabulary − this`), which is what stops the next aggregate added to the spec + from silently reaching that `COUNT(*)` default. + + **Behaviour that actually changes** — this is the rare narrowing that removes + reachable behaviour, and it is worth stating plainly: on `driver-mongodb` and on + the engine's in-memory fallback these two DID compute. A raw QueryAST + aggregation against those backends returned an array or a joined string and will + now be refused at parse. That unpredictability is precisely what the ruling + ended — an aggregation that worked on one backend and failed on another is not a + capability — and both of those backends are inside the #5499 freeze. Their code + is untouched; it is simply no longer reachable through a spec-valid request. On + the dataset path nothing changes: `compileDataset` refused both by name already. + + + +- 2bc1876: fix(service-analytics): refuse a dotted `measures` entry loudly instead of aggregating the base column (#5918) + + **Observable behaviour change.** An analytics query whose `measures` entry + carries a dot that is not the cube-name qualifier — `owner.region_count_distinct`, + `total.sum` — now answers `400 INVALID_FIELD` naming the entry **as the request + spelled it**. Some of these queries used to succeed. + + That is the point: succeeding is what was wrong with them. The auto-inference + path minted a measure by dropping the first segment of any dotted entry, so on + an object that happened to carry a same-named column the query ran + + ``` + SELECT COUNT(DISTINCT region) AS "owner.region_count_distinct" FROM "crm_account" + ``` + + — no JOIN, no error, a response column labelled with a relation attribute and a + number that came from the base table. The caller could not tell from the result + that it was wrong. Where the object had no same-named column it degraded to the + #4437 gate's `400 INVALID_FIELD`, which was honest about what reached SQL + (`aggregates field 'score'`) but named a string nobody had written; the caller + had sent `owner.score_sum`. + + `measures` was the fourth and last mint site of the punctuation #5739 sorted + out on `dimensions` / `where` / `timeDimensions`. It is ruled the other way, and + deliberately so: `lookupMember`'s relation-traversal tier is dimension-only, so a + dotted measure has no correct traversal answer to converge on. A refusal is the + honest answer, and it costs nothing that was working. Maintainer ruling, + 2026-08-07. + + Both a genuine traversal intent (`owner.amount_sum`) and a plain typo + (`total.sum`) get this refusal. They are lexically indistinguishable on this + path, and separating them would need field metadata the ad-hoc path does not + have. A real relation-traversal measure (`SUM("owner"."amount")` + LEFT JOIN) + would be a capability with its own justification, not a side effect of a strip. + + The refusal is applied at both places a Metric is minted from a request + spelling — the ad-hoc mint and the suffix-augmentation mint for a cube that is + already registered — because the ad-hoc path registers what it infers, so the + very same query reaches the second one from the second request onwards. + + Unchanged: the `.` qualifier (`crm_account.region_count_distinct`) is + still stripped and still runs; bare measures (`region_count_distinct`, `count`, + `created_at_max`) are untouched; a cube's own declared measure is authored, not + minted, so a Cube whose measure names a related column in its `sql` still + compiles the JOIN — which is the supported way to aggregate across a + relationship; and dotted **dimensions** still traverse, per #5739. + + **Migration.** Aggregate one of the object's own fields + (`_sum` / `_avg` / `_min` / `_max` / `_count_distinct`), or declare a Cube + whose measure names the related column. The refusal message says both, and names + the entry you sent. + +- 1d0faa7: fix(service-analytics): postgres 的「缺列」措辞不再被判为「缺源」(#6035) + + 数据集查询的降级路径靠驱动措辞判断「后端表没挂载」,从而把控件渲染成空网格而不是 500。 + 它的判据 `isMissingSourceError` 自己的文档写明范围**只含缺表/缺对象,不含列/语法错误—— + 后者要保持硬失败,好让真正的查询 bug 浮上来**。有一条 postgres 措辞按构造违反了这条承诺: + + ``` + column "label" of relation "acct" does not exist (SQLSTATE 42703) + ``` + + 它内部**逐字包含**一整段合法的缺表措辞 `relation "acct" does not exist`。#5717 把 postgres + 那一支从「同时含两个词的任意句子」收紧为锚定真实缺表措辞后,这条依然命中——它必然命中,因为它 + 字面上**就是**那段措辞。所以任何对「这句话是不是在说某个 relation 不存在」的收紧都排除不掉它, + 只有**先问更具体的问题**才可以:修法是一个**判定顺序**(先摘掉缺列措辞,再做缺源判定),而不是 + 一个更好的正则。 + + 两种后果都是错的,而具体触发哪一种只取决于措辞里那个关系名是否恰好是数据集自己的对象: + + - 名字是**被 JOIN 的表** → 报出一条响亮但**虚假**的跨数据源拓扑错误,把一个拼写错误说成数据源 + 布局问题; + - 名字是**数据集自己的对象** → 控件降级成空网格,只留一条 warn,拼错的列名不会告诉任何人。 + + 两半现在都作为回归钉住。判定顺序抄 `rest-server.ts` 的 `mapDataError` 自 #5352 起就在用的先例 + (它同样先摘出这条措辞,于是 REST 面回答 `400 INVALID_FIELD` 而不是 `404`),用的是同一条正则 + 而不是它的第二种方言——两个面不该对「postgres 什么时候在说 column」给出不同答案。兄弟函数 + `missingSourceRelation` 做同样的前置摘除:实测在修改前它对这条措辞回答 `sys_team`,只修其一会让 + 「是不是缺了什么」与「缺的是什么」相互矛盾,而那正是 #5717 在这一支上刚消除的分歧。 + + **这不修线上事故,而是让判据与它自己的文档一致。** analytics 是只读面,而 postgres 在 SELECT + 下的未知列措辞是 `column "bogus" does not exist`(不含 `relation`,本来就不命中); + `column … of relation …` 是 INSERT/UPDATE/ALTER 措辞。价值在于:这条分歧不再依赖「读路径不产生该 + 措辞」这个假设活着——哪天有任何写形状语句、驱动改措辞、或多包一层 `cause` 把它送到这个 catch + 面前,它会被正确分类,而不是被静默吞掉。 + + #5717 量过的 13 条仓内真实措辞全部重新钉住,并且是**按调用方可观测的结果**(空网格 / 拓扑拒收 / + 原样上抛)钉的,而不是按私有判据的布尔值——实测 **13 条里只有 1 条改判**,就是缺列那条,其余 12 + 条(三个驱动家族的措辞、框架的 not-registered 信号、本包自己的拒收)逐条不变。 + +- 8e2bbba: fix(service-analytics): `compareTo` 在「日期维度本身就是网格维度」时把比较桶键平移回当期 (#6007) + + 趋势图 + 同比是 `compareTo` 最常见的形状:日期维度既写进 `selection.dimensions` + (它就是图表的时间轴),又被 `compareTo` 用作锚点。这个形状下比较趟从来没有对齐过。 + + 比较趟查询的是**平移后**的窗口,所以它的行按平移后的桶键落地;而 + `mergeByDimensions` 按 `selection.dimensions` 元组建键 —— `2025-01` 不等于 + `2026-01`,于是**没有一条**比较行合并得进去,全部作为新行追加。两趟各自只报告了自己 + 那一半,`fillEmptyGroups` 把另一半填成自信的 `0`,再加上平移后的桶键坐在网格里,而它们 + 落在调用方筛选窗口之外。一个 2 桶窗口的「今年 vs 去年同期」回来是这样的: + + ``` + [{"close_date":"2025-01","opp_count__compare":5,"opp_count":0}, + {"close_date":"2025-02","opp_count__compare":7,"opp_count":0}, + {"close_date":"2026-01","opp_count":1,"opp_count__compare":0}, + {"close_date":"2026-02","opp_count":2,"opp_count__compare":0}] + ``` + + 四行、每行一个 0、两行在窗口外;期望是 2 行 × 2 列。 + + **修法(维护者裁决 2026-08-07,方向 1):合并之前,把每个比较桶键用当期的说法重述一遍。** + 上例现在返回 `[{close_date:'2026-01',opp_count:1,opp_count__compare:5}, +{close_date:'2026-02',opp_count:2,opp_count__compare:7}]`。 + + - `previousYear` —— 窗口是按日历年平移的,所以逆运算就是按日历年往前推一年:对桶自己的 + 首日做平移再重新分桶。`2025-01` → `2026-01`、`2025-Q1` → `2026-Q1`、 + `2025-W03` → `2026-W03`。它刻意是 `shiftRange` 那套年运算的精确逆运算(含 + `setUTCFullYear` 的溢出行为),窗口与桶键因此不可能对「一年」有两种理解。 + - `previousPeriod` —— 任意天数窗口没有日历对应物,所以按**桶序(bucket ordinal)**对齐: + 上一窗口的第 n 个桶对上本窗口的第 n 个桶,n 各自从自己窗口的起点数起。序号由**日历**算出 + 而不是数组下标,所以本期网格里某个桶没有数据(存在空档)不会让其后每个桶都错位一格。 + + **响应形状不变** —— 仍然是 `__compare` 列,行仍然是网格维度元组,所以消费端 + (objectui#3337 正在收敛的那条契约)不受影响。 + + 不确定时一律**保持原样**(即改动前的行为),而不是猜:空桶(两条聚合路径上键都是 `null`, + 两趟本来就互相合并)、未分桶的日期维度(分组的是原始时间戳,不是桶键)、以及平移回来落在 + 当期窗口之外的桶(两个等长的天数窗口可以切出不同的桶数)。 + + 范围严格限定在坏掉的那个形状:锚点必须是**网格维度**(仅作窗口的锚点两趟都不是列,#5688 + 之后本来就对齐)且必须**被分桶**。两趟通过同一个 `granularityOf` 读取桶大小,所以这里重述 + 的桶大小按构造就是查询分组用的桶大小。 + +- 49f208b: fix(analytics): an `undefined` comparand in an analytics `where` is refused (400 `INVALID_FILTER`), not read seven different ways + + **Observable behaviour change.** A `where` key whose value is `undefined` used to + compile — in seven different ways, depending on where it sat. It is now refused + with `INVALID_FILTER` / 400, the envelope every other refusal at this door + already carries. + + The three that mattered WIDENED the query, which is the failure mode + `filter-normalizer.ts` forbids in its own body ("NEVER drop: a missing predicate + does not narrow the query, it WIDENS it"), while its entry line did exactly that: + + | `where` | used to normalize to | reading | + | ------------------------------ | -------------------------------- | -------------------------------------------------------- | + | `{d: undefined}` | `null` | the WHOLE filter dropped — the query ran **unfiltered** | + | `{stage: 'won', d: undefined}` | `stage equals 'won'` | the `d` conjunct vanished in silence | + | `{$not: {d: undefined}}` | `NOT (d set)` | `d IS NULL` — a predicate the author never wrote | + | `{d: {$eq: undefined}}` | `d equals [null]` | a value comparison, **not** `$eq: null`'s null predicate | + | `{d: {$gt: undefined}}` | `d gt [null]` | ditto | + | `{d: {$in: [undefined]}}` | `d in [null]` | ditto | + | `{d: {$ne: undefined}}` | `d notSet OR d notEquals [null]` | ditto | + + The direction is silently **wrong results** — an analytics figure, a report + total, an aggregate, wrong with nothing to read — **not** a permission bypass: + read scope is compiled by a different door (`read-scope-sql.ts`) and never passed + through here, so a caller still saw only rows it was entitled to, just more of + them than it asked for. + + **What to change if this refuses your filter.** `undefined` cannot cross JSON, so + neither REST door can carry it — this only reaches in-process callers of + `AnalyticsService.query({ where })` that spread a possibly-absent value into the + filter object (`{ owner_id: ctx.user?.id }`). Two repairs, both stated by the + error message: + + - meant the null predicate → write `{ field: null }` or `{ field: { $null: true } }`; + - the value is genuinely absent → **omit the key**, which is the same "no + constraint" without the ambiguity. + + Inside stored metadata, the platform's own answer to "scope this to the current + user" is unaffected and was already fail-closed: a `{current_user_id}` + placeholder resolves through `resolveFilterTokens`, which raises + `FILTER_TOKEN_UNRESOLVED` / 400 rather than emitting `undefined`. + + ⛔ **`null` does not move.** `{d: null}`, `{$eq: null}`, `{$ne: null}`, + `{$null: …}`, `{$exists: …}` and `$contains: null` keep their exact lowering — + `null` is a declared comparand and is the null predicate. `$null` / `$exists` + carry a declared boolean flag rather than a comparand and are likewise untouched. + +- 2604d34: fix(analytics): a field constraint mixing `$` operators with non-`$` sibling keys is refused (400 `INVALID_FILTER`), not silently narrowed to its operators + + **Observable behaviour change.** A `where` field wrapper that carries `$`-operator + keys and non-`$` keys at once used to compile its operators and silently DROP + every non-`$` sibling. It is now refused with `INVALID_FILTER` / 400, the + envelope every other refusal at this door already carries. Ruled Option A + (refuse) on #6444, 2026-08-08; Option B (flattening the siblings as nested + paths) was rejected because it would compile the likely-real cause — a dropped + `$` — into a predicate on a non-existent member such as `amount.gte`. + + | `where` | used to normalize to | reading | + | ----------------------------------------- | ------------------------- | --------------------------------------------------- | + | `{d: {$eq: 1, nested: 'x'}}` | `d equals [1]` | the `nested` conjunct vanished in silence | + | `{amount: {gte: 10, $lte: 20}}` | `amount lte 20` | the missing-`$` typo: the lower bound silently gone | + | `{$not: {d: {$null: true, nested: 'x'}}}` | `NOT(d set AND d notSet)` | a contradiction that negates to TRUE — every row | + + Every row WIDENED the query — a dropped conjunct returns rows the author + excluded, with nothing to read (the #3650 family this module refuses everywhere + else). Unlike #6386's `undefined` comparand, this shape survives JSON, so it can + sit in stored dashboard / report / dataset metadata as well as in-process + callers of `AnalyticsService.query({ where })`. + + **What to change if this refuses your filter.** The message names the offending + key(s) and both repairs, because the shape has two readings this door cannot + tell apart: + + - an operator missing its `$` was meant → spell it with the prefix + (`gte` → `$gte`: `{ "amount": { "$gte": 10, "$lte": 20 } }`); + - a nested-relation member was meant → give it a wrapper of its own with no `$` + siblings (`{ "d": { "nested": "x" } }` compiles to the member `d.nested`) and + AND it with the operator constraint explicitly via `$and`. + + ⛔ **The two pure shapes do not move.** A wrapper that is all `$`-operators + compiles exactly as before (`{amount: {$gte: 10, $lte: 20}}` stays the AND of + its bounds), and a wrapper that is all non-`$` keys keeps flattening to the + dotted member (`{d: {nested: 'x'}}` → `d.nested`). `$null` / `$exists` flag + semantics, the `null` comparand rulings (#5332 / #5526) and the sibling door + `read-scope-sql.ts` — which has always failed closed on this shape — are + untouched. + +- 3cc8676: fix(analytics): read scope 里非布尔的 `$null` / `$exists` 比较数改为拒收,不再按真值性编成相反的谓词 (#6387) + + **⚠️ 行为变更。** `compileScopedFilterToSql` 遇到 `$null` / `$exists` 上的非布尔比较数,从「按 JS 真值性归入两个声明答案之一、静默编出合法 SQL」改为 `READ_SCOPE_COMPILE_FAILED` / **500** 拒收。今天靠这个静默翻转在跑的 read scope,从此会响亮地失败。 + + ## 实测到的毛病 + + 发射器读的是 `val ? … : …` —— **真值性**,不是 `@objectstack/spec` `FieldOperatorsSchema` 声明的 `z.boolean()`。在 `5faa23ca3` 上直接调 `compileScopedFilterToSql`,alias `t`: + + | read scope | 编译结果 | | + | ------------------------------------ | ---------------------------- | ------------------------- | + | `{ owner_id: { $null: "false" } }` | `"t"."owner_id" IS NULL` | ⛔ 与作者写的意思**相反** | + | `{ owner_id: { $null: "true" } }` | `"t"."owner_id" IS NULL` | | + | `{ owner_id: { $null: 0 } }` | `"t"."owner_id" IS NOT NULL` | | + | `{ owner_id: { $null: null } }` | `"t"."owner_id" IS NOT NULL` | | + | `{ owner_id: { $null: undefined } }` | `"t"."owner_id" IS NOT NULL` | | + | `{ owner_id: { $exists: "false" } }` | `"t"."owner_id" IS NOT NULL` | ⛔ 与作者写的意思**相反** | + | `{ owner_id: { $exists: 0 } }` | `"t"."owner_id" IS NULL` | | + | `{ owner_id: { $exists: "no" } }` | `"t"."owner_id" IS NOT NULL` | | + + 两行 ⛔ 是要害:字符串 `"false"` 是**真值**,于是它落在它被写下来所要表达的 `false` 的**对面** —— `{ $exists: "false" }` 写来表示「没有 owner 的行」,编出来是「**有** owner 的行」。这与 #6125 那一格方向相反:那边是 fail-**closed**(匹配零行、只是安静),这边是**加宽** —— admit 了策略要排除的行,出现在一个自述「A read-scope predicate must never be silently dropped、fail-closed」的模块里。 + + ## 修法 + + 按 #5347(`$null`)/ #5369(`$exists`)在 `driver-sql` 面确立的先例,理由逐字适用:非布尔比较数**按声明拒收**,不做强转。闸落在 `compileField`,紧挨 #6125 的 `undefined` 闸 —— 两道闸的作用域互不相交(那一道按名字跳过这两个算子),所以谁也盖不住谁的措辞。 + + 两个算子**共用一条措辞**(#5240「一个条件一种措辞」),只有算子名与 `path` 不同:`driver-sql` 给孪生实现两条措辞,是因为各自要指名**自己**发射器默认倒向哪边;本模块只有一条规则(真值性)同时管着两个算子,两者失败方式完全一样,所以一条措辞才是诚实的写法。测试里有一条断言把「只有这两处不同」钉死。 + + 信封沿用本模块自述的那一个(`READ_SCOPE_COMPILE_FAILED` / 500),不是 #5347 的 `INVALID_FILTER` / 400:read scope 由平台自己从 CEL 与库存 metadata 编出来,报 400 等于让调用方去修一个他既没写、也改不动的东西。继承的是**处置**(拒收),不是信封。 + + 极性表**同 PR 一起改**:`nullValueSatisfiesOperator` 的 `$null` / `$exists` 两臂从真值性(`Boolean(value)` / `!value`)改为恒等(`value === true` / `value === false`)。每张极性表钉的是它**自己**发射器的拼写(#5146 / #5298),只改发射器不改表,不变量会安静地断在定义处。这条差异消失后,本编译器与 `driver-sql` 的同名表第一次逐臂一致。 + + ## ⚠️ 触达性:实测结论是**库存 metadata 走不通** + + 定级依据是测量,不是立单时的措辞。`{ $null: <非布尔> }` **无法**从库存 metadata 走到本编译器,三道闸各自独立关死:`RowLevelSecurityPolicySchema` 把 `using` / `check` 声明为 `z.string()`(CEL 谓词,不是 FilterCondition),存对象直接被拒;CEL 下降只在两处发射 `$null` 且比较数是**硬编码布尔**(`== null` → `{$null: true}`,`!= null` → `{$null: false}`),`$exists` 一次都不发射;绕开 schema 塞裸对象会在 `sqlPredicateToCel` 里抛错,被 `getReadFilter` 的 catch 变成 `RLS_DENY_FILTER`。其余 read scope 生产者(Layer 0 租户过滤、`plugin-sharing` 的 `buildReadFilter`、controlled-by-parent、deny 哨兵)压根不含这两个算子。 + + **仍然开着的那条**:`getReadScope` 是 `AnalyticsPluginOptions` 上有文档的公开扩展点,宿主自带的 read scope(来自 JSON 配置或没走类型检查的 JS)与本编译器之间没有任何闸 —— 本单也确认了 `plugin-security` 全路径无 `FilterConditionSchema` / `safeParse`。所以:今天不从库存 metadata 触达,但没有任何结构性的东西挡住下一个生产者。在编译器处拒收,才让「声明为布尔」等于「强制为布尔」,与谁写这条 scope 无关。 + + ## ⛔ 一字未动的邻居 + + - **合法布尔**:`$null: true/false`、`$exists: true/false` 的 SQL 逐字节不变(`IS NULL` 下降正是 RLS 用来圈无主行的写法,也是 CEL 唯一能产出的四种形状)。有自己的对照组回归 pin。 + - **比较数位置上的 `null`**:`{ d: null }`、`{ $eq: null }`、`{ $ne: null }`、`$in: [null]` 等 #6125 的 `NULL_CONTROL` 全部保持绿。 + - `driver-sql` / `driver-turso`(#5347 / #5369 已落地)、`packages/spec`(声明已是 `z.boolean()`)、以及本包的 `where` 门 `strategies/filter-normalizer.ts` 均未触碰。 + +- e15bf7e: fix(analytics): read scope 里的 `undefined` 比较数改为拒收,不再编成绑了 `undefined` 的合法 SQL (#6125) + + **⚠️ 行为变更。** `compileScopedFilterToSql` 遇到比较数位置上的 `undefined`,从「编出合法 SQL、绑一个 `undefined`、匹配零行、零日志」改为 `READ_SCOPE_COMPILE_FAILED` / **500** 拒收。 + + ## 实测到的毛病 + + #6050 于 2026-08-07 裁定(B 案):比较数位置的 `undefined` 一律拒收,并落在了**已证实可触达**的 `driver-sql` / `driver-turso` 两面。#6125 在同一轮把仓内其余求值面逐格实测,同一个形状拿到五种读法;本条改的是其中一格 —— `service-analytics` 的 `read-scope-sql.ts`。在 `d8e8d9cbc` 上把本次拒收关掉复测,alias `t`、字段 `d`,四格与 #6125 正文表一致: + + | read scope | 编译结果 | 绑定表 | + | ----------------------------- | --------------------------------------------- | ------------- | + | `{ d: undefined }` | `"t"."d" = ?` | `[undefined]` | + | `{ d: { $gt: undefined } }` | `"t"."d" > ?` | `[undefined]` | + | `{ d: { $in: [undefined] } }` | `"t"."d" IN (?)` | `[undefined]` | + | `{ $not: { d: undefined } }` | `NOT (("t"."d" IS NOT NULL AND "t"."d" = ?))` | `[undefined]` | + + 绑定表里是 JS 的 `undefined` 本身,不是 `null`:`applyReadScope`(`native-sql-strategy.ts`)在把 `?` 改写成 `$N` 时原样 `push(scopeParams[i])`。所以 NULL 是**驱动**对一个 JS `undefined` 的读法 —— 同一格在不肯猜的驱动上则是一句裸 `Undefined binding(s)` 崩溃。一次绑定、两种败法,取决于数据源恰好挂的是哪个驱动,这正是它该在编译器处拒收、而不是在某一个消费者处修补的理由。 + + 方向与 #6050 不同,如实记:那边是**越权**(`{ owner_id: ctx.user?.id }` 在 Turso remote 上编成 `IS NULL`,匹配全环境行);这边是 fail-**closed** —— 匹配零行,永远不会多给行。所以它不是潜伏的权限绕过,#6125 也没有按那个级别定级。之所以照样拒收:一个「答了没人问的问题、且一条日志都不报」的 read scope,与一个真的生效了的 read scope 在外部完全无法区分。本次改动的价值就是把沉默变成响亮。 + + ## 修法 + + 一道闸落在 `compileField` 的开头 —— 在 `quoteIdent` 之后(不安全标识符是注入向量,保留它自己的措辞与优先级),在任何 `bind()` 之前。 + + 拒收的**位置**逐个清点,因为「比较数」是位置而不是类型:直接比较数(`{ d: undefined }`)、单值算子的比较数(`$eq`/`$ne`/`$gt`/`$gte`/`$lt`/`$lte` 与 LIKE 族)、列表算子数组的**成员**(`$in`/`$nin`/`$between`)。四格共用**一条**措辞,只有 `path` 不同(#5240「一个条件,一种措辞」)。 + + 信封沿用本模块自述的那一个(`READ_SCOPE_COMPILE_FAILED` / 500),不是 #6050 的 `INVALID_FILTER` / 400:read scope 的 filter 由平台自己从 CEL 与库存 metadata 编译而来,不是调用方输入 —— 报 400 等于让调用方去修一个他既没写、也改不动的东西。消息里指名要修的是**生产者**(管理员写的共享规则 / 权限集、它的 CEL 下降、或进程内拼这条 FilterCondition 的代码),并按 #5367 只进日志、不进响应体。 + + 三个位置**故意不扫**,各自因为本模块已经用更贴切的诊断拒了它:`$null` / `$exists`(比较数是声明的布尔量,不是比较数位置)、直接位置上的裸数组(`compileField` 整体拒「用 `{ $in: [...] }`」)、以及约束对象里的非 `$` 键(那是嵌套关系,改写成 `null` 一样编不过 —— 这一条是与 `driver-sql` 孪生实现的唯一有意分歧,来自本模块拒收嵌套关系,而不是对 #6050 的另一种读法)。 + + ## ⛔ `null` 一字未动 + + `{ d: null }` / `{ $eq: null }` → `IS NULL`;`{ $ne: null }` → `IS NOT NULL`;`$null` / `$exists`、`$in: [null]`、`$nin: [null]`、`$between: [null, 5]`、`$contains: null`(`%null%`,#5526)、以及 `$not` 下的各式 —— SQL 与绑定表逐字节不变。这是本次改动唯一可能造成伤害的方向(模块里每张极性表都只用一个 `===` 把 `null` 与 `undefined` 分开),所以它有自己的对照组回归 pin。 + + ## 刻意不动的邻居 + + - ⛔ `@objectstack/formula` 把同一个 `undefined` 读作「这个键在记录里不存在」—— 那是**第三种语义**,不是第三个 bug 拼写,也正是 #5299 在争的问题。在这里顺手改掉等于替 #5299 拍板。 + - ⛔ `driver-memory` / `driver-mongodb` 维持 #5499 投入冻结,只 pin 不改。后果是本编译器与 `driver-memory` 在这一格上从此不一致 —— 这是裁决接受的代价,解冻时一并还,账记在 #6125。 + - ⛔ `driver-sql` / `driver-turso` 已由 #6050 落地,未触碰。 + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/services/service-analytics/package.json b/packages/services/service-analytics/package.json index e34cfbb33d..1dd11be267 100644 --- a/packages/services/service-analytics/package.json +++ b/packages/services/service-analytics/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-analytics", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 52cc8961f0..c745379fb6 100644 --- a/packages/services/service-automation/CHANGELOG.md +++ b/packages/services/service-automation/CHANGELOG.md @@ -1,5 +1,282 @@ # @objectstack/service-automation +## 17.0.0-rc.6 + +### Patch Changes + +- 6517448: fix(service-automation): 降级版挂起态读取器的「存储读不到」告警不再把驱动错误拼进 message,改走 meta (#6230) + + `engine.ts` 的 `loadSuspendedRun` —— `loadSuspendedRunStrict` 的**降级版**读取器 —— + 在 catch 里把**我们不控制文本**的数据源驱动失败原因直接插进了 `logger.warn` 的 message。 + `ObjectLogger.write()` 一次调用只加一个「时间戳 + 级别」记录头,message 里的换行会把 + **一条**记录变成多个物理行,后面几行既无级别也无时间戳。 + + 这条比 #5912(PR #6228)刚治完的那条**多一层危害**:`ObjectLogger` 把 `warn` 路由到 + **stdout**,而 `serve` 的 boot-quiet 窗口只包了 `process.stdout.write`,其 + `BootLogCapture.offer()` 仅在该物理行带级别头时才保留 —— 所以无头续行是被**直接丢弃**, + 不只是被误读。而它在 boot 期真实可达:`plugin.ts` 的 `start()` → `rearmSuspendedWaitTimers` + → 对 overdue 运行 `engine.resume()` → `resume()` 的授权 gate 走的正是这个降级版读取器。 + + 实测:一个三行的 better-sqlite3 驱动错误把这条告警切成 **3 个物理行**,过 boot 缓冲的 + 过滤后**只剩 1 行**留下 —— 而留下的那一行恰恰不含任何驱动事实。 + + 改法与 #5048 / #5575 / #5636 / #5661 / #5737 / #5912 完全同一套,零新词汇:**message + 单行自足**,外来 cause 交给 `Logger` 契约(`packages/spec/src/contracts/logger.ts`) + `warn(message, meta?)` 的**第二**参 —— 注意与 `error(message, error?, meta?)` 的第三参 + 不同,`warn` 没有 `Error` 槽。 + + 对运维可见的变化(日志形状,非行为): + + - 这条记录恒为**一个**物理行,不论日志格式,boot-quiet 窗口内不再丢字节; + - 原因文本从 `msg` 末尾的 `: <驱动文本>` 移到记录的 `error` 字段(`meta`),多行驱动 + 错误由 `JSON.stringify` 转义换行后完整保留 —— 一个字节都不丢; + - message 补上了这条降级的**后果**:读失败被翻译成 `null`,调用方(resume gate、screen + 取数)看到的与「本来就没有这个挂起运行」完全一样,而运行本身未被触碰、仍停在原处; + 原文本只说了「读失败」,没说读失败被翻译成了什么。 + + 刻意**不变**的一处,已钉上回归测试:**级别仍是 `warn`**。这是一个刻意的**功能性**降级 + 读取器(注释写明它服务于只需要 best-effort 答案的顺带读取方),真正需要区分「存储挂了」 + 与「运行没了」的 `resumeInternal` 用的是严格版 —— 按 #4632 的判据这不是耐久性降级, + 上调到 `error` 才是该规则的镜像误用(整个故障期间每次 gate 查询都报警)。 + + 按记录末尾驱动文本字面量 grep 这条记录的日志查询,需要改成读记录的 `error` 字段。 + +- 24122a9: fix(service-automation): the 13 residual `engine.ts` seams stop splicing uncontrolled thrown text into log messages, plus the one self-authored multi-line message; run-history persist failure is re-graded `error` (#6499) + + #6299 / PR #6498 fixed three `engine.ts` seams and closed with "this file is now + clean"; #6499 is the corrective record: 13 more logger calls in the same file + still interpolated a thrown value's `.message` — a datasource driver's, a + plugin's (trigger / node-executor), or, second-hand via the + `AutomationResult.error` envelope, a failing node's — into the log MESSAGE. + `ObjectLogger.write()` adds one ` ` head per call, so a cause + carrying newlines turned ONE record into several physical lines of which only + the first is greppable, and `serve`'s boot-quiet window drops the headless + continuations outright on the stdout (warn) path. All 13 now log a single-line + message stating the site's own consequence and hand the cause to the logger's + structured slot (`describeThrownForLog`). + + A 14th site with the opposite cause is fixed alongside, argued on its own + terms: `validateFlowExpressions`' advisory schema pass authored a literal + `\n source: …` continuation into a message we control, with the flow + author's (newline-tolerant CEL) expression as the second line. The message now + stays one line; the expression source rides the structured slot (`source`). + + The level was judged per seam (#4632), not batch-copied: + + - **`recordLog`'s fire-and-forget `store.recordTerminal` → RAISED to `error`.** + The write half of the run-history claim: a TERMINAL run's history row failed + to land while the run completed and every caller reads healthy — nothing + retries it, nothing upstream is told. After the next restart the run is + invisible to the Runs surfaces, `inspectStrandedRequests` (#3456) reads + "no suspension + no terminal row" as a STRANDED approval, and + `releasePendingForTerminalRuns` (#4469) reads "no terminal row" as + still-alive, so a finished run's leftover pending approvals are never + auto-released. + - **`persistSuspendedRun` stays `error`** (#4460's raise; #4420 is this exact + seam's accident) — no re-grade, message and slot fixed only. + - **Everything else stays `warn`** (functional): `listRuns` / `getRun` + (observability reads degrading to ring buffer / null — each record now says + the caller cannot tell the degraded answer from a real one), the four + plugin-supplied seams (`releaseSuspension`, `unregisterTrigger`, + `activateFlowTrigger`, `deactivateFlowTrigger`), the grants resolver, lookup + expansion, the screen `visibleWhen` probe, and both `bubbleToParent` + branches. Nothing these degrade claims to be persisted. + + Operator-visible: one record moves from stdout/`WARN` to stderr/`ERROR` + (run-history persist failure), and the reworded messages keep their original + lead phrases (`run-history read failed`, `durable run lookup failed`, + `Failed to bind flow`, `could not resolve grants`, …) so existing greps still + match; alert rules keyed on the trailing `: ` splice need the + structured `error` / `source` / `visibleWhen` fields instead. + +- b0d54bf: fix(service-automation): the last three `engine.ts` seams stop splicing a driver's failure into the log message, and two of them are re-graded `error` (#6299) + + All three catches sit around the `SuspendedRunStore` driver and rendered their + failure by interpolating the thrown value's `.message` into the log MESSAGE. + `ObjectLogger.write()` adds exactly one ` ` head per call, so a + driver error carrying newlines turned ONE record into several physical lines of + which only the first was greppable — and on the `warn` path, inside `serve`'s + boot-quiet window, `BootLogCapture.offer()` keeps only lines with a level head, + so the continuation lines were dropped outright. Measured on the restored + concatenation: a three-line driver error became 3 physical lines and the boot + filter retained 1, and that one carried no driver fact. The cause now goes to + the logger's structured slot (`describeThrownForLog`), so the record stays on + one physical line in every format. This closes the family of #5048 / #5575 / + #5636 / #5661 / #5737 / #5912 / #6230 for this file. + + The level was judged per seam (#4632), not batch-copied from #6230: + + - **`forgetSuspendedRun` → raised to `error`.** The hot cache is dropped before + the store delete and this is the single choke point every consumption of a + suspension passes through, so a failed `delete` leaves the suspension gone + in-process and the durable row alive. Callers still report success, and the + surviving row is re-listed and re-resumed after the next restart, running a + continuation that already ran. + - **`cancelRun` → raised to `error`.** An unreadable store makes the failed read + read as "no such suspended run", so the method returns `false` — which its + contract calls idempotent success — and the cancellation is silently skipped + while the call reads clean. The run stays parked and durably resumable. + - **`listSuspendedRunsDurable` → stays `warn`.** Nothing claimed-persisted + failed to land: the rows are intact and still resumable by id. The listing + degrades to the in-memory cache alone, so the message now says out loud that + the result is short and that the caller cannot tell. + + Operator-visible: two records move from stdout to stderr and from `WARN` to + `ERROR`, and all three messages are reworded to state their consequence. Log + filters or alert rules keyed on the old `warn`-level text for a failed + suspended-run delete or cancel need updating. + +- 4d552af: feat(spec)!: `FlowNodeSchema` parses its own ADR-0031 regions — the post-parse pass retires (#4415) + + `FlowSchema.parse` normalized a flow's own `nodes[]` / `edges[]` but could not reach a + **region**, because a region lives inside `FlowNodeSchema.config` — a deliberately open + `z.record` (ADR-0018). #4381 closed the resulting gap with a **post-parse pass**, + `normalizeControlFlowRegions`, that every caller had to remember to run: + + ```ts + const flowShell = FlowSchema.parse(converted); + validateControlFlow(flowShell); + const parsed = normalizeControlFlowRegions(flowShell); // ← had to remember + ``` + + That is an unwritten rule on top of a parse, and it is exactly the condition the #4347 + family of defects grows in: a new consumer — a Studio publish path, an MCP tool, a bulk + validation script — takes a `FlowParsed` and uses it, holding a **half-parsed flow that + looks finished**. Nested edge predicates were still bare strings, nested nodes had not been + through `.strict()`, and nothing said so. + + Now the schema does it. `FlowNodeSchema` carries a `.transform()` that parses each declared + region slot — `loop.config.body`, `parallel.config.branches[]`, `try_catch.config.try` / + `.catch` — through the schema that slot's value _is_. Nesting needs no manual recursion: a + region's `nodes` are `z.array(FlowNodeSchema)`, so Zod re-enters the transform on the way + down. **"Parsed" now means parsed at every depth** (Prime Directive #1), from any entry + point — including `FlowNodeSchema.parse(node)` on a single node, which the old whole-flow + pass could not serve at all. + + ## Migration + + **`normalizeControlFlowRegions` is removed from `@objectstack/spec/automation`.** Delete the + call; the parse above it already did the work: + + ```diff + const parsed = FlowSchema.parse(converted); + validateControlFlow(parsed); + - const normalized = normalizeControlFlowRegions(parsed); + ``` + + Its replacement, `parseFlowNodeRegions(node)`, is exported for the same purpose one node at + a time, but you should not normally need it — it is the transform's own body. + + **`FlowNodeSchema` is now a `ZodPipe`, not a `ZodObject`,** so it no longer has `.shape` / + `.extend()` / `.pick()`. `z.infer` / `z.input` / `.parse` / `.safeParse` and + `z.toJSONSchema` are unaffected, and the authorable key set is byte-identical (verified by + `check:authorable-surface`). If you were reaching for the object half, read it from the + pipe's input side — `FlowNodeSchema.def.in` — which is also what the repo's own generators + do (`pipeAuthorableSide` in `scripts/lib/zod-graph.ts`). + + One visible consequence in the generated reference: `content/docs/references/automation/flow.mdx` + now renders FlowNode's **input** shape, so keys carrying a `.default()` (`boundaryConfig.interrupting`, + `inputSchema[].required`) show as optional. That is what an author actually writes, which is + what an authoring reference should say. + +- 72847c5: fix(service-automation): resume 时「存储不可达」的日志不再把驱动错误拼进 message,改走 meta (#5912) + + `engine.ts` 的 `resumeInternal` 在读挂起态存储失败的那一支,把**我们不控制文本**的 + 数据源驱动失败原因直接插进了 `logger.error` 的 message。`ObjectLogger.write()` 一次 + 调用只加一个「时间戳 + 级别」记录头,所以 message 里的换行会把**一条**记录变成多个 + 物理行,后面几行既无级别也无时间戳。在 `pretty` / `text` 格式(`os dev` / `os serve` + 的默认)下,文件 sink 会把它们当成独立记录存,而 `grep ERROR` 只捞得到不含任何事实 + 的那一行 —— 恰恰是运维正在找的那条。实测:一个三行的 better-sqlite3 驱动错误把这条 + 告警切成 **3 个物理行**,只有第 1 行带 `ERROR` 头。 + + 改法与 #5048 / #5575 / #5636 / #5661 / #5737 完全同一套,零新词汇:**message 单行 + 自足**,外来 cause 交给 `Logger` 契约(`packages/spec/src/contracts/logger.ts`) + `error(message, error?, meta?)` 的**第三**参(第二参留空,否则每条记录都会带上整个栈)。 + + 这是这条 resume 路径上最后一处。#5737(PR #5911)修完 `wait` 节点五处之后,同一次 + 「resume 时存储不可达」会产生两条记录:wait 节点那条已是干净单行,engine 这条仍被 + 切碎;本次之后两条都干净。 + + 对运维可见的变化(日志形状,非行为): + + - 这条记录恒为**一个**物理行,不论日志格式; + - 原因文本从 `msg` 末尾的 `: <驱动文本>` 移到记录的 `error` 字段(`meta`),多行驱动 + 错误由 `JSON.stringify` 转义换行后完整保留 —— 一个字节都不丢; + - message 补齐了 #4632 要求的后果与修法(挂起态**未被消费**、运行仍停在原处、存储 + 恢复后可原样重试),并指明 cause 在本记录的 meta 里。 + + 刻意**不变**的两处,已各自钉上回归测试: + + - **返回值信封** `AutomationResult.error`(`STORE_UNAVAILABLE`)仍逐字拼接驱动文本。 + 它是给调用方读的结构化返回值,经 REST 出去是 JSON 字符串字段、不按行切分;#5636 + 对 `degradedReason` 是同源取舍,且 PR #5911 已让 wait 节点侧把它整体放进 meta 保留。 + - **级别仍是 `error`**。运行在盘上而 resume 没落地,正是 #4632 定义的耐久性降级, + `pnpm check:durability-log-level` 照旧覆盖。 + + 按记录末尾驱动文本字面量 grep 这条记录的日志查询,需要改成读记录的 `error` 字段。 + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [07c68b0] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [e9b5265] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [6965160] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/formula@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Minor Changes diff --git a/packages/services/service-automation/package.json b/packages/services/service-automation/package.json index 483dcd13b2..8910f55d9f 100644 --- a/packages/services/service-automation/package.json +++ b/packages/services/service-automation/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-automation", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 3c5f804418..03daa19272 100644 --- a/packages/services/service-cache/CHANGELOG.md +++ b/packages/services/service-cache/CHANGELOG.md @@ -1,5 +1,68 @@ # @objectstack/service-cache +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/observability@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/services/service-cache/package.json b/packages/services/service-cache/package.json index 8435eb3c69..938d17e23b 100644 --- a/packages/services/service-cache/package.json +++ b/packages/services/service-cache/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cache", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 3b92c8b116..4a0aa580f7 100644 --- a/packages/services/service-cluster-redis/CHANGELOG.md +++ b/packages/services/service-cluster-redis/CHANGELOG.md @@ -1,5 +1,67 @@ # @objectstack/service-cluster-redis +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/service-cluster@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/services/service-cluster-redis/package.json b/packages/services/service-cluster-redis/package.json index e8294ea202..a2a67874ec 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": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 62b642cb83..9a27bd8745 100644 --- a/packages/services/service-cluster/CHANGELOG.md +++ b/packages/services/service-cluster/CHANGELOG.md @@ -1,5 +1,67 @@ # @objectstack/service-cluster +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/services/service-cluster/package.json b/packages/services/service-cluster/package.json index 13627039a2..b6bb7afc34 100644 --- a/packages/services/service-cluster/package.json +++ b/packages/services/service-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cluster", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 7cd95d8fa4..f509bb0a60 100644 --- a/packages/services/service-datasource/CHANGELOG.md +++ b/packages/services/service-datasource/CHANGELOG.md @@ -1,5 +1,163 @@ # @objectstack/service-external-datasource +## 17.0.0-rc.6 + +### Patch Changes + +- 01faeb1: fix(service-datasource): a `pool` block on a `memory` datasource is rejected, not dropped in silence (#5931) + + #5714 made a `pool` block the driver cannot honour a loud authoring error, but + its ruling was scoped to the two sqlite arms — `memory` kept dropping it. The + `memory` arm hands `InMemoryDriver` nothing but `buildMemoryConfig(spec)`, which + reads `spec.config` and never `spec.pool`, so a sized pool reached nothing and + said nothing. Measured through the real factory: + + ```text + memory + pool{min:3,max:9} driver config {"persistence":false} pool undefined + sqlite + pool{min:3,max:9} rejected (since #5714) + postgres + pool{min:3,max:9} knex config.pool {"min":3,"max":9} live {min:3,max:9} + ``` + + `memory` now joins `POOL_UNSUPPORTED_DRIVER_IDS`, so the same three doors that + already rejected sqlite reject it: the Setup wizard's create/update, the + boot-time auto-connect pre-pass, and the driver factory itself. + + **Behaviour change.** A datasource declaring `driver: 'memory'` (or `inmemory` / + `in-memory` / `mingo`) together with a non-empty `pool` block used to load and + run; it now throws at whichever door it arrives through. The fix is the one edit + the message names — delete the `pool` block. Nothing is lost by deleting it: it + configured nothing before. An absent or empty `pool` is unchanged, and every + `memory` datasource without one builds exactly as it did. No declaration in this + repo, the example apps included, carried the combination. + + **Its own explanation, not SQLite's.** SQLite is rejected because a second + connection to `:memory:` opens a separate, empty database, so sizing the pool + would split one datasource across several stores. That reasoning is false for + `memory`: there is no connection at all — the store is a plain data structure in + this process — so the message says that instead. Telling an author their driver + picked a connection strategy for them would send them looking for a knob that + does not exist. Reasons are now keyed by driver id, which makes an arm joining + the set without writing one a type error. + + Maintainer ruling 2026-08-07, which also set the default for the next sister + arm: when a declared key is silently dropped on one arm and an earlier ruling + already made it a loud authoring error on a sibling, the new arm joins the + existing rejection set rather than queueing for a ruling of its own — unless the + original rationale was measured to be arm-specific. + + No API surface is added — `POOL_UNSUPPORTED_DRIVER_IDS`, + `driverReadsDeclaredPool`, `unsupportedPoolIssue`, `unsupportedPoolMessage` and + `assertDatasourcePoolSupported` keep the signatures #5714 published, and the + sqlite arms' rejection text is byte-for-byte unchanged. + +- d92ed03: fix(service-datasource): 未构建的工作区不再被当成「配置写错了」(#5794) + + datasource 的 fail-fast 报错原本只有一句收尾建议,不分成因: + + ``` + ✗ datasource 'default': connect failed — Cannot find module + '…/@objectstack/driver-sql/dist/index.mjs' imported from … + Fix the datasource configuration, or set OS_ALLOW_DRIVER_CONNECT_FAILURE=1 + to boot anyway and serve errors until it is reachable. + ``` + + 对「数据库真连不上」——错的 DSN、轮换掉的密码、断掉的网络——这句话是对的。 + 但对**驱动包没构建**这一个成因,两半都是有害建议: + + - **「Fix the datasource configuration」** 把读者支去改一份本来就正确的配置。 + 在那里写什么都变不出一个 `dist/` 目录。 + - **「set OS_ALLOW_DRIVER_CONNECT_FAILURE=1 to boot anyway」** 比没用更糟: + 它不是绕过问题,而是**藏起**问题。半个工作区会宣称自己启动成功,然后对每个 + 请求回 `ERR_DATASOURCE_UNAVAILABLE`——比诚实地拒绝启动难查得多。那个开关是 + 为「数据库暂时不可达」准备的(一个关于世界的事实,可能自己好起来);缺构建产物 + 是关于这份 checkout 的事实,不该有任何环境变量能启动越过它。 + + 而唯一有效的修法(`pnpm build`)一个字都没提。 + + 现在 connect 失败会按**成因**选收尾句。底层错误是模块解析失败时(ESM `import()` + 报 `err.code === 'ERR_MODULE_NOT_FOUND'`,CJS `require()` 报 `MODULE_NOT_FOUND`; + `code` 被 re-throw 丢掉时退回 `Cannot find module` / `Cannot find package` 文本), + 消息改成: + + ``` + The driver package could not be LOADED at all — it is not installed, or its build + output is missing. That is a build precondition, not a datasource fault: the + configuration is fine, and no boot-time override can make a driver that does not + exist answer a query. Run `pnpm install && pnpm build`, then start again. + ``` + + 一个正确修法,只说一次,**不提**那个逃生开关——连「别用它」都不提:一个已经卡住的 + 读者会去找最短的那行看起来能让他继续的话。这与 `datasource-pool-support.ts` + (#5714 / #5931)和 `check:dev-prereqs`(#5795)是同一条消息纪律。 + + 判据复用 `@objectstack/types` 的 `isModuleNotFoundError`(framework#3265 起的唯一 + 所有者),不另起一份;它先看结构化的 `err.code`、再退回文本,而这个结构化信号原本 + 在 `handleFailure` 只收 `reason: string` 时被丢弃了,所以抛出值本身现在也一并传入。 + + **纯诊断分类,零行为变化。** fail-fast 的判定、触发时机、抛出的错误类型、保留的 + 连接状态,以及设了 `OS_ALLOW_DRIVER_CONNECT_FAILURE` 时的降级启动路径全部不变; + 其它成因(真连接失败、驱动不受支持、凭据解析不出)的消息逐字未动。 + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/services/service-datasource/package.json b/packages/services/service-datasource/package.json index 424aa109aa..61f7ee39e9 100644 --- a/packages/services/service-datasource/package.json +++ b/packages/services/service-datasource/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-datasource", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 10cbb9442e..dcb495c5e0 100644 --- a/packages/services/service-i18n/CHANGELOG.md +++ b/packages/services/service-i18n/CHANGELOG.md @@ -1,5 +1,115 @@ # @objectstack/service-i18n +## 17.0.0-rc.6 + +### Patch Changes + +- f8fe47e: feat(runtime,rest,plugin-auth,service-i18n,service-storage): route-ledger 条目类型加可选 `responseSchema` (#5791) + + #3877 的「最小首步」,维护者 2026-08-06 已批。**纯增量、零行为变更**:五个 route + ledger 的现有条目一行未改,字段缺省即「未声明」。 + + ## 为什么是这一步 + + #3877 量到的洞不是「发出的和声明的不一致」,而是**大多数路由根本没有可对账的声明**: + 237 条已挂载路由里 215 条是 `sdk` 面,而携带 schema 引用的是 **0 条**。于是同一单 + 里裁定了两件事——Stage C(批量补 ~190 条响应 schema)**永不排期**(一条响应 schema + 是「这个端点承诺什么」的产品决定,批量生产正是 #3676 / #3833 / #3847 / #3870 四个 + 缺陷的成因),以及先把「这条路由声明了什么」变成**可查询数据**,让 Stage D 的棘轮 + 将来有东西可棘。本次落地的就是后者。 + + ## 字段语义 + + `responseSchema` 是 `@objectstack/spec/api` 导出名,指向该路由**响应载荷**的声明: + 路由套 `{ success, data }` 信封时指 `data`,不套时指整个 body。信封本身不归它管, + 由 `pnpm check:route-envelope` 结构化守住——一个字段无法同时诚实地描述两层。 + + 五个 ledger 是五个各自独立声明、按约定同形的 interface,因此是五处同名同措辞的可选 + 字段,**不是**新建共享类型包。三个 ledger 明确要求保持 import-free(客户端守卫按 + 相对**源文件**编译它们),且 `zod` 并非每个持有 ledger 的包的依赖,故字段存的是 + **名字**而非 live schema 对象,解析放在能 import spec 的守卫里。 + + ## 已填的两条(实证,不是批量) + + 只填 #5682 已给出双断言覆盖(safeParse 判**值** + 键集判**键**)的 discovery 族两条, + 且刻意分处两个 ledger,以证明一个字段形状确实服务五个独立声明的条目类型: + + - `packages/runtime` `GET /discovery` → `DiscoverySchema`(走信封,指 `data`) + - `packages/rest` `GET /api/v1/discovery` → `DiscoverySchema`(裸发,指整个 body) + + `GET /api/v1` 这条 bare-base 别名**故意不填**:它与上面那条共用同一个 + `discoveryHandler` 闭包,但 #5682 的测试只驱动 `/api/v1/discovery`,「同一个 handler + 所以同一个形状」是对代码的论证而非对代码的测量。没有覆盖就不填。 + + ## 新增守卫 + + - `packages/client/src/route-ledger-response-schema.test.ts` —— 五个 ledger 的并集里 + 每一个 `responseSchema` 都到**活的** `@objectstack/spec/api` 导出里解析,并且真的 + 调用一次 `safeParse`(spec 的 schema 是 `lazySchema()` 代理,只查属性存在会被代理 + 陷阱满足)。含否定对照(少一个字母的名字、空串、导出了但不是 schema)与反空转下界。 + - `discovery-schema-conformance.test.ts`(runtime / rest 各一)—— 钉住 ledger 报的 + schema 就是该套件实际解析用的**同一个对象**,并各自测量了载荷所在的层级。 + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/services/service-i18n/package.json b/packages/services/service-i18n/package.json index 2b47fc49ec..83f1e66073 100644 --- a/packages/services/service-i18n/package.json +++ b/packages/services/service-i18n/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-i18n", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 0bfd463112..bc9e869ce8 100644 --- a/packages/services/service-job/CHANGELOG.md +++ b/packages/services/service-job/CHANGELOG.md @@ -1,5 +1,68 @@ # @objectstack/service-job +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/services/service-job/package.json b/packages/services/service-job/package.json index 1cdda2a9a3..f3314b9ba0 100644 --- a/packages/services/service-job/package.json +++ b/packages/services/service-job/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-job", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 5d95846f7d..28645100d6 100644 --- a/packages/services/service-knowledge/CHANGELOG.md +++ b/packages/services/service-knowledge/CHANGELOG.md @@ -1,5 +1,67 @@ # @objectstack/service-knowledge +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/services/service-knowledge/package.json b/packages/services/service-knowledge/package.json index 3f59bd935c..0a6bdcd377 100644 --- a/packages/services/service-knowledge/package.json +++ b/packages/services/service-knowledge/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-knowledge", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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-messaging/CHANGELOG.md b/packages/services/service-messaging/CHANGELOG.md index c060fced44..60e668fc8c 100644 --- a/packages/services/service-messaging/CHANGELOG.md +++ b/packages/services/service-messaging/CHANGELOG.md @@ -1,5 +1,159 @@ # @objectstack/service-messaging +## 17.0.0-rc.6 + +### Patch Changes + +- f1850d8: fix(services): `markAllRead` clears the WHOLE inbox, not one 200-row window (#6436) + + `POST /api/v1/notifications/read/all` is published as "mark **every** + currently-unread inbox message as read". It swept + `listInbox(userId, { read: false, limit: 200 })` — one page of the LIST, and + `200` is that list's hard cap — so it cleared at most 200 receipts per call. + + Measured over the real stack (sqlite-wasm + ObjectQL + service-messaging + hono + + - dispatcher), one user, 260 unread: + + | request | before | after | + | :------------------------------------- | ----------------: | ---------------: | + | `POST /notifications/read/all` | `readCount: 200` | `readCount: 260` | + | `GET /notifications` (same user, next) | `unreadCount: 60` | `unreadCount: 0` | + + **#6363 did not cause this — it removed the cover.** While `unreadCount` was + itself window-scoped the shortfall was self-consistent and invisible: clear 200, + poll, see a window with nothing unread in it, badge 0. Now that the badge is a + true total, the same request pair states the contradiction out loud, which also + raises the severity — a user presses "mark all read" and the badge stays lit. + + **A second, sharper face of the same defect, fixed with it.** That window was + `created_at desc` over ALL rows, with the `read` filter applied in memory AFTER + the truncation. An inbox whose newest 200 were already read therefore handed the + sweep an EMPTY id list and marked **nothing at all**, however much older unread + sat behind it. That is also why "loop the pages until one comes back empty" is + not the fix: it exits on exactly that empty first page. + + **What it does now.** It reads the unread SET rather than a page of the list, in + a FIXED two reads whatever the inbox size — the same one-column, unwindowed + projection of `sys_inbox_message` that #6363's `countUnreadTotal` already issues + to answer the badge, joined against the receipt spine `listInbox` already reads + unbounded. No loop and no page count to bound; nothing is asked of the data + layer that the bell's poll does not already ask on every saturated page. The + write stays one receipt per unread notification — that is the receipt model + itself (ADR-0030) — and `markRead`'s check-then-act upsert, its unique-conflict + convergence and its "no receipt row yet" insert are untouched. + + `readCount` now reports the number of **distinct notifications this call flipped + to `read`** (it reported "the unread ones inside the newest 200 rows"). Two + consequences: a notification materialized by several inbox rows counts once, and + an inbox row carrying no `notification_id` is skipped rather than counted — + read-state is keyed by the event id, so the receipt the old code wrote for those + (keyed by the inbox ROW id) was one the join could never read back. + + Unchanged: the list window (default 50, cap 200, newest first), `unreadCount`, + `markRead`, and an inbox smaller than the old window — which does the same + writes it always did, and no extra ones. + +- 17d0954: fix(services): `unreadCount` counts the TOTAL unread, not the returned window (#6363) + + `ListNotificationsResponseSchema.unreadCount` is published into the API + reference as **"Total number of unread notifications"** — a `.describe()`, so it + is the documentation shipped to every consumer of + `GET /api/v1/notifications`. It was counted inside `rows.map(...)` in + `MessagingService.listInbox`, i.e. over the rows that `limit` had already + truncated, so the badge saturated at the window size forever. + + Measured on a real stack (sqlite-wasm + ObjectQL + service-messaging + hono + + dispatcher) with 60 unread messages: + + | request | `notifications[]` | `unreadCount` (before) | `unreadCount` (after) | + | :---------- | ----------------: | ---------------------: | --------------------: | + | no `limit` | 50 | **50** | **60** | + | `?limit=10` | 10 | **10** | **60** | + + The declaration was right and the implementation was wrong, so the + implementation moved (maintainer ruling, 2026-08-07). Every consumer that + renders `unreadCount` as a bell badge now gets the number it asked for; nothing + had to learn an implementation detail to read the field correctly. + + **The list itself is unchanged.** `notifications[]` is still the window — + `limit` rows, default 50, hard cap 200, newest first. The two bounds were + conflated, not shared. + + Read-state lives on `sys_notification_receipt`, not on the inbox row + (ADR-0030), so the total is a reverse join rather than a `count()`. It is + computed only when the window came back **saturated** (`rows.length === limit`) + — a short window is already the whole matching set, so the common inbox costs + exactly what it cost before. When the window does saturate, the extra work is + one projection read of a single column (`notification_id`) under the same + `where`, no `orderBy` and no `limit`: the same order as the receipt scan + `listInbox` already performs unconditionally, and exact under a `type` filter + and for rows carrying no `notification_id`. + + Two related behaviours are unchanged and now pinned: a `read` filter narrows + the list and never the badge (asking for the read half does not mean zero + unread), and a `type` filter narrows both (the count answers the query that was + asked). + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/services/service-messaging/package.json b/packages/services/service-messaging/package.json index d93f61d14f..fbbbd58e88 100644 --- a/packages/services/service-messaging/package.json +++ b/packages/services/service-messaging/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-messaging", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 4be31b3d9f..7a3182edaf 100644 --- a/packages/services/service-package/CHANGELOG.md +++ b/packages/services/service-package/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/service-package +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [1a53a02] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [3d4c545] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/metadata-core@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/services/service-package/package.json b/packages/services/service-package/package.json index 2daa181d3a..0fa0e567d1 100644 --- a/packages/services/service-package/package.json +++ b/packages/services/service-package/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-package", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "Package management service for ObjectStack — publish, install, and manage packages", "type": "module", diff --git a/packages/services/service-queue/CHANGELOG.md b/packages/services/service-queue/CHANGELOG.md index 83d3a0a17c..5f3587dd99 100644 --- a/packages/services/service-queue/CHANGELOG.md +++ b/packages/services/service-queue/CHANGELOG.md @@ -1,5 +1,68 @@ # @objectstack/service-queue +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/services/service-queue/package.json b/packages/services/service-queue/package.json index e241aec459..6f4032ae1a 100644 --- a/packages/services/service-queue/package.json +++ b/packages/services/service-queue/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-queue", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 76ef36e214..3c5fe1d912 100644 --- a/packages/services/service-realtime/CHANGELOG.md +++ b/packages/services/service-realtime/CHANGELOG.md @@ -1,5 +1,68 @@ # @objectstack/service-realtime +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/services/service-realtime/package.json b/packages/services/service-realtime/package.json index b0c8e1902c..6c23ce9579 100644 --- a/packages/services/service-realtime/package.json +++ b/packages/services/service-realtime/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-realtime", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 96d4096a02..46af9ec7bb 100644 --- a/packages/services/service-settings/CHANGELOG.md +++ b/packages/services/service-settings/CHANGELOG.md @@ -1,5 +1,205 @@ # @objectstack/service-settings +## 17.0.0-rc.6 + +### Patch Changes + +- babddf6: fix(service-settings): localization's declared standards are the enforcement boundary — `valueDomain` enforced on both doors (#5712) + + `localization.timezone` promised "IANA zone" and `localization.currency` promised + "ISO 4217 code", but since #5131 the write path treated their curated 17/9-entry + `options` tables as exhaustive, and since #5204 the env path agreed — so + `PUT /api/settings/localization` with `timezone: 'Europe/Zurich'` (or + `currency: 'CHF'`) was refused with `invalid_option`, and + `OS_LOCALIZATION_TIMEZONE=Europe/Zurich` was ignored, despite both being values + every `Intl`-based consumer downstream handles. Maintainer ruling (2026-08-06, + reading 1): the curated tables are UI convenience lists; the boundary is the + standard's membership. + + The manifest now declares the merged spec vocabulary (#5933 / `SpecifierValueDomainSchema`) + on the three keys that promised a standard all along — `timezone: 'iana_time_zone'`, + `currency: 'iso_4217_currency'`, and `default_country: 'iso_3166_alpha2'` (third + case of the same hole: `^[A-Za-z]{2}$` admits `ZZ`) — and `SettingsService` + enforces a declared domain at the one decision point per door: + + - **Write door** (`validatePatch`): a domain-bearing specifier skips the + exhaustive-options check and judges the standard's membership instead, after + `pattern` (shape and membership narrow independently; the shape breach is the + coarser fact and speaks first). A breach is `invalid_value` with + `constraint: { valueDomain }` — no `FieldErrorCode` member names a + standard-domain breach, and `invalid_option` would misname the set that was + consulted. + - **Env door** (`effectiveEnvOverride`): the same membership judgment, so a + garbage override is loudly reported and ignored (falls back down the cascade, + pins nothing — #5204's contract, unchanged) while a legal one wins the cascade + and locks the key. + + Membership definitions follow the spec's pinned TSDoc: `iana_time_zone` is the + `Intl.DateTimeFormat` probe (NOT `Intl.supportedValuesOf('timeZone')`, whose + CLDR subset omits `UTC`, `Asia/Kolkata` and `Europe/Kyiv`); `iso_4217_currency` + is `Intl.supportedValuesOf('currency')`; `iso_3166_alpha2` is an explicit list + of the 249 officially assigned codes (no standard-library oracle exists — + `Intl.DisplayNames` names `ZZ` and `UK`). + + A specifier that declares no `valueDomain` is byte-for-byte unchanged: #5131's + exhaustive-options semantics stay in force for registry-backed tables such as + `mail.provider` / `sms.provider`, pinned by regression tests on both doors. + +- 4afdd3e: fix(service-settings): 写入路径与 env 路径执行 settings 声明的 `step` 网格 (#6199) + + `step` 是 `SpecifierSchema` 五个值约束里的**第五个**,也是最后一个只声明不执行的。 + #5932(PR #6201)补齐 `min`/`max`/`minLength`/`maxLength` 之后,`step` 在 + `packages/services/service-settings/src/` 里仍是**零读取点**:superRefine 不校验它, + 写入路径不读它,env 路径不读它。 + + **为什么判定为「值约束」而不是「纯 UI 提示」。** issue 提了两种读法,定论取自 schema + 自己的写法:`step` 与 `min`/`max` 声明在**同一段** `/** number / slider: numeric +bounds and step. */` 注释之下,即它是按「界」被作者写下的,而 #5932 的裁决(声明了 + 的界就必须绑定)随之传递。另一种读法(它只是 `input[type=number]` 上下箭头的步进, + 从不表达「其他值非法」)经核查不成立:落地时 `step` 在本仓库与 `objectui` 中**没有 + 任何消费者**——没有渲染器读它。按那种读法,这个键就是在为一个并不存在的渲染器表达 + 「呈现」,那正是 ADR-0049 的洞,而不是 UI affordance。 + + **修法与 #5932 同形,是同一族的第五个成员:** + + - `step` 挂进 `DeclaredBounds` 与 `firstRangeViolation`,因此它按构造同时到达两扇门 + ——写入路径(`validatePatch`)与 env 路径(`effectiveEnvOverride` 这**一个**判定点) + ——不可能成为「只在一侧执行」的下一个键。 + - 越界发码表里现有的 `invalid_value`(ADR-0114:「rejected for a reason no other + member names」)。码表里没有任何成员命名「网格」,而码表是刻意封闭的; + `rest-server.ts` 早已把 Zod 的 `not_multiple_of` 映射到同一个成员,即同一条件从另一 + 个方向到达时的同一裁决。⛔ `packages/spec` 未改动。 + - 沿用 #5131 / #5932 的 **TOUCH 闸门**:只校验本次 patch 触及的键。网格在产品生命 + 周期里会被**放粗**(0.05 的滑杆改声明成 0.1),持有旧值的工作区必须仍能编辑无关设置。 + + **锚点(anchor)约定:** 值须落在 `min + k * step` 上;未声明 `min` 时锚点取 `0`。 + 这是 HTML step-base 约定,也是声明读起来的唯一自洽含义 —— `min: 1, step: 2` 指的是 + **奇数**,而不是偶数;一律锚 0 会把这个 specifier 整个反转。`constraint` 同时带 + `step` 与(声明了的话)`min`,客户端据此自行重建网格。 + + **容差规则:** 网格判定为 `|value - nearest| <= max(|value|, |anchor|, |step|) * 1e-9`, + 其中 `nearest = anchor + round((value - anchor) / step) * step`。精确取模是错的 —— + 二进制浮点下 `0.7 / 0.1` 是 `6.999999999999999`、`1.2 / 0.1` 是 `11.999999999999998`, + 而这两个都是控制台滑杆自己会发出的值。容差取**相对**而非绝对:绝对量随操作数变化, + `1e-9` 在 `step: 1e-6` 上会宽到三分之一步长,在 `max: 1048576` 上又比一个 ULP 还紧。 + `1e-9` 落在两类误差之间:double 的相对精度约 `2.2e-16`,几步算术累积约 `1e-15`,比这 + 个界低六个数量级;而真正的越格差一小截步长(`0.15` 在 `0.1` 网格上差 `0.05`,相对 + `3e-1`),比它高八个数量级。比较在**值域**而非倍数域进行,以免容差的含义随网格粗细改变。 + 剩余存疑的方向也是刻意的:本闸门是对「昨天什么都收」的收紧,所以在算术确实分辨不出时 + (量级大到网格比 double 自身间距还细)判**收**。 + + **非正的 `step` 声明不构成网格。** `step: 0`(`anchor + k * 0` 是一个点)、负值、 + 非有限值一律**不记录网格**,与「option-bearing specifier 没有 options 表」同一处置: + 无可执行者,行为不变,永不拒写。这与 #5204 的注册期姿态一致 —— 注册**报告**、从不 + 拒绝 —— 而这里没有可报告的:声明了不可能网格的 manifest 既不拒写也不误配部署,它只是 + 没有约束住,和其余没声明 `step` 的 specifier 处境完全相同。 + + **已知后果,裁决时已接受:** 全仓库唯一的 `step` 声明是 `ai.manifest.ts` 的 + `temperature`(`min: 0, max: 2, step: 0.1`)。执行之后 `0.15` 被拒。这是该声明按其 + 字面绑定,而不是本闸门的缺陷;这份声明本身是否该改(若 `0.15` 应当合法,则该 manifest + 应声明更细的 `step` 或不声明),属于 manifest 属主的问题。 + +- 9566c38: fix(service-settings): 写入路径与 env 路径执行 settings 声明的 min / max / minLength / maxLength (#5932) + + `SpecifierSchema` 从存在起就声明了五类值约束 —— `pattern` / `min` / `max` / + `minLength` / `maxLength` —— 而 `SettingsService.validatePatch` 只读其中一类。 + 另外四个在整个写入路径上**没有任何读取点**:已发布的 manifest 里 42 个 + specifier 声明了取值窗口,每一个都只是装饰。 + + 落点最重的是 `auth.password_min_length`。它声明 `min: 6`,控制台的数字框也按这个 + 下限渲染,而 `PUT /api/settings/auth` 会接受 `1`(以及负数)并存下来,better-auth + 的口令策略随后照这个值执行。也就是说,声明是唯一一个宣称「存在下限」的东西,却没有 + 任何一层在守它 —— 正是 Prime Directive #10 的正面形状。`ai.manifest.ts` 的六项 + (temperature / max_tokens / timeout 等)同理。 + + **修法与 #5131(options 表)同形,是同一族的第三个成员:** + + - `validatePatch` 补一个取值窗口分支,发既有码表里的 `FieldError`(ADR-0114 D2): + `min_value` / `max_value` / `min_length` / `max_length` —— 与 + `record-validator.ts` 对同一类越界发出的码一致。`constraint` 带**完整窗口** + (`{ min, max }`,长度类再带 `actual`),客户端据此自行组织文案,不必解析我方 + 英文句子。⛔ `packages/spec` 未改动:约束早已声明,码表现有即够用。 + - 沿用 #5131 的 **TOUCH 闸门**:只校验本次 patch 触及的键。取值窗口在产品生命周期里 + 会被**收紧**(口令下限从 6 提到 8),窗口下方的老工作区必须仍能编辑它无关的设置, + 只在重写该键时才被告知。 + - env 侧走 `effectiveEnvOverride` 这**一个**判定点,与 options 表同处,复用同一组 + 比较函数 —— #5204 的成因就是同一比较有两份实现并各自漂移。因此 + `OS_AUTH_PASSWORD_MIN_LENGTH=1` 与写入路径得到同一个裁决:该 override 不生效、 + 不贡献 cascade 条目、不锁定该键,并在注册时打出一条(且仅一条)`error` 日志。 + + **刻意不做的判断:** 取值窗口只裁决**可比较的值** —— `min`/`max` 只看数字(含经 + JSON / 表单往返变成字符串的数字),`minLength`/`maxLength` 只看字符串。布尔、数组、 + 对象不做强制转换(`Number(true)` 是 1、`Number([])` 是 0):值的**形状**是 + `invalid_type`,属于另一个约束、另一个负责人,在这里发明裁决会拒掉本检查从未被要求 + 过问的写入。空值仍归 `required` 管。 + + 约束的读取以**声明**为准,而不是以 specifier 的 `type` 为准 —— 与旁边按类型收口的 + options 检查不同,这个差异是 spec 定的:`SpecifierSchema` 的 superRefine 把 options + 表**绑定**到 `select`/`radio`/`multiselect` 三型,却没有把四个窗口键绑定到任何类型。 + 在这里自拟一份类型清单,正是 options 注释警告的「第三份会漂移的清单」,并且会把本 + issue 原样复制到下一层:窗口键声明在清单外的类型上,照样解析、照样渲染、照样不执行。 + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/services/service-settings/package.json b/packages/services/service-settings/package.json index dac977e0d2..0982310e42 100644 --- a/packages/services/service-settings/package.json +++ b/packages/services/service-settings/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-settings", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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 35ba6353e6..7b100f4a95 100644 --- a/packages/services/service-sms/CHANGELOG.md +++ b/packages/services/service-sms/CHANGELOG.md @@ -1,5 +1,119 @@ # @objectstack/service-sms +## 17.0.0-rc.6 + +### Patch Changes + +- 1fa224a: feat(plugin-auth): the fixed-window counter gets its own `./rate-limit-storage` entry (#6040) + + `rate-limit-storage.ts` is the repo's ONE fixed-window counter — + `incrementFixedWindow` / `createLazyCounterStore` / `InProcessCounterStore`, + ADR-0069 D2 — and #4790's cross-reference asks later arrivals to reuse it + rather than write a third copy. They did, and from outside auth: + `@objectstack/runtime` counts inbound requests and endpoint policy through it, + and `@objectstack/service-sms` counts its daily SMS budget through it (#2814). + + `@objectstack/plugin-auth` published exactly one entry, `"."`, whose `export *` + chain takes **value** imports on `better-auth/adapters` + (`objectql-adapter.ts`) and `@better-auth/core/db` (`backfill-account-issuer.ts`). + Value imports are evaluated eagerly, so reaching those ~90 lines of counting + loaded `better-auth` + `@better-auth/{core,oauth-provider,scim,sso}` + `jose` + + `@noble/hashes` + `@objectstack/rest` + `@objectstack/platform-objects` first. + Measured against the built package: `require('@objectstack/plugin-auth')` puts + 109 modules in `require.cache`; the counter needs one. + + So the counter is now published on its own: + + ```ts + // before — 109 modules, the whole better-auth family + import { incrementFixedWindow } from "@objectstack/plugin-auth"; + // after — 1 module, 3.7 KB + import { incrementFixedWindow } from "@objectstack/plugin-auth/rate-limit-storage"; + ``` + + `tsup` emits the second entry with `splitting: false`, so it is a self-contained + bundle rather than a nominal split: `dist/rate-limit-storage.mjs` is 3.71 KB + against `dist/index.mjs`'s 330.28 KB, contains zero top-level imports and zero + occurrences of the string `better-auth`. The one better-auth reference that + survives is `import type { BetterAuthRateLimitStorage }`, which is erased at + build and costs a consumer nothing at runtime. + + **Nothing is removed.** The root still re-exports every one of these symbols, so + existing `@objectstack/plugin-auth` imports keep working unchanged — this is a + new entry point, which is why it is `minor` rather than breaking. The `patch` on + `runtime` and `service-sms` is the import-specifier switch in those packages; + their behaviour is identical. + + `src/rate-limit-storage-isolation.test.ts` pins the invariant from both sides, + in the shape `packages/types/src/node-isolation.test.ts` (#4700) established for + the `./node` split: it walks the real import graph from the subpath entry and + fails on any better-auth **value** import or any undeclared external package, + it fails if a consumer reaches the counter through the package root again, and + it fails if the root ever _stops_ pulling better-auth eagerly — because at that + point the split stopped buying anything and deserves re-measuring rather than a + suite that passes for the wrong reason. + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [1fa224a] +- Updated dependencies [35f7fb4] +- Updated dependencies [f8fe47e] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/plugin-auth@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/services/service-sms/package.json b/packages/services/service-sms/package.json index 0f8a44f665..5389812aaa 100644 --- a/packages/services/service-sms/package.json +++ b/packages/services/service-sms/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-sms", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "SMS service for ObjectStack — ISmsService + transport-pluggable outbound delivery (Aliyun / Twilio / log).", "main": "dist/index.js", diff --git a/packages/services/service-storage/CHANGELOG.md b/packages/services/service-storage/CHANGELOG.md index 4443d00b8f..fec9e872fc 100644 --- a/packages/services/service-storage/CHANGELOG.md +++ b/packages/services/service-storage/CHANGELOG.md @@ -1,5 +1,137 @@ # @objectstack/service-storage +## 17.0.0-rc.6 + +### Patch Changes + +- db59e9c: hooks: drop the last three `doc` / `previousDoc` alias reads on a hook context — read the engine's own keys only + + Behaviour is unchanged: every one of these limbs guarded against a producer that + has never existed, so none of them could be reached. + + - `service-storage` attachment lifecycle read `ctx.result ?? ctx.input.doc ?? ctx.input.data` + - `plugin-sharing` primary-BU projection read `(ctx.input.data ?? ctx.input.doc).user_id` + - `runtime`'s hook sandbox read `engineCtx.input ?? engineCtx.doc` and `engineCtx.previous ?? engineCtx.previousDoc` + + Every ObjectQL write context spells the payload `data` — measured and pinned by + `hook-input-shape-contract.test.ts` in `@objectstack/objectql` ("insert carries + `data` — never `doc`", #5273). The top-level pair is the same family one level + up: `HookContextSchema` declares `input` / `result` / `previous` and neither a + `doc` nor a `previousDoc`, and `engine.ts` — the sole producer of a HookContext + — builds neither. The limbs survived only because the old `HookContext.input` + contract table documented insert as `{ doc, options }`; that table was corrected + in #5668, and the same alias was removed from `trigger-record-change` in #5671. + These are the remainder (#5906), removed rather than left as a second de-facto + contract (PD #12). + +- f8fe47e: feat(runtime,rest,plugin-auth,service-i18n,service-storage): route-ledger 条目类型加可选 `responseSchema` (#5791) + + #3877 的「最小首步」,维护者 2026-08-06 已批。**纯增量、零行为变更**:五个 route + ledger 的现有条目一行未改,字段缺省即「未声明」。 + + ## 为什么是这一步 + + #3877 量到的洞不是「发出的和声明的不一致」,而是**大多数路由根本没有可对账的声明**: + 237 条已挂载路由里 215 条是 `sdk` 面,而携带 schema 引用的是 **0 条**。于是同一单 + 里裁定了两件事——Stage C(批量补 ~190 条响应 schema)**永不排期**(一条响应 schema + 是「这个端点承诺什么」的产品决定,批量生产正是 #3676 / #3833 / #3847 / #3870 四个 + 缺陷的成因),以及先把「这条路由声明了什么」变成**可查询数据**,让 Stage D 的棘轮 + 将来有东西可棘。本次落地的就是后者。 + + ## 字段语义 + + `responseSchema` 是 `@objectstack/spec/api` 导出名,指向该路由**响应载荷**的声明: + 路由套 `{ success, data }` 信封时指 `data`,不套时指整个 body。信封本身不归它管, + 由 `pnpm check:route-envelope` 结构化守住——一个字段无法同时诚实地描述两层。 + + 五个 ledger 是五个各自独立声明、按约定同形的 interface,因此是五处同名同措辞的可选 + 字段,**不是**新建共享类型包。三个 ledger 明确要求保持 import-free(客户端守卫按 + 相对**源文件**编译它们),且 `zod` 并非每个持有 ledger 的包的依赖,故字段存的是 + **名字**而非 live schema 对象,解析放在能 import spec 的守卫里。 + + ## 已填的两条(实证,不是批量) + + 只填 #5682 已给出双断言覆盖(safeParse 判**值** + 键集判**键**)的 discovery 族两条, + 且刻意分处两个 ledger,以证明一个字段形状确实服务五个独立声明的条目类型: + + - `packages/runtime` `GET /discovery` → `DiscoverySchema`(走信封,指 `data`) + - `packages/rest` `GET /api/v1/discovery` → `DiscoverySchema`(裸发,指整个 body) + + `GET /api/v1` 这条 bare-base 别名**故意不填**:它与上面那条共用同一个 + `discoveryHandler` 闭包,但 #5682 的测试只驱动 `/api/v1/discovery`,「同一个 handler + 所以同一个形状」是对代码的论证而非对代码的测量。没有覆盖就不填。 + + ## 新增守卫 + + - `packages/client/src/route-ledger-response-schema.test.ts` —— 五个 ledger 的并集里 + 每一个 `responseSchema` 都到**活的** `@objectstack/spec/api` 导出里解析,并且真的 + 调用一次 `safeParse`(spec 的 schema 是 `lazySchema()` 代理,只查属性存在会被代理 + 陷阱满足)。含否定对照(少一个字母的名字、空串、导出了但不是 schema)与反空转下界。 + - `discovery-schema-conformance.test.ts`(runtime / rest 各一)—— 钉住 ledger 报的 + schema 就是该套件实际解析用的**同一个对象**,并各自测量了载荷所在的层级。 + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/observability@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/services/service-storage/package.json b/packages/services/service-storage/package.json index 118ca178cc..76bf82bdd0 100644 --- a/packages/services/service-storage/package.json +++ b/packages/services/service-storage/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-storage", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "Storage Service for ObjectStack — implements IStorageService with local filesystem and S3 adapter skeleton", "type": "module", diff --git a/packages/spec/CHANGELOG.md b/packages/spec/CHANGELOG.md index 711f4c77ab..064c4a01cf 100644 --- a/packages/spec/CHANGELOG.md +++ b/packages/spec/CHANGELOG.md @@ -1,5 +1,2624 @@ # @objectstack/spec +## 17.0.0-rc.6 + +### Major Changes + +- f6609e6: feat(spec): action param 的 `options[]` 讲得出逐选项 `visibleWhen` —— 一个接好线却被门挡着的门控能力 (#5016) + + `ActionParamSchema.options[]` 的契约一直是 `{ label, value }`,#4001 批 14 把它从「靠删除来执行」改成了说出口。批 14 同时记下一个它不打算猜的能力问题:这个选项列表该不该讲 `SelectOptionSchema`(`data/field.zod.ts`)已经声明的那套逐选项词汇?#5016 逐键量了一遍,答案**不是整套照搬,而是一个键**。 + + ## 只开 `visibleWhen`,因为只有它有读者 + + | 键 | 声明在 | action param 选项这条路上的消费者 | 本次 | + | :------------------ | :----------------------- | :------------------------------------------------------------------------------------------------------------------------- | :------------------------------ | + | `visibleWhen` | `SelectOptionSchema` | **有** —— 四个选项控件全都经 `useCascadingOptions` → `resolveCascadingOptions` 按它过滤(ADR-0058 / objectui#2284) | **开放** | + | `color` | `SelectOptionSchema` | 无 —— 只有**已存值**的展示渲染器读(网格单元格 / 详情徽章);对话框只拿列表建输入控件,提交完就丢 | 继续拒绝,附指路 | + | `default` | `SelectOptionSchema` | 无,且是**层级写错** —— 对话框参数的默认值走参数自己的 `defaultValue`,高一层 | 继续拒绝,附改法 | + | `icon` / `disabled` | 仓里任何 spec 形状都没有 | 无 —— 只活在 objectui 内部 `SelectOptionMetadata` 接口里,四个选项控件里每一个 `disabled` 都是**字段级**的 `props.disabled` | 继续拒绝(#5016 的 C 选项未采纳) | + + 挡在作者和一个**能工作**的逐选项门控之间的,此前就只有 spec 这道门:内联参数的 `options` 是逐字下沉的(objectui `resolveActionParam` 内联分支 `options: param.options` → `ActionParamDialog` 逐条 spread 只翻译 `label` → `paramToField` 原样交给控件),而 `ExpressionInputSchema` 产出的 `{ dialect, source }` 信封正是 `evalFieldPredicate` 接受的形状。 + + ## 行为激活面 —— FROM → TO + + **这是本次最需要注意的一行:同一份元数据,以前写了等于没写,现在真的生效。** + + ```diff + params: [{ + name: 'severity', type: 'select', + options: [ + { label: 'Normal', value: 'normal' }, + { label: 'Overload', value: 'overload', + visibleWhen: "record.status == 'open'" }, + ], + }] + ``` + + | 版本 | 上面这份 metadata 的下场 | + | :------------------------ | :------------------------------------------------------------------------------------------------------------------------------------- | + | 16.x | parse **成功**,出来的是 `{"label":"Overload","value":"overload"}` —— `visibleWhen` 在任何渲染器看到它之前就被静默剥掉,选项**永远可选** | + | 17.0.0-rc(#4001 批 14 起) | parse **失败**,`unrecognized_keys` 明确报错 | + | 17.0.0(本次) | parse 成功,键**保留并生效** —— `record.status != 'open'` 时该选项**不再出现在下拉里** | + + 所以从 16.x 升上来的应用,如果曾经推测性地写过逐选项 `visibleWhen`(当时无害,因为它被丢掉了),升级后选项集会**变窄**。请复查这些谓词是否是你今天真正想要的:不想要就删掉键,想要就确认表达式对 `record` / `current_user` 求值的结果符合预期。`color` / `icon` / `disabled` / `default` 在 16.x 同样被静默剥掉,本次**不会**突然生效 —— 它们改为在 publish 时响亮拒绝,并各自指向该词汇真正生效的地方。 + + ⚠️ **客户端隐藏是 UX,不是授权。** `enforceActionParams` 按声明的选项**值**校验提交(ADR-0104 D2),它不求值逐选项 `visibleWhen`;因访问控制而屏蔽的选项必须由 action 自身的 body 或权限检查再拒一次,只把它从下拉里藏掉是可绕过的。 + + ## 本次**没有**修的一件事(objectui 侧,已另行记录) + + **字段回退那条路仍然丢键**:`resolveActionParam` 走的是 `param.options ?? normaliseOptions(field.options, …)`,而 `normaliseOptions` 把每个**继承来的**条目重建成 `{ label, value }`。这条丢弃早于本次改动、也不受本次影响(作者显式写的 `options` 数组优先级更高,压根不经过它),修复归属 objectui。因此本次的拒绝文案仍然刻意**不**开「把参数改成 field-backed 去继承」这张药方 —— 那是一条不存在的路(账本 finding 18:错误里的文案是行为,自信而错的处方比没有更糟)。 + + `bulk-action.zod.ts` 的 `.passthrough()` 特例维持不动:#4909 那两条理由(逐字到达 grid、objectui `BulkActionParam` 有显式 `[key: string]: unknown` 兜底)在这条路上都不成立,而这里的目标词汇是封闭的 —— 目标词汇封闭,正是「声明」胜过「容忍」的场合。 + +- 53068c1: feat(spec)!: the bare type name is now the AUTHOR state — 1384 aliases flipped, 102 `XInput` synonyms retired (ADR-0122 phase 2, #6083) + + A Zod schema denotes two types: `z.input` (what an author writes — defaulted keys + optional, pre-transform) and `z.infer` (what `.parse()` returns). Until protocol 17 the + bare name `X` meant the second one in 1384 places and the first one in 86, with nothing + recorded about which was which. + + **[ADR-0122](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0122-schema-type-alias-naming-convention.md) + settles it: the bare name `X` is the AUTHOR state, `XParsed` is the PARSED state.** + Phase 1 (16.x, additive) gave every schema with two distinct shapes its `XParsed` name so + nothing would be stranded. **This release is phase 2: it flips the bare names.** It is the + breaking half, and it is the reason `@objectstack/spec` goes to 17.0.0. + + ```ts + // before (16.x) // after (17.0.0) + export type Connector = z.infer<…>; export type Connector = z.input<…>; + export type ConnectorParsed = z.infer<…>; export type ConnectorParsed = z.infer<…>; + export type ConnectorInput = z.input<…>; // ConnectorInput: RETIRED + ``` + + ## FROM → TO + + There are exactly two migrations, and each has a mechanical test. + + ### 1. `XInput` → `X` (102 names removed) + + The flip made `XInput` a character-for-character synonym of the bare name, and ADR-0122 + D3 forbids a permanent synonym. Every retired name has the same fix: **drop the `Input` + suffix.** + + ```diff + - import type { ConnectorInput } from '@objectstack/spec/integration'; + - const c: ConnectorInput = { name: 'acme', label: 'Acme', type: 'saas' }; + + import type { Connector } from '@objectstack/spec/integration'; + + const c: Connector = { name: 'acme', label: 'Acme', type: 'saas' }; + ``` + + Find them: `rg '\b\w+Input\b' --type ts` over your own code, then keep only the hits that + resolve to `@objectstack/spec`. Every one of them is a compile error on upgrade — there is + no silent failure in this direction, because the name is gone. + + The 102 retired names, by module: + + | module | retired | + | :------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | `api/auth` | `SessionUserInput`, `LoginRequestInput` | + | `api/dispatcher` | `DispatcherRouteInput`, `DispatcherConfigInput` | + | `api/endpoint` | `ApiEndpointInput` | + | `api/plugin-rest-api` | `RequestValidationConfigInput`, `ResponseEnvelopeConfigInput`, `ErrorHandlingConfigInput`, `OpenApiGenerationConfigInput`, `RestApiPluginConfigInput` | + | `api/protocol` | `NotificationPreferencesInput`, `NotificationInput` | + | `api/query-adapter` | `RestQueryAdapterInput`, `ODataQueryAdapterInput`, `QueryAdapterConfigInput` | + | `api/rest-server` | `RestApiConfigInput`, `CrudEndpointsConfigInput`, `MetadataEndpointsConfigInput`, `BatchEndpointsConfigInput`, `RouteGenerationConfigInput`, `RestServerConfigInput` | + | `api/versioning` | `VersioningConfigInput` | + | `automation` | `FlowFunctionDeclarationInput`, `ActionDescriptorInput`, `TimeRelativeTriggerInput`, `WebhookInput` | + | `data/analytics` | `CubeInput`, `AnalyticsQueryInput` | + | `data/datasource` | `DatasourceInput` | + | `data/field` | `FieldParseInput` (→ `Field`), `CurrencyConfigInput` | + | `data/mapping` | `MappingInput` | + | `data/object` | `ObjectFieldGroupInput`, `RowCrudActionOverrideInput`, `ServiceObjectInput`, `ObjectExtensionInput` | + | `data/seed`, `data/seed-loader` | `SeedInput`, `SeedLoaderConfigInput`, `SeedLoaderRequestInput` | + | `identity` | `EvalUserInput`, `PositionInput` | + | `integration/connector` | `ConnectorInput` | + | `kernel` | `ClusterCapabilityConfigInput`, `ExecutionContextInput`, `ObjectStackManifestInput`, `PackageArtifactInput`, `PluginVendorInput`, `PluginQualityMetricsInput`, `PluginStatisticsInput`, `PluginRegistryEntryInput`, `PluginSearchFiltersInput`, `PluginInstallConfigInput`, `ServiceRegistryConfigInput`, `StartupOptionsInput` | + | `security` | `ExplainRequestInput`, `AdminScopeInput`, `PermissionSetInput`, `SharingRuleInput` | + | `system` | `CacheTierInput`, `CacheConfigInput`, `DistributedCacheConfigInput`, `BackupConfigInput`, `FailoverConfigInput`, `DisasterRecoveryPlanInput`, `EmailTemplateDefinitionInput`, `KeyRotationPolicyInput`, `EncryptionConfigInput`, `FieldEncryptionInput`, `EnvironmentArtifactInput`, `RouteHandlerMetadataInput`, `MiddlewareConfigInput`, `ServerCapabilitiesInput`, `JobInput`, `FeatureInput`, `PlanInput`, `SecurityContextConfigInput`, `StackServerConfigInput`, `RowLevelIsolationStrategyInput`, `SchemaLevelIsolationStrategyInput`, `DatabaseLevelIsolationStrategyInput`, `TenantSecurityPolicyInput`, `TranslationBundleInput`, `TaskRetryPolicyInput`, `TaskInput`, `QueueConfigInput`, `BatchTaskInput`, `BatchProgressInput`, `WorkerConfigInput` | + | `ui` | `ActionInput`, `InlineActionInput`, `NavigationContributionInput`, `AppInput`, `DashboardInput`, `DatasetDimensionInput`, `DatasetMeasureInput`, `DatasetInput`, `PageInput`, `JoinedReportBlockInput`, `ReportInput`, `ReportChartInput`, `ReportSortInput`, `ThemeInput` | + + **Nine `*Input` names are NOT retired** and need no change: `ExpressionInput`, + `CronExpressionInput`, `TemplateExpressionInput` and `PredicateInput` are the bare aliases + of their own `…InputSchema`, and `FormFieldInput`, `QueryInput`, `FieldInput`, + `ObjectStackDefinitionInput` and `NavigationItemInput` are composed types (recursive or + `Partial`-shaped) that no bare alias denotes. + + ### 2. `X` → `XParsed` **only where you hold a parse result** + + If you annotate a value you _wrote_, do nothing — the bare name is now correct, and this + is the whole point of the change: + + ```ts + // This did not compile in 16.x unless you knew to write `ConnectorInput`. + // In 17.0.0 it is simply right, in every domain. + const c: Connector = { name: "acme_erp", label: "Acme ERP", type: "saas" }; + ``` + + If you annotate a value that came _out of_ `.parse()` (or out of a `defineX()` factory, or + off the wire after the engine parsed it) and you read a defaulted key from it, move that + annotation to `XParsed`: + + ```diff + - const parsed: Connector = ConnectorSchema.parse(raw); + + const parsed: ConnectorParsed = ConnectorSchema.parse(raw); + if (parsed.enabled) { … } // `enabled` is `boolean` here, `boolean | undefined` on `Connector` + ``` + + **The grep that finds these:** `rg 'Schema\.parse\(' -A2` and `rg ': *\w+ *= *await'` in + your own code, then check each annotation. **The reliable finder is the compiler**: every + site that reads a defaulted key off an author-state value is a `TS18048` / + `TS2532` ("possibly undefined") or a `TS2345`. Upgrade, run `tsc`, and fix what it names. In + this repo — 1127 files annotate a value with a spec type — that came to **40 files outside + `packages/spec`**, and every one of them was a compile error first, never a silent change. + + **The one case tsc cannot name for you:** a _function's declared return type_. A parse + result is structurally assignable to the author state, so + + ```ts + function loadConnector(): Connector { + return ConnectorSchema.parse(raw); + } // still compiles! + ``` + + keeps compiling while quietly promising callers less than it delivers. If you have + factories or loaders that return a parsed value, re-declare them as `XParsed` by hand. + `@objectstack/spec`'s own 24 `defineX` factories were migrated exactly this way — + `defineApp(...)` now returns `AppParsed`, `defineConnector(...)` returns `ConnectorParsed`, + and so on for every factory whose schema has two shapes. + + ## What did NOT change + + - **No runtime behaviour.** Not one `.parse()` call, `.default()`, `.transform()` or schema + shape moved. This release changes which type name describes which value, nothing else. + - **`json-schema/` and `authorable-surface/` are byte-identical.** Those generators read + runtime `z.ZodType` exports, never type aliases. + - **Your metadata files.** `*.object.ts`, `*.view.ts`, connector and flow definitions + authored with `defineX(...)` are untouched. Bare-literal metadata files typed with + `XInput` need the suffix dropped and nothing else. + + ## Also in this release + + - **`check:spec-parsed-alias` is inverted.** It used to require every bare `z.infer` alias + to be paired or pinned; the flip empties that population, so it now refuses a bare name + that reads `z.infer` (the flip, enforced), refuses an `XInput` synonym of a bare name + (the retirement, enforced), and keeps the paired-or-pinned and stale-pin arms on the + flipped form. + - **57 previously ungoverned aliases were audited.** Inverting the gate widened it to the + 86 aliases that already read `z.input`, which phase 1 never examined. 22 gained an + `XParsed`; 35 were proved isomorphic and pinned, adding 35 to the pin registry (716 → 751 + on the merged tree, after #5055's four retirements and #5775's one addition). This closes + #5507's remaining scope. + - **`@objectstack/spec` public surface: 106 export names removed, 24 added.** The removals + are the 102 `XInput` aliases (plus re-exports); the additions are the 22 new `XParsed` + names (plus re-exports). All type-only — no runtime code, no bundle-size change. + + + +- 259459d: refactor(spec)!: retire `array_agg` / `string_agg` from `AggregationFunction` — `count_distinct` deliberately kept (#6188, ADR-0049) + + `AggregationFunction` declared eight functions; the SQL family compiles five. + `SqlDriver.mapAggregateFunc` and the Turso `RemoteTransport.aggregate` each lower + `count`/`sum`/`avg`/`min`/`max` and route everything else to one refusal, so + three of the eight were declared-but-unenforced against the backends this + platform targets — and, worse, the _set_ each backend implemented was different, + so "which aggregations can I use" had no answer an author could read off the + schema. + + What makes these two sharper than an ordinary inert declaration is that another + package had to carry a denylist for them. `service-analytics` subtracted + `array_agg` and `string_agg` by name in `UNSUPPORTED_AGGREGATES`, because + without that subtraction they reached the Cube strategy's `default` and came + back as `COUNT(*)` — **a row count in place of the value the author asked for**, + with no error and no log (objectui#2945). + + **The three unlowered functions were SPLIT, not retired as a block** (maintainer + ruling, 2026-08-07): + + - **`count_distinct` STAYS** and takes ADR-0049's _enforce_ leg. It is a + dashboard staple with one portable lowering (`COUNT(DISTINCT x)`), and + `service-analytics` lowers it already; the SQL-driver implementation follows + on its own card. Its declaration leads its implementation here by decision, + not by drift. + - **`array_agg` / `string_agg` take the _remove_ leg.** Display conveniences + with no measured pull, and `string_agg` never had one shape to lower to at + all: the delimiter is a second argument in PostgreSQL, a `SEPARATOR` clause in + MySQL and a differently named function in SQL Server. + + FROM → TO, both authoring surfaces: + + | Was | Now | + | :-------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | + | `aggregations: [{ function: 'array_agg', field: 'tag', alias: 'tags' }]` | no replacement — read the rows with an ordinary `fields` query and shape them in the caller, or materialise the roll-up as a stored field | + | `aggregations: [{ function: 'string_agg', field: 'name', alias: 'names' }]` | as above | + | `measures: [{ name: 'tags', aggregate: 'array_agg', field: 'tag' }]` | delete the measure — `compileDataset` already refused it by name, so it never produced a number | + + The retirement kit: + + - This is an enum **VALUE** retirement, so there is no `retiredKey()` tombstone: + the enum's own error map carries the prescription, keyed on the received value + so that only the two spellings which used to be legal are told they "were + removed" (the `crypto.hash` / `HookBodyCapability` precedent, #4391). A + mis-spelling still gets zod's list of the legal functions. For the same reason + nothing lands in `RETIRED_KEYS_BY_MAJOR` and the four surface ratchets are + byte-identical — no def and no authorable key changed. + - **ADR-0087 D2 conversion + D3 chain step** + (`dataset-measure-array-string-agg-removed`): `os migrate meta --from 16` + drops any `dataset.measures[]` declaring a retired aggregate, plus any derived + measure the drop strands, with a notice each. The measure is dropped rather + than stripped down because one with neither `aggregate` nor `derived` fails + the dataset's own refinement — a conversion whose output cannot parse is worse + than none. + - **D3 semantic entry** (`query-array-string-agg-retired`) for + `QueryAST.aggregations[].function`: a request surface, never stored, so there + is no source for the chain to rewrite and callers move their own queries. + - The engine's in-memory fallback (`@objectstack/objectql`) drops its arms for + both functions — a `switch` case on a value the enum no longer has does not + type-check, and a dead arm is how a retired vocabulary returns by accident. + - `service-analytics`' `UNSUPPORTED_AGGREGATES` is now **empty and kept**: it is + half of an arithmetic the lockstep suite enforces (`SUPPORTED = spec +vocabulary − this`), which is what stops the next aggregate added to the spec + from silently reaching that `COUNT(*)` default. + + **Behaviour that actually changes** — this is the rare narrowing that removes + reachable behaviour, and it is worth stating plainly: on `driver-mongodb` and on + the engine's in-memory fallback these two DID compute. A raw QueryAST + aggregation against those backends returned an array or a joined string and will + now be refused at parse. That unpredictability is precisely what the ruling + ended — an aggregation that worked on one backend and failed on another is not a + capability — and both of those backends are inside the #5499 freeze. Their code + is untouched; it is simply no longer reachable through a spec-valid request. On + the dataset path nothing changes: `compileDataset` refused both by name already. + + + +- 8140915: fix(spec)!: `composeStacks` 的 `i18n` 退出 last-wins —— 同值放行、冲突报错带处方 (#5051) + + #5005 把 `composeStacks` 的非数组顶层键统一成「同值放行 / 冲突报错」,并由维护者 + 2026-08-04 裁决点名否掉了 last-wins(**先声明的栈被后组合者无声覆盖**)。`i18n` 是 + 唯一逃过那一轮的键 —— 因为 #5005 的主题是**被丢掉**的键,而 `i18n` 本来就有一个能 + 工作(只是无声)的策略。于是它成了整个顶层键面上**仅剩的一个 last-wins**。 + 2026-08-06 裁决取 A(对齐),2026-08-07 窗口拉入 v17。 + + ## FROM → TO(行为激活面) + + | 场景 | FROM(v17 之前) | TO(本次) | + | :-------------------------- | :------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------- | + | 只有一个栈声明 `i18n` | 保留该声明 | **不变** | + | 多个栈声明**相同**的 `i18n` | 保留(数组里最后那个,值相同) | **不变**(同值放行) | + | 多个栈声明**不同**的 `i18n` | 数组里靠后的栈胜出,**靠前那个栈的声明被静默丢弃** —— 不报错、不告警,作者无从分辨「被覆盖」与「从没写过」 | **抛错**,点名冲突键 `i18n`、两个来源栈(manifest id,无 manifest 时用 `stack #N`)与两条出路 | + + 被静默丢弃的那一侧,过去从组合结果里彻底消失;现在它不再消失 —— 它以一条点名两个 + 来源栈的错误的形式出现,由作者决定保留哪一个。这就是本次「静默丢弃 → 不再丢弃」的 + 激活面:凡是今天**已经**在依赖 last-wins 覆盖语言配置的组合,升级后会在 + `composeStacks` 处响亮地停下。 + + ## 为什么本地化配置不能择一,也不能深合并 + + 一个栈的 `translations` 语言包是**照着它自己声明的 `supportedLocales` 写的**。让附加 + 包的 `i18n` 胜出,等于让基础栈的语言包去寻址一组组合后的应用不再承认的 locale —— + 运行时看到的只是「查不到翻译」,而不是「你的语言配置被覆盖了」。深合并同样出局 + (#5005 原裁决):它造出一个两位作者都没写过的第三种 locale 集合。 + + 仓内实测:`examples/app-crm`(`['en','zh-CN']`)与 `examples/app-todo` + (`['en','zh-CN','ja-JP']`)正是这一对 —— `defaultLocale` 一致,locale 集合不一致。 + + ## 迁移 + + 组合时报 `composeStacks conflict: top-level key 'i18n' …`,按错误信息里的处方二选一: + + ```ts + // A. 让两处声明一致(通常是取并集,由作者决定) + i18n: { defaultLocale: 'en', supportedLocales: ['en', 'zh-CN', 'ja-JP'], fallbackLocale: 'en' } + + // B. 只在应当拥有它的那个栈里声明 `i18n`,其余栈删掉这一段 + ``` + + `ComposeStacksOptionsSchema` **不加旋钮**:显式覆盖机制按 #5005 裁决,留给定制故事真 + 拉动时统一设计,不为单个键预支。数组键(含 `translations`)的拼接语义一字未变。 + +- f549a0d: refactor(spec)!: retire the L2 ETL layer — `automation/etl.zod.ts` had no executor, and the sync architecture doc was recommending it (#6414) + + `ETLPipeline`, `ETLPipelineRun`, `ETLSource`, `ETLDestination`, `ETLTransformation`, + the `ETLEndpointType` / `ETLTransformationType` / `ETLSyncMode` / `ETLRunStatus` + enums and the `ETL` factory are REMOVED under ADR-0049 enforce-or-remove. The whole + file goes, on the same reading #4738 used to retire L1 `DataSyncConfig` one layer up: + **narrative-only**. No engine ever parsed, scheduled or executed an `ETLPipeline`. + + Measured on `origin/main` immediately before the removal: the only non-spec + references in this repo are two fumadocs-generated documentation sources + (`apps/docs/.source/*.ts`), not executors; objectui has no reference at all; and + there is no `packages/spec/liveness/etl.json`, so no ADR-0049 gate ever had a reading + on the surface — while the same file family's EXECUTED half does have one + (`liveness/mapping.json`), which is what makes that absence meaningful rather than an + oversight. + + FROM → TO, layer by layer — with one gap stated plainly instead of redirected: + + | removed | use instead | + | --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | `ETLPipeline.source` + `syncMode` + `schedule` (scheduled extraction from an external system) | `ConnectorSchema.syncConfig` (`integration/connector.zod.ts`) — the live, parsed sync surface: strategy, direction, cron schedule, `conflictResolution`, batching, delete mode | + | `ETLTransformation` of type `map` / `cast`-like per-field work | `mapping.fieldMapping[].transform` (`data/mapping.zod.ts`) — `none`/`constant`/`map`/`split`/`join`/`lookup`, applied row by row by the REST import path | + | `ETLPipeline.schedule` alone | `system/job.zod.ts` | + | `ETLTransformation` of type `join` / `aggregate` / `script` / `merge` / `deduplicate` / … | **nothing.** There is no replacement because there was never an implementation — those ten transformation types named capabilities no runtime had. Do the work where it runs (the destination warehouse's ELT, a `flow`, a scheduled job), and let multi-stage movement return through ADR-0049's ENFORCE route: the engine first, the vocabulary second | + + **The fix:** delete the import. Nothing was ever deployed under an `ETLPipeline` — + that is the finding, not a consolation — so there is no data migration; `tsc` reports + TS2724/TS2305 at every import of a retired name. + + **`packages/spec/docs/SYNC_ARCHITECTURE.md` is rewritten in the same change**, and + that is not incidental. It named `ETLPipeline` as the recommended destination for + authors displaced by the L1 retirement and tabulated ten transformation types with + copyable examples down to `script | Custom JavaScript/Python`. Retiring the schema + while the doc still recommended it would have been self-contradictory, and + forwarding L1's authors to a second layer with no executor was the defect compounding + rather than closing. + + **Absorbed:** the #4962 `etl-retry-converged-onto-retry-policy` entry (`retry.maxAttempts` + → `maxRetries`, default 3 → 0) — both land in the unreleased protocol 17, so composed, + a rename on a shape that does not survive the major has no observable effect, and its + `retiredKey()` tombstone goes with the shape that carried it. + + The retirement kit — route 3: no tombstone, no D2 conversion. + `RETIRED_DEFS_BY_MAJOR[17]` (9 defs) plus the D3 `SemanticMigration` + `etl-pipeline-layer-retired` are the declaration. + + + +- cb3b6cd: fix(spec)!: `HierarchyScopeContext` carries the tenancy posture, so single-posture DEPTH is legal (#6139) + + Two accepted positions contradicted each other on `main`, and a resolver could + not satisfy both: + + 1. `IHierarchyScopeResolver.resolveOwnerIds` obliged an implementation to fail + CLOSED whenever `organizationId` was `null` — unconditionally, "no org is not + every org" (#5852/#5973). + 2. Ruling C (#5859, landed in PR #6067) requires a single-posture deployment — + one with no organizations at all — to feed an explicit `null` and still get + hierarchy DEPTH. + + `HierarchyScopeContext` had no way to tell those two `null`s apart, so a + strictly spec-conformant resolver (cloud PR #1196 fails closed unconditionally, + exactly as written) necessarily killed enterprise DEPTH on every single-posture + install. The contract demanded the behaviour the platform ruling forbade. + + **FROM** `{ userId, organizationId: string | null, tenantId?: string | null }`, + with `null` organization ⇒ fail closed, always. + **TO** the same plus a REQUIRED `posture: TenancyPosture` (ADR-0105 D1), with + the obligation now read from BOTH fields: + + - `posture: 'single'` + `organizationId: null` — **legitimate.** There is no + organization dimension at all, so `null` names the one implicit tenant. The + resolver MUST proceed and resolve DEPTH normally; refusing here retires + hierarchy scoping for every org-less deployment. + - `posture: 'group' | 'isolated'` + `organizationId: null` — **fail closed, + strictly.** A wall is in force, so `null` is a missing constraint. This half + is unchanged and unrelaxed: it is what closed the #5852 cross-organization + privilege escalation. + + A structured signal was chosen over prose ("single-posture deployments are + exempt") because prose cannot be read by the code that must act on it: the + resolver runs inside the enterprise package and needs the deployment fact at + call time, not a paragraph. + + `posture` is REQUIRED, on the same terms and for the same reason #5858 made + `organizationId` required: a producer that omits it must fail to COMPILE rather + than hand every resolver an `undefined` to guess about, when one guess leaks + across organizations and the other silently kills DEPTH. This is a breaking + change for anyone CONSTRUCTING a `HierarchyScopeContext`; implementors of + `IHierarchyScopeResolver` are source-compatible, though a resolver keying only + on `organizationId` is no longer conformant and should adopt the two-field read. + + Supplying it costs producers nothing new: the open sharing layer already + resolved the posture to decide whether to consult a resolver at all. That + derivation now lives in one place (`effectiveTenancyPosture()`), with the local + refusal expressed in terms of it, so the refusal and the reported posture cannot + drift apart. It still fails closed — an unresolvable posture reports the + strictest walled posture, never `single`. + + The `showcase-scope-depth` dogfood proofs now run a **spec-conformant** + reference resolver typed against the real interface. The previous fixture took + `c: any` and ignored the tenancy fields entirely, which is why 20 single-posture + e2e proofs stayed green throughout: no spec-conformant resolver was ever + exercised, so CI could not see the contradiction. Verified non-vacuous — with + the old unconditional rule restored, three DEPTH proofs fail. + +- 59b794f: feat(spec)!: `HookContext.api` 从 `z.unknown()` 收窄为 `IScopedContext`,文档教的第一个 hook 终于编译得过 (#5945) + + `HookContext.api` 是文档教的**主数据通道**,而它的类型是 `unknown`。于是所有文档、技能、示例里那个标准写法: + + ```ts + handler: async (ctx: HookContext) => { + const users = ctx.api.object("user"); // error TS18046: 'ctx.api' is of type 'unknown'. + }; + ``` + + 一行都编译不过 —— 包括 `hook.zod.ts` 里 `api` 这个键**自己 JSDoc 上的示例**。语料库全在这么教(`skills/objectstack-data/references/data-hooks.md`、`content/docs/automation/hooks.mdx`、`content/docs/api/error-handling-server.mdx`、`content/docs/kernel/runtime-services/*`),这些块都没进 `os:check`,所以从来没有一道门看见过。唯一进了 `os:check` 的那块(`runtime-services/examples.mdx`)也只能靠在示例里自建一个 `type CrossObjectApi = …` 再 `ctx.api as CrossObjectApi` 才编得过 —— 每个消费方各 cast 一遍、cast 的形状无人校验,正是 contract-first 要终结的方向。 + + **本次落地维护者裁决 C**:`packages/spec/src/contracts/` 新增 `IScopedContext` / `IScopedObjectRepository`(与 `IDataEngine` / `IObjectQLEngine` 同层同风格),`HookContext.api` 的 TS 类型指向它。 + + **声明面 = 语料库实测的调用点**,不多也不少(证据表在 PR 正文,逐条 file:line): + + - `IScopedContext`:`object(name)` + `transaction(cb, opts?)` + - `IScopedObjectRepository`:`find` / `findOne` / `count` / `insert` / `update` / `updateById` + + `upsert` / `delete` / `aggregate` / `create` 只出现在文档的**方法表与能力表**里、从没有一处调用点(表格不过编译器),`sudo()` 的三个调用方全部把值持成 `any` 且它是提权动作 —— 一律不声明,等到有调用点再按同一条规则加。这与 `IDataEngine` 当年(#4251)确立的「有证据才声明」是同一条纪律。 + + **运行时零变化**:Zod 侧仍是 `z.unknown()`(`z.custom` 会让 `HookContext` 在 JSON Schema 里不可表达,`gen:schema` 直接不再产出 `json-schema/data/HookContext.json`,进而在下次 `gen:docs` 抹掉它的参考页 —— 实测过,不是推测)。收窄是纯静态的:接受的值、JSON Schema、生成的参考页行全部逐字节不变,只有 `.describe()` 文案改了。 + + **漂移由编译器盯着**:`packages/objectql` 的 `ScopedContext` / `ObjectRepository` 声明了 `implements`,契约与引擎实际绑定的那个对象再也不能各说各话(把 `updateById` 改个名,objectql 的 `tsc` 会在 `implements` 处和五个 hook 派发点同时报错 —— 实测过)。 + + **FROM → TO —— 什么代码需要改** + + 读取端只会变宽,原来编译得过的读法一行都不用动(原来根本没有能编译过的读法)。两类**写入端**可能要改: + + ```ts + // 1. 自建 cast 的消费方 —— 删掉 cast 即可,`ctx.api` 现在自带类型 + -const api = ctx.api as CrossObjectApi; + -const account = await api.object('crm_account').findOne({ where: { id } }); + +const account = await ctx.api?.object('crm_account').findOne({ where: { id } }); + + // 2. 构造 HookContext 字面量的测试替身 —— `api` 现在必须是 IScopedContext 形状(或省略) + const ctx: HookContext = { + object: 'account', event: 'beforeInsert', input: {}, ql: {}, + - api: whateverStub, + + api: undefined, // 或一个带 object(name) / transaction(cb) 的替身 + }; + ``` + + `api` **仍是可选的**:`buildHookApi` 在全部五个派发点都会设置它,但改成必填会开始拒绝今天能过的部分上下文(没有活引擎时构造的 context),所以读法是 `ctx.api?.object(…)`。 + +- f549a0d: refactor(spec)!: retire `system/http-server.zod.ts`'s runtime vocabulary — the event, capability and status shapes nothing ever emitted (#5295) + + `ServerEventType`, `ServerEventSchema` / `ServerEvent`, `ServerCapabilitiesSchema` / + `ServerCapabilities` / `ServerCapabilitiesParsed` and `ServerStatusSchema` / + `ServerStatus` are REMOVED under ADR-0049 enforce-or-remove. This is the second and + final pass over the file: #4938 removed its CONFIG half (`HttpServerConfigSchema`, + nine keys, zero readers, zero authoring entry), and this removes the RUNTIME half — + a 7-member lifecycle event union, an eight-boolean capability report and a + five-state status record with connection and request counters. Nothing ever emitted, + consumed or parsed any of them. + + FROM → TO: + + | removed | what actually decides it | + | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | `ServerEventType` / `ServerEvent(Schema)` | nothing emits a server event feed. Lifecycle is the transport plugin's own start/stop seam; observability is `system/metrics.zod.ts` + `system/logging.zod.ts`, and `OS_SERVER_TIMING` for timings | + | `ServerCapabilities(Schema/Parsed)` | a transport plugin declares what it provides by implementing the kernel plugin contract — the seams it registers ARE the capability statement | + | `ServerStatus(Schema)` | `/health` for liveness, the metrics surface for counters | + + **The fix:** delete the import. There is no replacement key, because there was + never a key — none of the four was authorable on any shape. Server-level + configuration that IS authorable is untouched: `defineStack({ server: { trustProxy, +security } })` / `StackServerConfigSchema` (#5006) parses exactly as it did in 16.x, + as does the route-registration half of the same module (`RouteHandlerMetadata`, + `MiddlewareType`, `MiddlewareConfig`). + + **Why now, and what unblocked it.** The card was held rather than queued on a real + doubt: a response/capability vocabulary can legitimately be a REFERENCE surface for + host implementers, so "zero consumers in this repo" is weaker evidence for one of + those than for an authorable key. It was lifted by measuring the reference reader + itself — `plugin-hono-server`, the one in-tree host implementation, neither + implements nor reports any of the three: it names no capability record, no status + shape and no event union, and what it registers is routes and middleware. The + control passed in the same sweep (`MiddlewareConfig`, twelve lines away, resolves to + `packages/runtime/src/middleware.ts`). + + The retirement kit — route 3 of the retirement playbook, as #4938 was in this same + file: **no `retiredKey()` tombstone and no D2 conversion**, because a prescription + nobody can receive is noise and there is no authored document to rewrite. + `RETIRED_DEFS_BY_MAJOR[17]` (4 defs) plus the D3 `SemanticMigration` + `http-server-runtime-vocabulary-retired` are the declaration; the generated + baselines (`json-schema.manifest/system.json`, `authorable-surface/system.json`, + `api-surface/system.json`) lose their entries in the same change, deliberately. + + If host-implementer conformance becomes a real requirement it returns through the + ENFORCE route: an adapter contract with a checker behind it, vocabulary second. + + + +- 85ec26d: refactor(spec)!: reconcile the SDUI component props with the renderers that serve them — 4 keys retired, 9 declared (#5775) + + #5068 wired the first parse `ComponentPropsMap` ever had, and the corpus it + landed on diverged in **both** directions: keys objectui's renderers honour that + the schema never declared, and keys the schema declared — one of them + **required** — that no renderer has ever read. Maintainer ruling (2026-08-06), + direction A: the #5611 rule again, _the delivered and authorized shape is the + contract_. + + The sharpest case is the record picker. It required `displayField`, which + appears in no renderer; `record-picker.tsx` resolves `props.labelField ?? 'name'` + and renders `row[labelField]`. So an author who followed the schema and wrote + `displayField: 'title'` got a dropdown listing `name`, with a success receipt and + no diagnostic anywhere — ADR-0078 exactly. Two spellings of one concept, of which + only the undeclared one was ever read. + + **FROM → TO** + + | was | now | fix | + | ------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------ | + | `element:record_picker` `displayField: string` (**required**) | `labelField?: string` | Rename the key; the value (a field name) is unchanged. Optional now — the renderer defaults to `name`. | + | `element:record_picker` `searchFields?: string[]` | _(removed)_ | Delete the key. Use `filter` / `dataSource.filter` to restrict what the picker offers. | + | `element:record_picker` `multiple?: boolean` | _(removed)_ | Delete the key. Multi-record selection is not implemented on this element. | + | `page:card` `body?: Component[]` | `children?: Component[]` | Rename the key; the value is unchanged. `footer` is a distinct slot and is untouched. | + + `searchFields` and `multiple` go under ADR-0049 enforce-or-remove: the control is + a single-select `Select` with no search input, binding **one** record id into a + page variable — so `searchFields` narrowed nothing and `multiple: true` selected + nothing extra while reporting success. Either returns the day the capability is + implemented (#5021 / #4988 precedent); a declaration is not a roadmap. + + Newly **declared**, because the renderers already honour them (nine keys, no + behaviour change — this is the schema catching up): + `element:record_picker` `labelField` / `valueField` / `label` / `emptyText`; + `record:path` `stages[].terminal` (`'won' | 'lost'`, honoured ahead of the + renderer's value/label token heuristic); `page:tabs` `items[].value` (the stable + `?tab=` URL token) and `items[].count`; `page:card` `children`; and `children` on + `page:section` / `page:footer` / `page:sidebar`, which were declared `EmptyProps` + — "zero props" — while all three renderers render a child list. + + The retirement kit: + + - Four `retiredKey()` tombstones in `ui/component.zod.ts`, each carrying its own + prescription. `ComponentPropsMap`'s entries STRIP, so a bare deletion would + have replaced one silent no-op with another; a tombstone types the key `never` + (tsc at the authoring site) and raises the prescription at parse time. + - **ADR-0087 D2 conversions + D3 chain step** — + `record-picker-display-field-to-label-field`, `record-picker-inert-keys-removed`, + `page-card-body-to-children`, all `retiredFromLoadPath`, so `os migrate meta` + rewrites sources and the loader stays loud. Region level is the reach, as for + `page-header-subtitle-alias`: `PageComponentSchema` declares no children key, so + a component nested inside another's `properties` is covered by the tombstone + rather than the walk. + - `RETIRED_KEYS_BY_MAJOR[17]` entries for all four keys; baselines + (`authorable-surface/ui.json`, `json-schema.manifest/ui.json`, `api-surface/ui.json`) + and reference docs regenerated. + - Pins both ways: the prescription is asserted per key, and a clean parse is + asserted not to materialize any of them. + + Not in scope, deliberately: `page:card.visible` is a component-level visibility + predicate written into `properties` and hoisted by the renderer — a page to + rewrite onto the ADR-0089 `visibleWhen`, not a key to declare. + + No runtime behaviour changes. The renderers already read the declared spelling of + every key, and the four retired ones never had an effect to lose. + +- f7bd4e2: refactor(spec)!: retire the widget-registration vocabulary and five doorless i18n shapes — and KEEP `FieldWidgetProps`, which has a live consumer (#5055) + + `@objectstack/spec/ui` published two vocabularies nothing in the protocol carried. + Both are removed — **10 emitted defs, 26 exported names** — and the generated + `references/ui/widget` page with them. + + | file | removed | + | ------------------ | ---------------------------------------------------------------------------------------------------------------------------- | + | `ui/widget.zod.ts` | `WidgetManifest`, `WidgetLifecycle`, `WidgetEvent`, `WidgetProperty`, `WidgetSource` (its `npm` / `remote` / `inline` union) | + | `ui/i18n.zod.ts` | `I18nObject`, `PluralRule`, `NumberFormat`, `DateFormat`, `LocaleConfig` | + + There was no carrier key for any of them. No schema declared a key whose value was + a widget manifest or a locale config, so no metadata document could reach these + shapes and nothing ever parsed one. + + Three measurements, each re-run on `origin/main` immediately before the removal, + each with its controls passing in the same run: + + 1. **Static** — nothing under `packages/spec/src` imported `widget.zod` at all, + and every live import of `i18n.zod` names `I18nLabelSchema` or + `AriaPropsSchema` (both kept). `field.widget` is a `z.string()` naming a + component the **renderer** has registered; it has never referenced + `WidgetManifest`. + 2. **Graph** — a BFS over the in-memory Zod graph from all 24 metadata-type roots + plus `defineStack`'s `ObjectStackSchema` reached **none** of them, while + `PageSchema` and `ObjectListViewSchema` resolved `direct` in the same run and a + synthetic carrier flipped every one of them. So "unreachable" was a fact about + the graph, not a broken walker. + 3. **Call sites** — zero `.parse()` / `.safeParse()` in objectstack, objectui or + cloud outside these files' own unit tests. objectui's widget registry has + always carried its own runtime manifest (`RuntimeWidgetManifest` / + `RuntimeWidgetSource` in `@object-ui/types`, objectui#3161 / #4115), which + models different keys and never derived from these. + + Business ruling (2026-08-06; window moved from protocol 18 to 17 on 2026-08-07): + widget **registration** belongs to the renderer, not to the protocol — the + protocol's job is the props contract a widget implements. Localisation is already + delivered the other way: `I18nLabelSchema` documents that translation keys are + generated by the framework at registration time and translations live in + translation files, and the live translation surface is `system/translation.zod.ts`, + which uses none of these shapes. + + FROM → TO: + + | removed | what to do instead | + | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | `WidgetManifest` / `WidgetLifecycle` / `WidgetEvent` / `WidgetProperty` / `WidgetSource` | nothing to author — name a widget with the string `field.widget` (or the view's `widget` override) and register the component with the renderer. In objectui that is `@object-ui/core`'s `WidgetRegistry` and `RuntimeWidgetManifest`. | + | `I18nObject` | write the default-language string on `label` / `description`; the framework generates the translation key. Translations go in translation files (`system/translation.zod.ts`). | + | `PluralRule` | not a protocol surface — plural forms live in the translation files your i18n runtime reads. | + | `NumberFormat` / `DateFormat` / `LocaleConfig` | nothing to author — no formatter ever read one. Use `Intl.NumberFormat` / `Intl.DateTimeFormat` directly, as `packages/formula`'s template engine already does. | + + **No metadata document needs editing.** A stack that parsed before parses + byte-for-byte the same after: none of these was writable in the first place, and + `field.widget: my_picker` is untouched. The break is a TypeScript one — every + removed name is `TS2305` on `@objectstack/spec` and `@objectstack/spec/ui` after + upgrade. + + ## One of the nine widget sites is deliberately KEPT + + `FieldWidgetProps` / `FieldWidgetPropsSchema` / `FieldWidgetPropsParsed` **stay**, + and the reason is worth reading, because the issue that scheduled this batch + listed the site for removal on evidence that had been overtaken one day earlier. + + - It is a **React props contract**, not authorable metadata. It never appeared in + `authorable-surface/` or `json-schema.manifest/` at all — its `onChange` is a + `z.function()`, so no JSON Schema is emitted — so ADR-0049's question about a + declared-but-unenforced _authorable key_ never applied to it. Having no + `.parse()` is its design, not its defect: a props contract is enforced by `tsc` + in the repo that implements it. + - It has a **live cross-repo consumer**. objectui PR #3289 (merged 2026-08-03) + renamed `@object-ui/fields`' validation slot from `errorMessage` onto this + contract's `error` with no alias, made the form renderer produce it, and pinned + the result in `packages/fields/src/__tests__/spec-symbol-batch7.test.ts` against + `import type { FieldWidgetProps } from '@objectstack/spec/ui'` — deliberately, + so that "the day the spec stops exporting `FieldWidgetProps`, this file stops + compiling and the rename's reason is up for re-triage". Re-verified on objectui + `origin/main` 2026-08-07. + + `AriaPropsSchema` and `I18nLabelSchema` are likewise untouched. `AriaProps` is the + one **real door** in `i18n.zod.ts` — carried as `aria:` on ~30 live shapes under + six metadata-type roots and closed by #4001 批 16. + + ## ⚠️ objectui needs a companion PR in the same window + + Two objectui surfaces respond to this removal **by design**, not by accident: + + - `packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts` asserts the spec + _still owns_ `WidgetManifest` and `WidgetSource`, with the comment "if the spec + RETIRES one of these, the local dialect can take the natural name back… a + workaround should not outlive its reason (objectui#3169)". That assertion is + meant to go red exactly here. + - `packages/types/src/widget.ts`'s "Renamed off the spec's `WidgetManifest` / + `WidgetSource` name" docblocks now point at names that no longer exist. + + Neither is collateral damage — both are the tripwire firing as specified. The + objectui side is tracked separately; this repo cannot land it. + + The retirement kit: + + - **No `retiredKey()` tombstone, deliberately** — route 3 of the retirement + playbook ("nothing parses it → neither"), as used by #4988 (the ui/ + interaction-config family), #4834 / PR #4878 (kernel plugin-runtime family) and + #4938 / PR #5293 (`HttpServerConfig`). A tombstone is a message to whoever + writes the key; with no carrier key there is no shape for one to sit on and no + author who could ever receive it. + - **No ADR-0087 D2 conversion**, for the same reason: there is no source to + rewrite, because the keys were unwritable. The registered record is the D3 + `SemanticMigration` `ui-widget-i18n-family-retired`, with the protocol-17 step's + rationale extended, plus the ten `RETIRED_DEFS_BY_MAJOR` entries the #4725 + manifest-deletion gate reads. + - **`WidgetManifest.performance`'s tombstone is subsumed, not deleted in + isolation** — the #4657/#4834 shape. It goes with the shape that carried it, + which is strictly stronger: there is no longer a manifest to author the key + into, so the prescription an author needs is no longer "delete this key". + - **Whole-file deletion was rejected per file, not assumed.** Unlike #4988, both + files here keep a live occupant, so this is a shape retirement and the files + stay. That is asserted, not just intended. + - Baselines updated deliberately: `json-schema.manifest/ui.json` (−10, the #2978 + ratchet fires first and demands each deletion), `authorable-surface/ui.json` + (−65, adjudicated by the #4650 gate's path 3 "def no longer emitted by this + build"), `api-surface/ui.json` (−26). Reference docs, `references/ui/meta.json`, + the skill reference indexes and the strictness-ledger counts regenerated — the + `no door` bucket goes 14 → 1. + - `packages/spec/variant-docs.json`'s `type:inline|npm|remote` entry is deleted + with the discriminated union it described. A ledger row whose union has left the + source is the #5552 failure mode; `pnpm check:variant-docs` is the gate. + - **Pins are bidirectional.** `ui/widget-i18n-retirement.test.ts` asserts absence + across every public entry by resolved symbol identity _and_ the survival of the + three shapes a too-wide sweep would take — all three of which live in the two + files being emptied. It also pins the exact `error` slot objectui#3289 depends + on, so a change that would silently break that repo goes red in this one first. + - The #5056 clone-overlap regression fixture was rebuilt rather than re-pointed: + `door-reachability.testkit.test.ts` constructs the same 2-of-19 shared-leaf + shape locally, so the instrument's measured bound survives its subject. + + No runtime behaviour changes. That impossibility is the reason for the removal. + + + +- f549a0d: refactor(spec,client)!: retire `ViewProtocol`'s five viewId-addressed methods and their ten schemas (#6239) + + `listViews`, `getView`, `createView`, `updateView` and `deleteView` — the + `ViewProtocol` interface and `ListViews`/`GetView`/`CreateView`/`UpdateView`/`DeleteView` + Request+Response schemas in `api/protocol.zod.ts` — are REMOVED under ADR-0049 + enforce-or-remove (maintainer ruling 2026-08-07). `@objectstack/client` drops the + five response types it re-exported. + + Measured on `origin/main` immediately before the removal, the surface had none of + the three things a protocol method needs: + + - **no implementation** — `packages/metadata-protocol/src/protocol.ts` declares no + `listViews`/`getView`/`createView`/`updateView`/`deleteView`; its only view + resolver is `getUiView`; + - **no route** — `packages/rest/src/rest-server.ts` never mentions `viewId`, so + nothing viewId-addressed was reachable over HTTP at all; + - **no caller** — the only `ViewProtocol` mention outside its own file was + `content/docs/kernel/services-checklist.mdx`, which already recorded the five as + declared-and-unrouted. + + FROM → TO — both replacements are surfaces that were always the live ones: + + | removed | use instead | + | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | + | `listViews` / `getView` / `createView` / `updateView` / `deleteView` (+ their 10 schemas) | the generic metadata methods with `type: 'view'` — `getMetaItem` / `getMetaItems` / `saveMetaItem` / `deleteMetaItem`, served at `/api/v1/meta/view/:name` | + | `GetViewResponse` as "the shape of the resolved view" | `GetUiViewResponse` — `getUiView`, served at `GET /api/v1/ui/view/:object/:type` | + + **The fix:** delete the import and address views by NAME through the metadata API + (`view` is a metadata type), or by object+type through `getUiView`. Nothing + addressed a view by `viewId` before this change either; that is the finding. + + **Why a removal rather than a note.** The declared surface is name-identical and + semantics-adjacent to a real one, which makes it an attractive nuisance in every + grep — and it has already mis-directed a decision: **#5948's issue body AND its + 2026-08-07 maintainer ruling both read `GetViewResponseSchema` (zero + implementations) as the contract of `GET /ui/view/:object/:type`**, whose declared + response is `GetUiViewResponseSchema`, 250 lines up and one word different. That + ruling's reasoning happened to survive the mix-up; this removal stops relying on + that luck. + + The retirement kit — route 3: **no tombstone and no D2 conversion** (none of the ten + was a key on an authorable shape, and nothing parsed them, so there is no source or + `sys_metadata` row to rewrite). `RETIRED_DEFS_BY_MAJOR[17]` (10 defs) plus the D3 + `SemanticMigration` `view-management-protocol-retired` are the declaration; the + generated baselines and reference docs lose their entries in the same change. + + If "read and write ONE view by id" becomes a real requirement, it returns + implementation-first. + + + +- e8f435c: feat(spec)!: a `view` body must be a view before the union judges it (#5599) + + `ViewMetadataSchema` — the schema the `view` metadata type registers, and so the + one both `saveMetaItem`'s 422 gate and the read-time `_diagnostics` badge consult + — accepted **any object at all**. Measured on `origin/main`: + + ``` + getMetadataTypeSchema('view').safeParse({ nope: 1 }) -> success, data = { type: 'simple' } + getMetadataTypeSchema('view').safeParse({}) -> success, data = { type: 'simple' } + + saveMetaItem({ type: 'view', name: 'garbage_view', item: { nope: 1 } }) + -> { success: true, state: 'active', seq: 1 } + persisted body = {"nope":1,"name":"garbage_view"} + ``` + + The union's fourth member (`FormViewSchema.extend(…).strip()`) both strips + unknown keys **and** declares no required key — `type` even carries a `'simple'` + default — so it matched every object and handed the whole union a wildcard. The + `.strip()` is deliberate and load-bearing (#5074: it is what carries Studio's + round-trip keys); the defect is that an arm which strips _and_ requires nothing + is a universal match. So `view` was the one common overlay type whose declared + write-path spec validation (ADR-0005 §Validation) could be bypassed outright — a + `declared ≠ enforced` gap at union **member selection**, one level above the + object schemas #4001 closed. + + Because `saveMetaItem` persists the _original_ body rather than the parse output, + a wrong-shaped view — an AI-generated body in the wrong dialect, a hand-written + one with every key misspelled — did not fail loudly. It became an **active** view + overlay that renders nothing, and the read path then re-parsed it through the same + schema and badged it `_diagnostics.valid: true` (#5598), so Studio agreed it was + fine. + + **The fix.** A minimal identity precondition now runs ahead of all four arms: a + `view` body must carry at least one key some member declares, discounting the + keys the write path stamps onto every body itself (`name` always, plus + `viewKind`/`object`/`label` inherited from a shadowed registry entry — #2555). + The bar is _shape_, not completeness: `{ isPinned: true }` is not a renderable + view either, but it is unambiguously a view operation and still saves. No arm's + `.strip()` changed, and `/api/v1/meta/types/view` emits a byte-identical + `anyOf` of four in both the output and input directions, so Studio's SchemaForm + renders exactly as before. + + **Behaviour change** (why this is major — it is an enforcement close, not a new + capability): + + | `view` body | Before | After | + | :--------------------------------------------------------------------------- | :----------------------- | :----------------------------------- | + | `{ nope: 1 }`, `{ id: 'x' }` — no recognized key | saved, stored **active** | **422** | + | `{}` | saved, stored active | **422** | + | identity only (`{ name }`, `{ name, object, viewKind, label }`) | saved | **422** | + | `{ isPinned: true }`, `{ hidden: true }`, `{ sortOrder: 3 }`, `{ order: 2 }` | saved | unchanged — saved | + | any container / ViewItem record / flattened overlay | as before | unchanged | + | a body mixing garbage **with** a real view key | stripped and saved | unchanged — still stripped and saved | + + That last row is the deliberate residue of the minimal fix: the precondition asks + "is this a view", never "is every key meaningful". Closing it means closing the + arms, which would break the round-trip capability #5074 exists to protect. + + **FROM → TO.** Existing projects whose stored views carry stray-key bodies will + start seeing 422 on the next save of those views. Reads are unaffected — nothing + is deleted or rewritten — but the same documents now badge `valid: false`, which + is how you find them. The platform ships a sweep endpoint for exactly this: + + ```bash + curl -s "$OS_URL/api/v1/meta/diagnostics?type=view" -H "Authorization: Bearer $TOKEN" \ + | jq -r '.entries[] | "\(.name)\t\(.diagnostics.errors[0].message)"' + ``` + + Each row names the view and why it is rejected. The fix is per row: give the body + a real view shape, or delete the overlay if it was never a view to begin with. + + ```diff + - { "nope": 1, "name": "crm_lead.all" } + + { "name": "crm_lead.all", "object": "crm_lead", "viewKind": "list", + + "config": { "type": "grid", "columns": ["name"] } } + ``` + + The rejection carries its own prescription rather than a rootless + `Invalid input` — it names the key classes a view may open with, separates keys + it does not recognize from identity keys it recognizes but discounts, and it is + one issue, not one plus four `invalid_union` branches. + + **New export.** `VIEW_WRITE_PATH_IDENTITY_KEYS` (`@objectstack/spec/ui`) — the + discounted set, exported so the producer side can be pinned against it. It is: + `normalizeViewMetadata` must never stamp a key absent from that set, or the key + silently becomes evidence again and re-opens this hole; a behavioural test in + `@objectstack/metadata-protocol` fails in the file that would introduce it. + + Direction A from the issue — giving the form arm a required floor — remains + deliberately **not** taken. It needs Studio's flattened round-trip bodies + measured first, or it 422s writes the platform itself makes; the ruling on #5599 + deferred it as a possible second tightening on top of this one. + +### Minor Changes + +- 97e7e3c: feat(spec): `ActionSchema.visible` / `disabled` speak one shape — `boolean | string(CEL) | {dialect, source}` (#5970) + + An action's two condition keys accepted different vocabularies. `disabled` took + all three arms; `visible` had no **boolean** arm, so `visible: true` — the most + obvious thing an author can write, and a shape already present in stored + metadata — was a parse error on the spec side while objectui's `ActionDef` + accepted it and pinned it with tests. + + Both keys now accept the same three arms, cheapest first: + + | arm | example | meaning | + | :-------------------- | :----------------------------------------------------- | :------------------------------------------------------------------ | + | `boolean` | `visible: false` | the degenerate literal — settled at authoring time | + | `string` | `disabled: "record.status == 'closed'"` | CEL shorthand, normalized to the envelope at parse time | + | `{ dialect, source }` | `{ dialect: 'cel', source: '…', meta: { rationale } }` | the full envelope, for authorship metadata or a non-default dialect | + + **Purely additive** — every shape that parsed before parses the same way, and + every shape that was rejected is still rejected (an empty CEL string, a number, + `null`, an envelope missing `dialect`, an envelope with neither `source` nor + `ast`, an unknown dialect). No migration, no ADR-0087 disposition: nothing an + author can write was removed or renamed. + + The boolean arm is deliberately **not** normalized into + `{dialect: 'cel', source: 'true'}`. A literal survives as a literal so a + renderer can branch on it without standing up an evaluator, and `false` stays + statically greppable. + + **Why unify rather than leave it.** An asymmetry between two keys that mean the + same _kind_ of thing is a dialect nursery: it teaches every consumer to carry + its own widening, and each of those is a second de-facto contract (Prime + Directive #12). Console's `DeclaredActionsBar` was carrying exactly that as an + `(action as any).disabled` cast. This change is what lets #4075 step 3 derive + objectui's `ActionDef` from the spec schema and delete the casts. + + **One new rejection, at the interaction with `requiresFeature`.** The + declarative feature-gate sugar lowers into `visible`, so it now meets two + literals it never could before, and boolean algebra decides them in opposite + directions: + + - `visible: true` + `requiresFeature: 'x'` → the gate alone. `true && ` IS + ``, so spelling the default out explicitly lowers exactly like omitting + the key. + - `visible: false` + `requiresFeature: 'x'` → **parse error**. `false && ` + is `false` whatever the flag says, so the gate could never take effect and the + declaration is inert on arrival — the parses-clean-changes-nothing shape + ADR-0078 exists to reject. The message names both exits: drop + `requiresFeature` to keep it hidden, or drop `visible: false` to let the flag + decide. This combination was unwritable before (the boolean arm did not + exist), so no stored metadata can carry it. + + `bulkActions[].visible` is unchanged and keeps the two predicate arms only — a + per-record eligibility predicate has nothing to say as a constant. Its + description no longer claims shape-identity with `action.visible`. + +- d8e8d9c: feat(spec): `BulkActionDefSchema` accepts `requiredPermissions` — the capability gate the selection bar already enforces (#6257) + + The renderer has filtered selection-bar buttons on `def.requiredPermissions` + since objectui#3492 (`BulkActionBar` runs the same `useCapabilityGate` as the + row kebab and record header), but the `.strict()` `BulkActionDefSchema` did not + declare the key, so no legal metadata could ever reach that filter — + `enforced ≠ declarable`, the mirror image of the "declared ≠ enforced" gap. + The forms with no workaround were the INLINE data-plane defs + (`operation: 'update' | 'delete'`): they dispatch no action, so unlike a def + promoted from `bulkActions: ['']` (or an aggregate def naming a declared + action) they have nothing to inherit a gate from. In practice that meant a + declarative bulk delete — the button that most needs a gate — was visible to + every caller who could open the list, and rejected only per record, server-side, + after the click. + + `BulkActionDefSchema` now declares an optional `requiredPermissions: string[]` + with `action.requiredPermissions` semantics verbatim: absent or empty always + passes, several entries AND, a client that cannot resolve the caller's + capabilities fails OPEN (the server stays the authority), and the platform-admin + bit grants no exemption — the gate reads grants. On a data-plane def the key + governs visibility only; the write is still authorized by the data API's object + permissions and server hooks. The `ActionSchema` near-miss aliases + (`permissions`, `capabilities`, `requiresPermissions`, `requiredCapabilities`, + `acl`) rename onto the new key here too. No renderer change: objectui's + `BulkActionDef` type and `BulkActionBar` filter shipped in objectui 11 + (objectui#3548). + + Specimens: `examples/app-showcase` `showcase_project.default` gains the two + inline gated defs the #6157 action-gating matrix could not pin — `relabel_ops` + (`update` + `patch`, gated on the Ops-held `showcase.export_data`) and + `purge_restricted` (`delete`, gated on the granted-to-nobody + `showcase.restricted_ops`). + +- 94e749b: feat(spec): 定形 `multi: true` 批量写在 `before*` 阶段的按行 hook 契约(#6462) + + #5574 的维护者裁决(2026-08-06,方案 B)把「批量写按行语义为平台契约」(#4800 / + #4862,after 侧已由 #5038 交付)延伸到 `before` 型 hook,并指定 **contract-first + 拆分:spec 契约子单先行,engine 实现随后**。本次变更是该拆分的 spec 半边 —— 只落 + 契约、pin 测试与 ADR 附录,**engine 一行未动**。 + + **为什么需要这条契约。** 谓词写路径上 `ctx.previous` 在 before 阶段从未被绑定,于是 + 每一个按守卫写法写出来的 hook —— `if (ctx.previous?.locked) throw` —— 在批量写上 + 静默放行。hotcrm 实测:一次批量编辑绕过全部 15 个守卫 hook,把单行路径会拒绝的 + `readonly: true` 字段写成 `null`。失效方向是 fail-open,而让它静默的可选链正是 AI + 会写出的形状。 + + **新增契约面** `@objectstack/spec/data` → `bulk-write-hook-conformance.ts`: + + - `BULK_WRITE_HOOK_DISPATCH_CONTRACT` —— 四个写事件的按行派发表(before/after × + update/delete),逐条声明 per-row 上下文携带哪些键、载荷作用域,以及 **`delivered` + 标记**:after 半边由 #5038 交付(`true`),before 半边为已裁未交(`false`, + engine 半边 = #5574 engine 卡)。契约先行必须能被读出「尚未交付」,否则它就是一次 + declared ≠ enforced。 + - `MAX_BULK_PER_ROW_HOOK_ROWS` / `BULK_PER_ROW_HOOK_LIMIT_ERROR_CODE` / + `resolveBulkPerRowHookBudget()` —— `assertBulkPerRowHookBudget` 先例的契约级表述: + **两个阶段共用一个上限**,超限在**首次按行派发之前**整单拒绝(什么也不写、一个 + handler 也不跑),永不降级成「整批一次派发」。 + + **载荷可改写语义(裁决必答项 1)的答案是:载荷仍然只有一份,作用域是整批。** 每个 + per-row 上下文拿到的是**同一个** payload,而不是逐行副本 —— 于是 N 份载荷不可能分叉, + 没有合并步骤,谓词写永远不会被拆成 N 次单行写(仍是一次 `updateMany`、一个受影响行数 + #4639)。逐行副本 + 「一致则合并、分叉则拒绝」这条更显然的路线被实测证据否掉:objectql + 自带的 `sys_stamp_audit_update` 注册在 `'*'` 上,且在**每行**的 stamp 内部读 + `new Date()`,跨毫秒的两行 `updated_at` 天然不同 —— 该规则会非确定性地拒绝正常批量写。 + + **行为不变。** 本次不改任何 Zod schema 的接受面,不新增可授权键,不动 engine:今天能 + 通过校验的元数据,改动后逐字节仍然通过。`hook.zod.ts` 的 `input` 形状表继续描述引擎 + **当前**的构造(由 objectql 对真实派发钉住),只是补了一条指向新契约的前瞻说明 —— + 表与引擎不允许抢跑,这正是 #5273 的教训。 + + ADR-0058 新增 **Addendum II**,同时收纳裁决指定的 #5748 半边(`data.id` 与 + `where.id` 统一走标量测试,已由 PR #5919 交付),并把 Addendum I 中「`before*` hooks + are NOT per row」一段标记为 **SUPERSEDED**(原文保留 —— 被推翻的决策本身是记录)。 + +- ae31a19: fix(spec,metadata-protocol): `capability` 补齐三处注册 —— 授权面不再接受任意 JSON (#5961) + + `capability` 是「enforced but undeclared」——#5271 给 `api` 关掉的那个 + `declared ≠ enforced` 的镜像。平台早就把它当成一个 metadata kind 在用: + `PLURAL_TO_SINGULAR` 从 #5870 起就有 `capabilities` → `capability`, + `AppPlugin` 用这个名字注册 stack 声明的 capability, + `bootstrapDeclaredCapabilities` 再读回来 seed `sys_capability`。但三处注册表 + 里都没有它:`MetadataTypeSchema`(kind 枚举)、`BUILTIN_METADATA_TYPE_SCHEMAS` + (schema 解析)、`DEFAULT_METADATA_TYPE_REGISTRY`(谁可以写、怎么加载)。 + + 后果有两条,第二条才是这个 issue 属于授权缺陷而非整洁度问题的原因: + + - `getMetadataTypeSchema('capability')` 返回 `undefined`,于是 `saveMetaItem` + 走了它自己文档化的「未注册类型 → 不校验直接存」分支, + `PUT /api/v1/meta/capability/:name` 接受**任意 JSON** 落进 `sys_metadata`。 + capability 是靠**名字字符串**被解析的——授予侧 `systemPermissions`、 + 要求侧 `requiredPermissions` 都是——所以一行任意 JSON 直接落在活的授权命名 + 空间里。 + - `isRuntimeCreateAllowed` 镜像 `getMetaTypes()` 的合成规则:没有静态注册表条目 + 的类型被当作可运行时创建。所以缺的那一行不只是「没关上门」,它**把门打开了**。 + `/meta/types` 同步发布了这个虚构:`allowRuntimeCreate: true` + 无 schema, + metadata-admin 引擎据此渲染成一个 raw-JSON 文本框。 + + ### 改了什么 + + - **`BUILTIN_METADATA_TYPE_SCHEMAS['capability'] = CapabilityDeclarationSchema`**。 + 既有的 422 `invalid_metadata` 路径就此覆盖 `capability`,`/meta/types` 发出真 + JSON Schema。 + - **`DEFAULT_METADATA_TYPE_REGISTRY` 新增 `capability` 条目, + `allowRuntimeCreate: false` + `allowOrgOverride: false`**。ADR-0066 D1:包 + DEFINE capability,权限集 GRANT,资源 REQUIRE。管理员在运行时凭空造一个 + capability 在这个三分里没有位置——代码里不会有任何地方 require 那个名字,这行 + 只是授权命名空间里一个无人引用的授予目标。这一对标志就是 #5086 的 CODE-ONLY + 声明,`saveMetaItem` 在**任何** kernel 上都以 403 `not_creatable` 拒绝,并从条 + 目自己的 `filePatterns[0]` 读回「该去哪儿声明」。`supportsOverlay: false`—— + capability 只是名字/标签/scope,没有 merge 语义,而允许租户 overlay 一个包发布 + 的声明等于允许把 `scope` 从 `org` 抬成 `platform`。`loadOrder: 12` 早于 + `permission`/`position`(15),使权限集的 `systemPermissions` 解析时 capability + 已经存在。 + - **`MetadataTypeSchema` 枚举补 `'capability'`**。 + - **`CapabilityDeclarationSchema` 声明 ADR-0010 保护信封并收紧为 `.strict()`**。 + 信封是必须的:loader 对每个已注册类型都调 `applyProtection`,不声明就会 422 掉 + loader 自己的输出(#4001 在 `permission`/`position` 上补过同一个洞)。收紧则与 + `api` 不同——`ApiEndpointSchema` 同时是**存储行**的解析器,所以它留在 + `STILL_STRIP`;而没有任何地方拿这个 schema 重新解析 `sys_capability` 行 + (`bootstrapDeclaredCapabilities` 通过 `capabilityRowFields` 按名读字段), + 所以收紧零成本,买到的是一个授权面本就该有的 declared = enforced 姿态。 + 改用 `strictObject` 书写,已知键从 shape 派生,不新增手抄键表。 + + **包声明通道完全没动。** `AppPlugin` 通过 `registerInMemory` 注册 stack 的 + `capabilities[]`,文件系统 loader 按 `filePatterns` glob——两条都不经过 + `saveMetaItem`,所以 `bootstrapDeclaredCapabilities` 依旧照常 seed。 + `OS_METADATA_WRITABLE=capability` 仍是 ADR-0005 那唯一一道运维逃生门,而在它后面 + 写入现在由 `CapabilityDeclarationSchema` 判定(422),不再原样落盘。 + + ⛔ `role` / `profile` / `policy` **不搭车**:它们没有 `PLURAL_TO_SINGULAR` 映射、 + 没有声明 schema、没有读回接缝,是另一个问题,另开单。这条以断言形式钉在 + `capability-metadata-kind.test.ts` 里,因为「capability 有了条目,邻居也该有」 + 正是下一个显而易见却错误的改动。 + +- 5b4780b: fix(spec): `$gt`/`$gte`/`$lt`/`$lte` accept the ISO string the platform itself produces (#5685) + + The four ordering-comparison slots declared `number | Date | FieldReference` — + and the platform's own producers put a **string** in them and nothing else. The + declaration did not merely under-describe reality, it contradicted it: + + - `resolveFilterTokens` (`@objectstack/core`) is the evaluator for the `{token}` + grammar and **every** branch returns a string — `asYmd(…)` for a calendar day, + `.toISOString()` for the sub-day tokens. Its own module example is exactly + this shape: `{ close_date: { $gte: '{current_year_start}' } }` becomes + `{ close_date: { $gte: '2026-01-01' } }`. + - `date-macros.zod.ts` states the same rule from the other end: "the DRIVER only + ever sees ISO date / timestamp strings, never `{tokens}`". + - Three first-party callers send strings today — `lifecycle-service`'s retention + cutoffs, `plugin-email`'s outbox sweep, and `plugin-auth`'s better-auth + adapter. + + An author — an AI author in particular — reading `number | Date` concluded that + a date window must be a `Date` object or an epoch number, which is the one form + the date-macro path can never hand them. + + **This is additive and declaration-side only.** No producer, caller or driver + changed. Every evaluation surface already compared strings: `driver-sql` binds + `>`/`>=`/`<`/`<=`, and `formula`'s `matchesFilter` and `driver-memory`'s matcher + fall through to the JS operators. Filters that validated before still validate. + + Widened in all three places this contract is spelled: `ComparisonOperatorSchema` + (documentation), `FieldOperatorsSchema` (the copy `NormalizedFilterSchema` + validates against and `FieldOperators` is inferred from), and the `Filter` + TypeScript helper — where `T` is known, so it stays type-precise: a `Date` field + now also takes the resolver's ISO string, a `string` field (a `Field.time` + `'09:00'`, an autonumber code) is orderable instead of collapsing to `never`, + and a `number` field stays numbers-only. + + **The comparand form the contract guarantees** is the ISO/clock one — an ISO + calendar day (`YYYY-MM-DD`), a UTC ISO-8601 instant, or a wall-clock time of day + (`HH:MM[:SS[.fff]]`). Those are ASCII and fixed-width, so lexicographic order IS + chronological order and every backend agrees. The union is a bare `string` + rather than an ISO refinement because this schema is field-agnostic (it never + sees which column the operator applies to) and because an ISO refinement would + reject `Field.time`'s declared `HH:MM` form, which `SqlDriver.temporalFilterValue` + canonicalises in the comparand position. Ordering **non-temporal** text is + therefore permitted but not promised: the order is the backend collation's + (byte-wise on SQLite, the database locale on Postgres, UTF-16 code units in the + JS matchers), and those coincide only for ASCII. The `.describe()` on each slot + says so. + +- 84c86fb: feat(spec): `preview` / `trial` 的 discovery 折叠改为显式声明,并让折叠表对 EnvironmentType 穷尽 (#6287) + + `EnvironmentTypeSchema` 有七个成员,而 `NODE_ENV_TO_DISCOVERY_ENVIRONMENT` + (`api/discovery.zod.ts`)只为其中五个写了条目。`preview` 与 `trial` + 一直是靠 `resolveDiscoveryEnvironment` 末行的 `?? 'development'` + 兜底落到 `development` 的 —— 不是一条被写下来的决定,而是掉出表尾的副作用。 + 这张表的注释本来就写明它是给后来者读的,读表的人会以为它是全的。 + + ## 行为变化(唯一一处,消费者可见) + + `resolveDiscoveryEnvironment` 对两个输入的返回值改变: + + | `NODE_ENV`(或任何 operator 提供的字符串) | 之前(兜底) | 现在(声明) | + | :--------------------------------------- | :------------ | :--------- | + | `preview` | `development` | `sandbox` | + | `trial` | `development` | `sandbox` | + + `/discovery` 的 `environment` 字段是机器可读面,客户端读它回答「我是不是在跟生产说话」, + 并据此决定要不要放宽破坏性操作的二次确认。折向 `sandbox` 的三条理由: + + 1. **它们在本仓语义里是什么。** 本仓的 environment 是被开通的运行容器 —— 独立数据库、 + 规范主机名、套餐档位、按环境的 RBAC(`cloud/environment.zod.ts`)。`preview` / + `trial` 是这种东西,不是 `development` / `dev` / `test` 所描述的开发机与 CI 的一次性运行; + `sandbox` 正是这个枚举里「已开通的准生产」那一档。 + 2. **姿态按收紧方向取。** `trial` 尤其装着评估中客户的真实业务数据,答 `development` + 是**低报**姿态 —— 与 #5673 / #5936 把 unset 一行翻成 `production` 所要避免的是同一类错误, + 只是低一档。两者都不是 `production`:它们按定义就不是客户的生产部署,报 `production` + 会让这个字段唯一要回答的问题朝另一个方向答错。 + 3. **它保住了作者的区分。** 把环境标成 `preview` 的人手里本来就有 `development` 和 `test` + 而没有选;折到 `development` 会把这个选择携带的唯一信息抹平。 + + 其余五行、unset → `production`(#5673 / #5936)、未识别拼写 → `development`(#4828) + 三条规则一概未动。 + + ## 漏补条目从此不编译 + + 折叠表拆成两张:声明面 `Record`(七个成员,穷尽), + 与 operator 便利拼写 `prod` / `dev`(不属于词表,单列以免污染穷尽标注),合并后仍是原来那张查找表。 + 给 `EnvironmentTypeSchema` 加一个桶而不说它折向哪里,现在直接**编译不过**。 + + 这是**编译期**而非运行期断言,因为运行期断言看不见这个缺陷:兜底与三条已声明的行都产出 + `'development'`,所以调 `resolveDiscoveryEnvironment` 得到的答案在「有条目」与「`??` 现编」 + 两种情况下完全一致 —— 一条运行期穷尽测试在 #6287 报告的那个坏状态下本来就是绿的。 + + `?? 'development'` 兜底保留,职责收窄为它真正服务的那一类:既不是词表成员、也不是 + operator 简写的任意字符串(`qa`、`uat`、拼错),`NODE_ENV` 是 operator 提供的自由文本, + 这一类是真实输入,把它降级到 `development` 正是「猜测不得声称 production」。 + +- a80302a: fix(spec): `functions: { fn: { handler, effect: 'writes' } }` survives `objectstack build` (#4976) + + `FlowFunctionEntrySchema` gains a fourth union member — the **lowered + declaration**, a `functions` entry whose `handler` has been replaced by the + string ref `objectstack build` emits: + + ``` + functions: { + sweepProjectHealth: { handler: 'sweepProjectHealth', effect: 'writes' }, + } + ``` + + Nothing an author writes changes. This shape is produced by the CLI, not typed + by a person: `lowerCallables` replaces every inline callable with a serialisable + ref before the stack is parsed (it must — `z.function()` wraps callables and + would break the ref mapping), and since #4396 it keeps the declaration beside + the ref so what a function said about itself survives into the artifact. The + union was not extended in that change, so the artifact it started emitting was + rejected by the very schema it had to pass: + + ``` + ✗ Validation failed + + functions: + ✗ functions + invalid_union: Invalid input + ``` + + Loading from source was unaffected — `objectstack dev`, `objectstack validate` + and the test suite all passed — so the failure appeared only at build, on the + one spelling the platform asks writers to use. That is the same asymmetry #4343 + fixed for the bare handler ref, one shape over. + + **Why this was worse than a failed build.** `effect: 'writes'` exists so a + function that writes is not counted as having written nothing (#4396, #4354): a + `script` step reports no record metrics _because_ flow functions are + contractually pure, and a declared writer instead reports `unmeasuredEffect` so + the run's broken-sweep query (`selected > 0 AND acted = 0 AND unmeasured = 0`) + stays off it. The error above names no key, no entry and no reason, so the + practical repair an author reaches for is deleting the declaration — shipping an + undeclared writer, which is exactly the state it exists to prevent, recorded + permanently in `sys_automation_run`. + + **One behaviour change worth stating.** `{ handler: 'someName' }` written by + hand now parses where it used to be rejected as "handler is not callable". The + rejection could not survive this member and should not have: a bare string entry + (`functions: { foo: 'foo' }`) has been accepted since #4343 with the caveat that + it registers nothing, so refusing the record spelling of the same mistake while + accepting the string spelling was two dialects for one contract. Both fail the + same way, loudly, at execute: `no function named '…' is registered` (#1870). + Everything else stays strict — the lowered member is _derived_ from the authored + declaration rather than re-typed beside it, so `{ handler: 'fn', efect: 'writes' }` + still raises the named surface and the `` `efect` → `effect` `` prescription, an + unknown `effect` value is still refused, and an empty ref is still not a name. + + **Runtime is unchanged and was already correct.** `normalizeFlowFunctionEntry` + returns `undefined` for a lowered entry in both its shapes, because neither + carries a callable; `mergeRuntimeModule` re-attaches the sidecar module's + function to the declaration the JSON carried _before_ any collector runs, so + `effect` reaches `collectBundleFunctionEntries` intact on the built path. + + The two halves are now pinned against each other by a round-trip test that + drives the real pipeline (`defineStack` → `normalizeStackInput` → + `lowerCallables` → parse) instead of a hand-written sample of what the lowering + is believed to emit — the crossing neither side previously made, which is why + both stayed green while the build failed on the join. + +- 4d552af: feat(spec)!: `FlowNodeSchema` parses its own ADR-0031 regions — the post-parse pass retires (#4415) + + `FlowSchema.parse` normalized a flow's own `nodes[]` / `edges[]` but could not reach a + **region**, because a region lives inside `FlowNodeSchema.config` — a deliberately open + `z.record` (ADR-0018). #4381 closed the resulting gap with a **post-parse pass**, + `normalizeControlFlowRegions`, that every caller had to remember to run: + + ```ts + const flowShell = FlowSchema.parse(converted); + validateControlFlow(flowShell); + const parsed = normalizeControlFlowRegions(flowShell); // ← had to remember + ``` + + That is an unwritten rule on top of a parse, and it is exactly the condition the #4347 + family of defects grows in: a new consumer — a Studio publish path, an MCP tool, a bulk + validation script — takes a `FlowParsed` and uses it, holding a **half-parsed flow that + looks finished**. Nested edge predicates were still bare strings, nested nodes had not been + through `.strict()`, and nothing said so. + + Now the schema does it. `FlowNodeSchema` carries a `.transform()` that parses each declared + region slot — `loop.config.body`, `parallel.config.branches[]`, `try_catch.config.try` / + `.catch` — through the schema that slot's value _is_. Nesting needs no manual recursion: a + region's `nodes` are `z.array(FlowNodeSchema)`, so Zod re-enters the transform on the way + down. **"Parsed" now means parsed at every depth** (Prime Directive #1), from any entry + point — including `FlowNodeSchema.parse(node)` on a single node, which the old whole-flow + pass could not serve at all. + + ## Migration + + **`normalizeControlFlowRegions` is removed from `@objectstack/spec/automation`.** Delete the + call; the parse above it already did the work: + + ```diff + const parsed = FlowSchema.parse(converted); + validateControlFlow(parsed); + - const normalized = normalizeControlFlowRegions(parsed); + ``` + + Its replacement, `parseFlowNodeRegions(node)`, is exported for the same purpose one node at + a time, but you should not normally need it — it is the transform's own body. + + **`FlowNodeSchema` is now a `ZodPipe`, not a `ZodObject`,** so it no longer has `.shape` / + `.extend()` / `.pick()`. `z.infer` / `z.input` / `.parse` / `.safeParse` and + `z.toJSONSchema` are unaffected, and the authorable key set is byte-identical (verified by + `check:authorable-surface`). If you were reaching for the object half, read it from the + pipe's input side — `FlowNodeSchema.def.in` — which is also what the repo's own generators + do (`pipeAuthorableSide` in `scripts/lib/zod-graph.ts`). + + One visible consequence in the generated reference: `content/docs/references/automation/flow.mdx` + now renders FlowNode's **input** shape, so keys carrying a `.default()` (`boundaryConfig.interrupting`, + `inputSchema[].required`) show as optional. That is what an author actually writes, which is + what an authoring reference should say. + +- 69787f0: A hook registration can now express "global, EXCEPT these objects" — `registerHook(event, handler, { excludeObjects })`. + + `registerHook` carried one scope face: `object`, an allow list (absent = global, `'*'` = every object). An allow list and a deny list are interchangeable only over a closed universe of object names, and this one is open — a successful `/meta` PUT registers new objects into a running engine, and `SchemaRegistry.registerObject` emits no event a plugin could subscribe to. So a registrant wanting "everything except these platform tables" had two options, both wrong: keep the skip list inside the handler as an early return, which leaves the registration global and makes the per-object gates (`hasHooksFor`, the bulk-write row-set read) answer "hooks apply" for objects the handler is about to skip; or enumerate the complement into `object`, which freezes the list at boot so an object created afterwards is silently not covered — a compliance regression for the audit plugin, and a silent one. + + `excludeObjects?: string | string[]` is the deny half, subtracted from whatever `object` admits: `matches = allowMatches && !excludeMatches`. Absent means subtract nothing, so every registration that compiled before still behaves identically. Declared on the registration rather than left to a predicate callback, so the scope stays static, printable — the `Registered hook` debug record now reports it — and introspectable by diagnostics. + + Two shapes are refused at registration, following the same reasoning as the empty-target ruling: an empty name (`''`, `['']`, or a blank member) would subtract nothing while reading as though it subtracted something, and `'*'` would subtract every object and leave a hook that can never fire (ADR-0078: no silently inert declaration). Both throw, naming the fix. `excludeObjects: []` is accepted — it is the honest spelling of "subtract nothing", and the natural value of a spread whose source list is empty. + + `triggerHooks` (dispatch) and `hasHooksFor` (the bulk-write gate) were two hand-written copies of one matching semantic; adding a second scope dimension to two copies is how they drift, so both now call one shared matcher. A property test pins the direction that matters — the gate is never tighter than the dispatch, since a looser gate costs a wasted query while a tighter one silently drops hooks that were going to fire. + + The authorable `HookSchema` is deliberately untouched: the consumer is plugin code registering in TypeScript, and no metadata author needs "global minus a list" today. The key stays off the authoring surface until real pull appears. + +- a36db28: i18n label contract: inline locale maps are authorized, and filter-preset tabs get a translation slot + + **`I18nLabelSchema` accepts two forms, not one (#5728).** It declared a plain + `z.string()` while three published platform pages authored 31 inline + `{ en, 'zh-CN', 'ja-JP', 'es-ES' }` maps that objectui resolves through + `pickLocalized` — so the authoritative document was the wrong one, and the + #5068 component-props gate reported 42 findings against the platform's own + pages. The schema is now a union of the default-language string and an inline + locale map. `ElementTextPropsSchema.content` was declared a bare `z.string()` + and therefore out of that union's reach; it moves onto `I18nLabelSchema` in the + same change, which is the other 8 of the 42. The gate now reports **0**. + + This does not reverse #4667 / #5055. What those retired was the _key-reference_ + dialect (`{ key, defaultValue }`) — a shape with **no resolver**, whose label + reached the screen as a raw key or not at all. What is authorized here is the + inline locale map, which has a live resolver and which the CLI's `i18n-extract` + already understands. Same "declared = enforced" principle, applied in both + directions: the map's keys are constrained to BCP-47 tags (plus `default`), so + `{ key, defaultValue }` stays a parse error rather than becoming "a locale map + whose locales are named `key` and `defaultValue`". + + Zero breaking: every previously-valid label is still valid. The + translation-bundle channel remains the direction that scales and is unchanged. + + **Filter-preset tab labels are translatable (#5377).** `ObjectTranslationData` + gains `_tabs`, addressed by `ViewTabSchema.name`, and `resolveTabLabel` reads + it — explicit `_tabs` translation, then the referenced view's `_views.*.label` + for a tab that carries `view` (the path that already worked, preserved), then + the authored literal. A tab carrying only a `filter` referenced nothing to + inherit from and had no key of its own, so its label rendered in the source + language above a fully localized grid with no authoring workaround. `os i18n +extract` scaffolds the new keys, so the slot, the resolver and the extractor + land together. + + `I18nLabelSchema`'s description no longer claims "i18n keys are auto-generated + by the framework" — none are. `AriaPropsSchema.ariaLabel` now states that no + translation-bundle slot addresses it. + +- 2f59da0: fix(spec): `webhook` / `connector` / `sharing_rule` are validated at the `/meta` write door (#6245) + + Three stack collections could be written through `PUT /api/v1/meta//:name` + with **zero validation**. A spec-invalid webhook body was stored verbatim and + answered `success: true` — the repo pinned that behaviour itself + (`protocol-meta.test.ts`, which saved `{ name, url, events: ['x.created'] }` and + asserted success, where `events` is an alias of `triggers` and `'x.created'` is + not a `WebhookTriggerType`; that webhook subscribed to nothing). + + This is the hole #5271 closed for `api`, arriving through three more doors: + the kinds are produced and consumed today — artifact ingest maps + `defineStack({ webhooks, connectors, sharingRules })` onto items of exactly + these type names — but none is a member of `MetadataTypeSchema`, so + `getMetadataTypeSchema()` returned `undefined`, `resolveOverlaySchema()` + returned `null`, and `saveMetaItem` took its documented "unregistered type → + store without validation" branch. Enforced but undeclared. + + **FROM** `PUT /meta/webhook/my_hook` with any JSON → `200 { success: true }`, + stored unvalidated. + **TO** a malformed body → `422 INVALID_METADATA` with structured `issues[]`, + the same envelope every other kind already returned. A well-formed body is + accepted exactly as before. + + Each type binds the **same schema its stack collection is validated against**, + so no body can be legal in a stack and illegal through `/meta` or the reverse: + `WebhookSchema`, `DeclarativeConnectorEntrySchema`, `SharingRuleSchema`. + `connector` binds the _declarative entry_ schema rather than the bare + `ConnectorSchema` deliberately — the entry schema carries the ADR-0097 §3/§5 + rules (a provider-bound instance may not inline credentials via + `authentication`, nor author `actions`/`triggers`), and binding the base would + have left the inline-secret shape a stack refuses reachable through `/meta`, + which is this very bug class wearing a different key. + + **No new capability surface.** These are bound for shape validation only: no + `MetadataTypeSchema` member, no `DEFAULT_METADATA_TYPE_REGISTRY` entry, so + every authorization verdict keeps taking the identical "no static entry ⇒ + synthesised `allowRuntimeCreate: true`" branch. The write _door_ is unchanged; + only the 422 is new. #2657's B/C decision on whether these should become kinds + is untouched and unprejudged. + + Graded **minor**, following #5271: a write that previously returned 200 can now + return 422. Nothing well-formed changes behaviour, but a caller relying on the + API accepting malformed bodies will see the difference. + + **One schema change rides along, and it is load-bearing.** + `CriteriaSharingRule` / `SharingRule` now declare the ADR-0010 protection + envelope (`_lock`, `_lockReason`, `_lockSource`, `_lockDocsUrl`, `_packageId`, + `_packageVersion`, `_provenance`). Both metadata load paths call + `applyProtection` on **every** type, so a package-loaded sharing rule already + carries those keys — and this shape is `.strict()`, so it did not drop them, it + _rejected_ them. That was invisible only while the type resolved no schema at + the overlay door. Binding the door without this spread would have aimed the new + 422 at the runtime's own stamp instead of at malformed author input. The + existing guard in `metadata-type-schemas.test.ts` names this failure exactly and + prescribes this fix. Additive and internal-only — no authored field changes. + +- d7e0b42: feat(spec): share-link enforcement takes the full `ExecutionContext`; the narrow context is route-401 only (#6430, #6206 ruling A) + + `IShareLinkService.createLink` / `revokeLink` / `listLinks` now declare their + context parameter as the complete `ExecutionContext` envelope instead of the + five-field `ShareLinkExecutionContext`. All three ADJUDICATE access — the + [Finding-2] visibility re-read on create, the ADR-0111 D8 share-manager probe + on revoke, the context-scoped listing — so each needs the whole + `resolveAuthzContext` result, `accessible_org_ids` / `org_user_ids` / + `systemPermissions` / `posture` / `tabPermissions` included. + + The measured failure behind the ruling: the share-link route assembled exactly + those five fields and handed the result straight to `engine.find` as the + enforcement context. Under the `group` tenancy posture `accessible_org_ids` IS + the Layer 0 wall (ADR-0105 D2) and an absent set denies, so link creation + returned a blanket 403 on a posture that ships. Fail-closed, not a leak — but a + trimmed envelope feeding enforcement is a bypass-shaped pattern, and ADR-0095 + D2 already rules that posture is resolved once and carried, never re-derived at + the enforcement site. This was the third assembly site of that family (#5997, + #6071), so the contract converges on the whole envelope rather than keeping a + per-site subset. + + `ShareLinkExecutionContext` is retained and unchanged in shape — it is the + route's own "authenticated or 401?" vocabulary — with TSDoc that now states the + boundary and why TypeScript cannot enforce it (structural subtyping accepts a + narrow object wherever the wide type is expected, so the declared parameter + type plus the caller's obligation are what hold the line). + + Contract-only, no runtime behaviour change here: existing implementations keep + compiling (method parameters are bivariant), and the `@objectstack/plugin-sharing` + consumer that actually threads the envelope through is the follow-up half + tracked on #6206. + +- 3510e4a: refactor(spec,drivers,lint): one implementation of the filter identity reduction (#5659) + + `{ $and: [] }` matches every row, `{ $or: [] }` matches none, `{}` is a TRUE + disjunct that absorbs its `$or`, `{ $not: {} }` is FALSE. That is a ruling + (#5322/#5134) pinned for every backend by the four identity cases in + `FILTER_LOGIC_CASES` — and it was implemented four times over: `reduceFilterNode` + in `driver-sql`, the same function again in `driver-mongodb`, the + `every`/`some`/truthiness algebra of `driver-memory`'s matcher, and nearly a + fifth hand-written copy inside `@objectstack/lint`, which declined to write one + and filed this issue instead. + + **New in `@objectstack/spec` (`@objectstack/spec/data`): `reduceFilterVerdict`**, + beside the case table that proves it. It answers `'true' | 'false' | 'clause'` + for a filter node and never throws on its own; each backend's own refusals — the + undeclared `$`-combinator and the `undefined` comparand in `driver-sql`, the + query-level keys and the `$null` comparand in `driver-mongodb` — are passed in as + `FilterVerdictHooks` and are invoked from exactly the positions they were invoked + from before. `reduceFilterKeyVerdict` answers the same question for one key, which + is what both SQL and MongoDB emitters consult while walking a node. + + **No behaviour changes in the three drivers.** The move is mechanical: the shared + algebra replaces each private copy, the refusals stay where they were, and the + `FILTER_LOGIC_CASES` conformance suites are green on both sides of the change — + including the SQL-inheriting `driver-sqlite-wasm` and `driver-turso`. + + **`@objectstack/lint` gains two warnings it was structurally blind to.** The + `multi: true` unbounded-bulk-write rule (#5482) asked "does this filter have zero + keys", so a `delete_record` bounded by `filter: { $and: [] }` or + `filter: { $or: [{}] }` — a whole-object write by the ruling every driver executes + — passed silently. It now asks the reduction, and it warns about both while + staying quiet on `{ $or: [] }` and `{ $not: {} }`, which match nothing. The + message names the shape it saw (`a filter that REDUCES TO TRUE ({"$and":[]})`) + rather than calling a non-empty filter "empty". + + If you have a flow declaring a bulk write bounded by one of those two shapes, the + lint will now tell you so — the write was already unbounded at run time; only the + feedback is new. + +- 54299ca: feat(sharing): `ISharingService` 的每行写判定补三态 —— 放行 / 不表态 / 拒绝(#6428) + + #5492 的维护者裁决(2026-08-07,B 案)分两步兑现两种已声明的写扩权,本次是 **step 1: + 契约与默认实现**。plugin-security 前像门的 provenance 分层合成是 step 2,本次一行未动。 + + **为什么二态不够(实测,不是推演)。** `canEdit()` 用同一个 `true` 表达了两件事 —— + 「我有依据放行」与「本服务对这一行根本不设门」。对只**追加**一道门的调用方(sharing + 中间件、`sys_attachment` 父记录门、ADR-0055 master 判定)这没问题:`true` = 「我不拦 + 你」。对让这个答案去**顶替另一个权威的地板**的调用方就是 fail-open —— #5492 的 E2 实验 + 把前像写门委托给 `canEdit()` 后,在**没有 `owner_id` 列**的对象上,普通成员跨 creator + 的 UPDATE 变成 `ok: true`(main 上是 403),因为平台的 `created_by` 所有权地板正是这类 + 对象唯一的行级写门,而一个「不表态」的 `true` 把它盖掉了。 + + **新增契约面**(`@objectstack/spec/contracts`): + + - `SharingWriteVerdict = 'allow' | 'abstain' | 'deny'` —— 闭合联合,普通 TS 类型 + (非 zod 派生,不进 ADR-0122 的 pin 计数)。 + - `ISharingService.checkEdit()` / `checkDelete()` —— 三态主形态,动作边界照 ADR-0111 D3 + 继承:`edit` 级共享让 `checkEdit` 答 `allow`、同一行 `checkDelete` 仍答 `deny`;两者 + 的 `abstain` 集合完全相同(两道门对「哪些对象由共享设门」意见一致,只在动词上分歧)。 + + **兼容:`canEdit()` / `canDelete()` 原样保留,语义零漂移。** 它们被定义为三态的 + **投影** `verdict !== 'deny'` —— 从前对 public / 无 owner 字段 / bypass 对象返回的那个 + `true`,现在落在 `abstain` 上,投影回来仍是 `true`。真值表逐分支被测试钉住(9 个分支 + × 两个动词),因为 `resolveSharingCanEdit`(plugin-security)与 `sys_attachment` 父记录 + 门读的正是这一列,翻掉任何一格都是本 PR 未触及的包里的静默权限变更。 + + **fail-closed 落点:查询失败是 `deny`,永远不是 `abstain`。** 两者对合成方是相反的指令 + (`abstain` 把这一行交给另一个权威,`deny` 就地终结),把失败读成「没有意见」正是造出上述 + fail-open 的那个混淆。默认实现把所有权查询与共享查询整段包在 fail-closed 分支里,并 + `logger.error` 记名,不静默吞。 + + **行为变化(一处,方向收紧)**:引擎查询抛错时,`canEdit`/`canDelete` 从**向外抛**改为 + 返回 `false`。两个既有调用点本来就在自己那侧 catch 成 `false`(`resolveSharingCanEdit` + 的 #5386 fail-closed、attachment hook 的降级读),所以对它们是同一结果;其余调用点由 + 「异常中止写入」变成「403 拒绝写入」,严格不更宽松。 + + **解锁**:#5492 step 2 的前像门可以按 provenance 分层合成 —— `abstain` 回落平台所有权 + 地板、`allow` 按声明顶替地板、`deny` 维持拒绝 —— 而不必在 security 侧重算一份 + owner/depth/share/bypass(那会是同一契约的第二份实现)。#5491 与 #5492 同批落地。 + +- 2fdb36e: feat(spec): SettingsManifest specifiers can declare a standard `valueDomain` (#5933) + + `SpecifierSchema`'s value constraints were `options` / `pattern` / `min` / `max` / + `minLength` / `maxLength`, and none of them can express "the legal values here are + whatever the published standard says". `pattern` constrains the _shape_ of a string, + so `^[A-Za-z]{2}$` admits `ZZ` and `Mars/Olympus` is a shape-valid time zone that does + not exist; `options` is exhaustive (#5131), so completing it would mean checking a + 600-entry tzdb table into a manifest and re-checking it every tzdb release. The + `localization` manifest hits this on three keys at once — `timezone`, `currency`, + `default_country` — and `company.country` carries the same two-letter pattern with the + same hole. + + **New optional key: `specifier.valueDomain`**, a closed enum with three members: + + - `iana_time_zone` + - `iso_4217_currency` + - `iso_3166_alpha2` + + Declaring it moves the enforcement boundary: the standard's membership becomes what a + write is judged against, and `options` degrades to a **UI convenience list** — a curated + dropdown of values worth suggesting, no longer an exhaustive statement of what is legal. + A value outside `options` but inside the domain is accepted. + + **Nothing changes when it is absent.** `options` stays exhaustive and the save path keeps + rejecting anything the table does not list, which is the right shape for tables the + platform itself backs (`mail.provider`, `sms.provider`) where "legal" means "this + deployment ships an adapter for it". `pattern` / `minLength` / `maxLength` still apply + alongside a domain and still narrow — shape and membership are independent, and a value + must satisfy both. + + The **enforcement** is not in this release. `packages/spec` declares the domain and + nothing more (Prime Directive #2); the write-path check lands in `service-settings` + (#5712, blocked on this). What ships here so both halves agree is the _definition of + membership_ for each domain, pinned by tests rather than left to prose, because for two + of the three the obvious oracle is the wrong one: + + - `iana_time_zone` is the `Intl.DateTimeFormat` probe, not + `Intl.supportedValuesOf('timeZone')` — measured on the Node 22 baseline, that list + holds 418 CLDR canonical names and omits `UTC` (this platform's own declared default) + and `Asia/Kolkata` (a value the shipped localization manifest curates), carrying the + latter only under the legacy spelling `Asia/Calcutta`. + - `iso_4217_currency` **is** `Intl.supportedValuesOf('currency')` — 162 entries, + admitting `CHF` and all nine curated options while rejecting `XYZ`. + - `iso_3166_alpha2` has no standard-library oracle at all: + `Intl.DisplayNames(…, { type: 'region' }).of()` returns a distinct name for `ZZ` + ("Unknown Region", the exact value this domain exists to reject) and for `UK` (a CLDR + alias that is not an ISO 3166-1 code), so the enforcing side must carry an explicit + code list. + + `bcp47_locale`, the fourth member the proposal listed, is deliberately **not** in the + vocabulary. Its only candidate key is `localization.locale`, whose options are exactly + the shipped message catalogs — a registry-backed table, so a domain there would loosen + it and admit locales with no catalog. And BCP-47 has no membership registry to enforce + against (`Intl.getCanonicalLocales('xx-YY')` succeeds), so the "domain" would only + re-check syntax — the weakness `pattern` already has and this key exists to fix. + +- eb7613c: feat(spec): page component copy is translatable — `pages..components.` (#6080) + + A page's cards, KPI blocks, pickers and forms had **no translation key at all**. + Not a drifted key — no key: `pages` was a `.strict()` four-key record whose + `title`/`subtitle` mean the page's `page:header`, so every other component's + user-visible string reached the user as whatever literal the `*.page.ts` author + typed, in every locale, and `.strict()` (correctly) refused the keys a + translator invented. + + The asymmetry was the giveaway: `dashboards..widgets.` has + carried `title`/`description` all along, and a page's components have stable + `id`s exactly like a widget does. Downstream, hotcrm's `sales_home_page` — the + `isDefault` landing page for sales reps — rendered a translated header above + four English cards and four English KPI blocks in zh/ja/es (12 strings across 8 + pages). + + ```ts + pages: { + sales_home_page: { + label: '销售看板', + components: { + quick_create: { title: '快速新建' }, + kpi_revenue_won: { label: '已赢收入' }, + ai_briefing: { title: '询问 AI 助手', description: '从右侧边缘打开助手面板。' }, + }, + }, + } + ``` + + **Declared AND resolved in the same change.** `translatePage` + (`system/i18n-resolver.ts`) overlays the entry onto the component's + `properties`, so the face is not a declaration waiting for a reader. + + **The key face is measured against `ComponentPropsMap`, not mirrored from the + issue's sketch** — `title`, `description`, `label`, `placeholder`, `emptyText`, + `submitLabel`, each one a copy prop some component actually declares as a plain + string with no inline `{en, zh}` form, i.e. one whose only localization route is + this bundle. Two deliberate exclusions: + + - **`help` is not declared.** No component in the model has it; it would parse + clean and translate nothing (ADR-0078). It is an alias onto `description`. + - **`subtitle` is not declared.** `page:header` is its only declarer and is + addressed by page name, so a per-component `subtitle` would give one string + two spellings — which is how this asymmetry started. + + Resolution rules, all tested: `label` lands on the component's own top-level + `label` when it declares one and in `properties.label` otherwise (copy goes + where the author wrote it); keys resolve **individually** across the locale + chain, so a partially-translated `zh` entry still falls back to `en` per key; + and the id-addressed route beats the page-name route wherever both could apply + (a `page:header` that does carry an `id`). + + Purely additive and `.strict()` is unchanged — `components` is optional, every + previously-valid bundle still parses, and every previously-rejected key is still + rejected. + +- 1818998: feat(spec,objectql,metadata-protocol): validate-only data operation — ask for the write's verdict instead of predicting it (#6037, #4633 ruling D) + + `import`'s dry run predicted the write path's verdict with a hand-copied mirror + of the engine's rules (`rest/src/import-coerce.ts`). A copy cannot structurally + keep up with the family it mirrors — ADR-0104 value shapes, `format` checks, + object-level `validations`, the state machine — so ruling D replaces prediction + with the verdict itself. + + **New:** `DataProtocol.validateData(request)` returns the write path's verdict + for candidate rows and persists nothing. + + ```ts + const verdict = await protocol.validateData({ + object: "lead", + mode: "insert", // or 'update', which judges only supplied keys + data: [{ first_name: "John", email: "not-an-email" }], + }); + // → { valid: false, + // results: [{ valid: false, errors: [{ field: 'email', code: 'invalid_email', … }], warnings: [] }], + // posture: { valueShapeStrict: true, mediaValueShapeStrict: false } } + ``` + + **Declaration and execution land together, deliberately.** `engine.validate()` + (objectql) calls the same `validateRecord` / `evaluateValidationRules` that + `insert()` calls, and `metadata-protocol` implements `validateData` on top of + it. Agreement between preview and write is therefore guaranteed by + construction, and a test asserts it directly by running both against one engine + in both postures. This is the ruling's own clause, not a style choice: + `BatchOptions.validateOnly` was retired in #4052 as a flag that promised a dry + run while the batch surfaces persisted regardless, so a caller previewing a + mutation had it EXECUTED. The new operation avoids that spelling too — the + tombstone still stands and still rejects `validateOnly`. + + **The verdict is the target deployment's, not an absolute.** The response + carries the ADR-0104 `posture` it was reached under. On a self-certified + deployment a bad value shape is an error; on a warn-first one the same row is + valid and the finding appears in `warnings` with the same `code` — one finding + that changed buckets, not two vocabularies. An unconditionally-strict preview + was considered and rejected (#4633 option B): it would fail rows on every + un-migrated deployment that the write would have accepted, which teaches + authors to distrust the one gate in front of a bulk import. + + Two boundaries worth knowing, both deliberate and both documented at the + implementation: + + - **No hooks run.** `beforeInsert` fires before validation on the real path, so + a hook deriving a _business_ field could change a verdict this does not + simulate. Firing arbitrary user hooks in a preview — mail, outbound calls, + writes to other objects — is the #4052 defect in a new spelling, so the gap is + documented rather than closed. Audit/ownership stamps are `system`/`readonly` + and validation skips them regardless. + - **Warn-first admissions are not recorded as certification evidence.** The + `#4769` sink exists so a boot cannot certify a contract it has just written + against; a preview writes nothing, so recording there would let a _preview_ + block a later migration. + + Additive: `validateData` is optional on `DataProtocol`, and nothing existing + changes shape. `valueShapeStrictEffective` / `mediaStrictEffective` are now + exported from objectql's record validator so the response reports the posture + that actually decided the verdict rather than the raw deployment flag. + + Unblocks #4633's consumption half (rest/import adopting the operation and + retiring the `import-coerce.ts` mirror). + +### Patch Changes + +- c2429b0: fix(spec): action-param rejection names the built-in a "differs by one underscore" key meant + + `validateActionParams` (ADR-0104 D2) rejected every undeclared key with the + same sentence — `Unknown action param "selectedIds" — not declared on this +action` — including keys one leading underscore away from a built-in + (`ACTION_PARAM_BUILTIN_KEYS`: `recordId` / `objectName` / `_selectedIds`). + That sentence is true, and its only actionable reading is false: the reader's + next step is to declare the key on the action, and a built-in is precisely the + key that **cannot** be declared. #5568's reporter walked that road to its end + on `params.selectedIds`, concluded that REST carried no legal shape for a bulk + selection at all, and opened a platform issue — while `params._selectedIds` + was live the whole time. + + The `unknown_field` message now appends a near-miss hint when `'_' + key` or + `key` minus its leading underscore is in the allowed built-in set: + + ``` + Unknown action param "selectedIds" — not declared on this action. Did you mean + the built-in "_selectedIds"? Built-in params are never declared on an action — + an aggregate bulk dispatch (`execution: 'aggregate'`) injects every selected + record id under it, and a handler reads `ctx.params._selectedIds`. + ``` + + The origin sentence is per built-in, because the three have three different + producers: `recordId` / `objectName` are merged into the bag server-side by the + dispatcher, `_selectedIds` arrives from the renderer's aggregate bulk dispatch. + A key reached through a custom `builtinKeys` override gets the generic + "the dispatcher supplies it". + + **Message copy only — the verdict does not move.** The key is rejected before + and after, the accepted set is unchanged, and an unknown key that is _not_ a + near-miss keeps today's message byte for byte (the match is one leading + underscore, not a similarity score). This is not a second acceptance channel + for `selectedIds`: the contract still has exactly one spelling, + `params._selectedIds`. + +- e8dc61e: fix(spec): 正文里裸露的源码路径,`../` 前缀回到链接里面 (#6229) + + `file-description.ts` 把「正文中裸露的 `*.zod.ts` 路径」改写成链接的那一步,正则以 + `\b((?:\.\./)?…)` 开头。**单词边界要求两侧有一个单词字符,而 `../` 三个字符全是非单词 + 字符**,所以那个 `\b` 永远无法在 `.` 处成立:匹配只能从第一个路径段开始,前缀被丢在它 + 本该进入的链接**外面**,发布成: + + ``` + See also: ../../[system/cache.zod.ts](/docs/references/system/cache) for application-level caching + ``` + + `content/docs/references/api/http-cache.mdx` 与 `content/docs/references/system/cache.mdx` + 两页,读者看到的是一串裸文本紧挨着一个链接。 + + **实测把最初记录的成因修正了一处。** 原记录说前缀组「只允许一级,而语料用的是两级」。逐个 + 输入量过之后:该组对**任何现实输入都不成立**,不是「只支持一级」—— + + | 输入 | 修复前匹配到的 | | + | --------------------------- | ------------------------ | -------------------------------------------------- | + | `../../system/cache.zod.ts` | `system/cache.zod.ts` | 前缀漏在外面 | + | `../system/cache.zod.ts` | `system/cache.zod.ts` | **同样漏在外面**,并非原记录所说「本来就正常」 | + | `system/cache.zod.ts` | `system/cache.zod.ts` | 正常 | + | `x../system/cache.zod.ts` | `../system/cache.zod.ts` | 唯一能让该组生效的拼法:点号前有单词字符,没人这么写 | + + 于是两个半边**都**是必需的,而且互相不可替代:把 `?` 放宽成 `*` 而不动 `\b`,是在一个永远 + 到不了的组上改重复次数 —— **完全的空操作**;只把 `\b` 挪到组后面而保留 `?`,两级前缀里仍有 + 外层一个 `../` 留在链接外。两条都用「回退一半、跑 pin」逐条量过,方向与预测一致。 + + 修正后的正则把 `\b` 放在前缀组**之后**、紧贴第一个路径段: + + ``` + /(? development` row had been false for the runtime + caller since #5673 and is now two rows, one per rule. + +- a2e157c: fix(spec): 参考页里写在括号中的裸源码路径重新成链接 (#6420) + + 参考页开篇那段模块描述由 `packages/spec/scripts/lib/file-description.ts` 渲染。其中 + 把 JSDoc 里裸写的 `*.zod.ts` 路径改写成站内链接的那一步,正则两端各挂着一个前后瞻 + ——「前面不是 `(`」和「后面不是 `)`」。这对前后瞻是 tokenizer 出现**之前**的产物, + 本意是「别去动已经是链接目标的路径」:`](route)` 恰好把那个路径夹在这两个字符中间。 + 它从来表达不了这件事(前后瞻说不出「不在链接内部」,模块注释里写着),而 #6136 之后 + 它更是无事可做了 —— 成形的链接是独立的 `link` token,这一步只会看到 `text` token。 + + 它**仍在**做的,是把作者自己写在普通括号里的每一个路径一并拒掉。那是散文,不是链接, + 于是这些路径既没成链接也没成代码,以纯文本发布在三张参考页上: + + - `references/automation/etl` —— `- **Enterprise Connector** (integration/connector.zod.ts) - …` + - `references/integration/connector` —— `- **ETL Pipeline** (automation/etl.zod.ts) - …` + - `references/shared/mapping` —— `- Integration connectors (integration/connector.zod.ts)` 与 `- External lookups (data/external-lookup.zod.ts)` + + 现在删掉这对前后瞻,它们原本想守的不变量交还给 tokenizer 守。读者可见的变化就是上面 + 四处从纯文本变成可点的站内链接,路由分别指向 `/docs/references/integration/connector`、 + `/docs/references/automation/etl`、`/docs/references/data/external-lookup` —— 三条都 + 对应真实存在的页面。 + + 放宽的**实测**半径就是这四处,别无其他:在修好的生成器上重跑 `gen:docs`,231 个产物 + 里 3 个文件、4 行发生变化。渲染成链接的前提没有放宽 —— 目标没有页面的路径照旧回退成 + 代码段,所以括号位置永远不会产出 404。 + +- 95c4227: fix(spec): docs-gen renders a module description as the markdown it was written as (#5553, #6136) + + Two independent defects in `scripts/lib/file-description.ts`, both from a + transform applied at the wrong granularity. The block SELECTION rule #5059 added + is untouched: all 185 sources that carried a module header still render one, and + no page gained or lost an opening paragraph. + + **#5553 — line layout is content, not decoration.** The renderer dropped every + blank line and joined what survived with `\n\n`, making each SOURCE LINE its own + paragraph. Anything that legitimately wraps across lines was then cut in half by + a paragraph boundary, and an inline code span cannot cross one, so both of its + backticks fell out as literal text — `` `explain(principal, object, `` / + `` operation)` `` on `security/explain`, and three more like it. The same pass + escaped `{` and `}` everywhere including inside code, where a backslash is not + an escape character but a character the reader sees, so pages published + `` `\{ dialect, source \}` ``. + + The fix is to stop rewriting the layout: strip the `*` gutter and keep the + lines as authored. Markdown's own rules then do what the issue asked for — + consecutive lines are one paragraph, a blank line opens the next — and lists, + headings, tables and code blocks keep working, which the literal space-join the + issue floated would have broken on the 85 sources that write a list. Escaping and + link resolution are now scoped to prose: fenced and indented code blocks are + copied verbatim, and within prose a tokenizer keeps inline code spans out of + reach. + + One construct is deliberately NOT reproduced as authored: an indented (4-space) + code block is re-emitted as a fenced one. MDX dropped CommonMark's indented code + blocks so that indentation could lay out JSX, so such a block reaches the MDX + compiler as ordinary prose — and unescaped braces in prose are an expression. + `data/date-macros` and `data/context-tokens` write their placeholder examples + that way and are almost entirely braces; left indented they fail to compile + ("Could not parse expression with acorn"), and escaped instead they show `\{` in + what is meant to be code. The target dialect has one spelling for a code block. + + Measured over the 185 rendered descriptions: paragraphs with unpaired backticks + 8 → 0 (`automation/flow-function`, `security/explain`, `shared/expression`, + `system/settings-client`), and backslash-brace residue inside code 296 → 0 across + 33 pages. 32 pages get their fenced `@example` sample back as a real code block + instead of one escaped paragraph per line, and 47 regain the indentation that + made a nested list nested. The issue named five victim pages; `system/doc` is not + among them because #5059 has since found its header documents `DocSchema` and + stopped publishing it. + + **#6136 — a rewriter that ran over its own output.** The untitled + `{@link }` branch emits `[]()`, whose link TEXT is the path + itself. The bare-source-path rewriter ran next over the whole string and matched + that text, wrapping it a second time into a link nested in a link. Lookaround + cannot express "not nested inside a link", so the rewriter is now applied per + prose token with formed links excluded. `automation/etl` and + `integration/connector` each get their "See also" back as one clickable link. + + 169 reference pages are regenerated. No runtime, package export or protocol + semantics change — this is the docs generator only. + +- 2a61116: fix(spec): the reference generator elides an over-wide enum inside an inline shape summary, and says how many members it hid (#5340) + + `formatType()` capped how many **keys** an inline object summary prints + (`INLINE_KEY_LIMIT = 4`) but never capped how wide a single key's **type** could + be. One long enum reached through a summary therefore printed every member into + one table cell. The issue was filed on `BulkActionDef.params` at ~900 + characters; measuring the whole corpus found that is not close to the worst — + the 261-member error-code vocabulary is inlined into the `error` shape of 80 + rows across 13 `api/*.mdx` pages, at **6242 characters in a single cell**. + + An `Enum` body rendered below a summary's `{ … }` is now cut to 80 characters + and the count of what was cut is printed in its place: + + ``` + type: Enum<'text' | 'textarea' | 'email' | 'url' | 'phone' | 'password' | 'secret' | … +42 more> + ``` + + The count is the safety property, not decoration. A silent prefix would leave + the page looking complete while it was not — a reader cannot tell a 7-member + vocabulary from the first 7 of 49 — and these pages are the authoritative input + for AI authors (ADR-0033), so a page that lies by omission is a worse defect + than a wide cell. + + **Nothing that owns its vocabulary is elided.** The cut applies only below an + inline shape summary, which is by construction a _second_ copy: a schema's own + row (`BulkActionParam.type`, `ErrorResponse.code`), a union variant on its own + row (`Enum< … > | string`, the `PageComponent.type` shape), a top-level + `Record< string, Enum< … > >` and an array of a top-level enum all still print + every member. For 457 of the corpus's 805 in-shape occurrences the elided + copy's full list is still on the same page for that reason; for the remaining + 348 the count carries it, and the JSON Schema under `json-schema/` remains the + authority it always was. + + **The 80 is measured, not chosen.** Across 216 pages / 8541 type cells / 1768 + `Enum` occurrences, the 805 in-shape ones are bimodal and their density per + character collapses at 80 (3.6 occurrences/char over `(64,80]`, 1.6 over + `(80,100]`, 0.5 over `(100,200]`). Below it sit the ordinary short vocabularies + a reader wants spelled out; above it sit listings. A tighter budget buys almost + nothing and costs real information — budget 24 would elide 79% of them to save + 4% more characters — and a fixed member cap is worse at every setting. + + An elision must also **pay for its own marker**: a body only a member or two + over budget gives back less than `… +N more` costs to print, so it is left + whole. That is why 31 in-shape enums between 81 and 107 characters are + unchanged, and why the limit is not a cliff at exactly 81. + + 42 reference pages change, 144 rows, all in one direction: every changed row is + shorter (462,140 characters removed in total, largest single row -6266), and + every one carries a `… +N more` marker — no row is silently truncated and none + grew. Cells over 900 characters go from 76 to 4, over 200 from 246 to 145; + the p95 cell width is unchanged at 145, i.e. ordinary cells do not move. + +- d4df105: fix(spec): 参考页里写在同目录的裸源码路径不再以纯文本落地 (#6484) + + 参考页开篇那段模块描述由 `packages/spec/scripts/lib/file-description.ts` 渲染。它把 + JSDoc 里裸写的 `*.zod.ts` 路径改写成站内链接,而这条机制的**两侧**过去都要求路径里 + 至少有一个目录段:改写正则的 `[\w-]+/` 分组是必需的,`build-docs.ts` 的 + `sourcePathToDocsRoute()` 也要求那个斜杠、并把第一段读作分类名。 + + 于是作者按最自然的方式引用邻居 —— 写 `auth.zod.ts` 而不是 `identity/auth.zod.ts` —— + 两侧都匹配不上,既没成链接,也没回退成代码段,以**纯文本**发布在四张参考页上,共 9 处: + `api/realtime-shared`、`cloud/package`、`identity/identity`、`system/security-context`。 + + 缺的从来不是正则,而是**上下文**:`build-docs.ts` 按分类遍历,自己知道正在渲染哪个目录, + 却只把一个成员交给渲染方。现在 `FileDescriptionContext` 增加 `fromCategory`,由 + `build-docs.ts` 传入,裸文件名在渲染方补全成 `<分类>/<文件>` 后再去解析 —— 与 + `schemaHrefFrom(fromCategory)` 是同一道缝。补全放在调用方一侧是有意的:裸名不是身份 + (#4696),`auth.zod.ts` 在多个分类下都存在,让解析器自己去全分类搜同名文件只会答出 + 目录遍历最后到达的那一个。 + + 读者可见的变化是这 9 处:**5 处成为可点链接**(`api/realtime`、`api/websocket`、 + `cloud/package-version`、`cloud/environment-package`、`system/encryption`),**4 处回退成 + 代码段**(`auth`、`audit`、`compliance`、`masking` —— 这四个邻居本就不存在,按 #6229 + 的规矩「目标没有页面就不发链接」)。纯文本是三种结果里唯一错的那种,现在一处不剩。 + + `sourcePathToDocsRoute()` 同时补上了它文档里一直声明、实现却没做的那一半:分类是真的 + 不等于页面存在。旧实现只校验分类,这在放宽之前侥幸成立(能匹配上的路径恰好都有页面); + 放宽后那 4 个不存在的邻居会各产出一条 404 链接。现在按本次运行真正发出的页面清单判断, + 全语料 216 条站内路由、437 个位置,无死链。 + +- d9bef45: fix(spec,rest): `OVERLAY_PERSISTENCE_FAILED` leaves the error-code ledger — it lost its only producer (#5783) + + `ERROR_CODE_LEDGER` registered `OVERLAY_PERSISTENCE_FAILED` under + `@objectstack/metadata-protocol`, but nothing in the repository can emit it any + more. Its one emission point was the `catch` inside `saveMetaItem`'s legacy + raw-engine branch, and #5264 (PR #5782) deleted that branch. A registered code + with no producer is ADR-0112's "no silent fourth state" read backwards: the + vocabulary promises a client a code no response can carry, and the ledger's own + admission test cannot notice, because it checks casing, duplication and + shadowing — never whether anyone still throws the code. + + Verified before removing: a declaration-and-emission search over `origin/main` + finds the name only in the ledger row itself, two generated reference pages, one + `rest-server.ts` comment, one historical changeset plus its CHANGELOG entry, and + two `packages/rest` tests that construct the error themselves. No producer, and + no consumer — including `objectui` and `cloud`, both searched at their + `origin/main` — reads the literal. Removal only shrinks a dead row: nothing + gates an emission on ledger membership, so no runtime or gate starts rejecting + anything it accepted before. + + **Wire impact: none.** No response carried this code, so no client can lose one. + The narrowing is type-level: `ErrorCode` (`StandardErrorCode` ∪ the ledger, what + `ApiErrorSchema.code` validates) no longer admits the string, so TypeScript + would now reject `code: 'OVERLAY_PERSISTENCE_FAILED'` at a call site — and there + is no such call site left to reject. + + Note for whoever compiles the release: #5437's changeset + (`rest-5xx-message-withheld.md`) names this code as one of two examples of a + `code` that "still rides on the response". That sentence was accurate when it + was written; the other example, `NOT_IMPLEMENTED`, is unaffected and still + demonstrates the same behaviour. + + The two `packages/rest` tests that asserted `resolveErrorResponse`'s handling of + a declared 5xx keep their substance and switch to a producer that still exists — + `metadata-protocol`'s `batchData` atomic refusal (`501` / `NOT_IMPLEMENTED`) and + the surviving overlay-delete `500`. Three stale comments are corrected in the + same pass: the `agent` entry in `metadata-plugin.zod.ts` (which described a + routing mechanism replaced by #5086's 403 refusal), the reachability argument in + `rest-5xx-message-sanitization.test.ts`, and `resolveErrorResponse`'s own + docblock in `rest-server.ts`. + +- 881a3cc: fix(spec): the `fallbackNodeId` tombstone names the key that actually routes faults (#6094) + + `flow.errorHandling.fallbackNodeId` was retired in 17.0.0 (#3896), and its + migration message tells the author what to draw instead. It named the wrong key: + + - FROM: "the engine routes unrecoverable node errors via per-node fault edges + (an edge with **condition `'fault'`**)" + - TO: "… (an edge with **`type: 'fault'`**)" + + `condition` on `FlowEdgeSchema` is a **CEL predicate** returning boolean + (`flow.zod.ts` — `ExpressionInputSchema`), while the fault/default/conditional/back + routing lives on `type` (`z.enum([...])`). An author following the old wording + verbatim would write `{ source, target, condition: 'fault' }`, which **parses + clean** — `condition` accepts any expression string — and produces an ordinary + edge that is not a fault path. So the tombstone handed them a second silently + inert key in exchange for the one it took away: they delete a fallback that never + existed, then draw a fault edge that isn't one. + + The repo already states the correct rule elsewhere (`flows.mdx`: "`type: 'fault'` + is what routes — a label is not"), and every other mention in the tree spells it + `type: 'fault'`; this was the only site out of step. The closing sentence + ("draw a fault edge from the failing node to the handler node instead") was + already correct and is unchanged. + + Message text only — no schema, validation, or runtime behaviour changes. + +- 8a88885: docs(spec): field- and section-level `visibleWhen` stop advertising `current_user` (#6146) + + `FormField.visibleWhen` and `FormSection.visibleWhen` documented their runtime + binding root as "`record` + `current_user`". The second half was never true at + that level, and the failure mode is silent: + + - FROM: "Root: `record`+`current_user` (runtime forms) or `data` (metadata forms)" + - TO: "Root: `record` (+ `previous`, `parent`) in runtime forms, or `data` in + metadata forms" — plus an explicit note that `current_user` is **unbound** here. + + Field- and section-level rules are evaluated by `evalFieldPredicate` / + `resolveFieldRuleState` in `@object-ui/core`, which binds `record`, `previous`, + and an `extra` scope (`parent`, for master-detail line items) — nothing else. + Every production call site passes no user scope, and objectui#1582 pins the same + set for the authoring autocomplete (`FIELD_RULE_ROOTS = ['record','previous','parent']`, + commented "nothing else (no `current_user`)"). + + Why this mattered more than a wording slip: an unbound identifier makes the + evaluation fault, and every fault resolves to the caller's fallback — which for + visibility is `true`. So a predicate written exactly as the spec described it + (`'admin' in current_user.positions`) does not hide the field, it makes the + field **permanently visible**. Authors following the documentation got the + inverse of what they wrote, on the surface where the mistake is least visible. + + `current_user` remains documented, and remains correct, for **per-option** + `visibleWhen` (`SelectOption`): options resolve through a different evaluator, + `resolveCascadingOptions` against the host's predicate scope, which does bind it + (ADR-0068 / objectui#2284). That JSDoc previously claimed the per-option + environment was "the SAME binding environment as field-level `visibleWhen`" — + the very equality that is false — so it now states the asymmetry instead of + asserting it away. + + Documentation only: no schema, validation, or runtime behaviour change. Authors + whose field-level predicates reference `current_user` should know those + predicates are already faulting open today; this change does not alter that, it + stops the spec from recommending it. + +- 474f131: flow 不再允许按组织覆盖:`allowOrgOverride` 回滚为 `false` + + `DEFAULT_METADATA_TYPE_REGISTRY` 里 `flow` 那一行的 `allowOrgOverride: true` 来自 + commit ba252da0b,没有 ADR 支撑,并且与 ADR-0005 的白名单表(automation ❌ —— + 「Carry execution side-effects (events, jobs, audit). Per-org variants are a + deployment, not an overlay.」)直接冲突;同一行的 `supportsOverlay: false` 也早已 + 说明加载器根本不会合并 per-org 的 flow overlay。两者相加的结果是:写入被批准、 + 落库成功,而下一次冷启动读路径把该行滤掉,绑定消失。 + + 现在这类写入在发生的那一刻就被响亮拒绝:对**已由代码包交付的** flow 做 org 覆盖 + 返回 `403 not_overridable`(与 `object` 同一条路径,且不依赖部署拓扑 —— + `environmentId` 未设置的单内核同样拒绝)。 + + 未收紧、也不打算收紧的一侧:`allowRuntimeCreate` 仍为 `true`,租户依旧可以通过 + 运行时 API 新建一条**全新的** flow —— 它没有影子化任何代码包交付的自动化,正是 + ADR-0005 所说的「a deployment」。 + + 影响面:此前唯一能走通这条写路径的是「写入成功但重启即失绑」的幻觉状态,收掉的 + 是幻觉而不是能力。 + +- bf0ae99: Reorder the three hand-written `unrecognized_keys` error maps so the fix is read before the explanation (#6416, applying #5955's ruling). + + `strictVisibilityError` (`shared/visibility.ts`), `strictWidgetAnalyticsError` (`ui/dashboard.zod.ts`) and `strictTenancyError` (`data/object.zod.ts`) are independent `$ZodErrorMap` functions rather than `strictUnknownKeyError` call sites, so #5955's reorder of the shared template did not reach them and #5593's `strictObject` migration cannot either. Each reproduced the exact shape #5955 was filed against: a non-actionable explanatory sentence sitting between the offending key and the prescription that fixes it, which on the single-line renders several consumers use (`os validate`'s `• where: message`, CI logs, `validateFlowTriggerReadiness`) pushed the fix out of the part an author actually reads. + + Every message now emits front matter (which key is wrong) → every fix channel (the `visibleWhen` alias pointer; the ADR-0021 dataset / objectui-quarantine / #5022 drill branches; the per-key `tenancy` tombstone bullets) → the explanatory sentence last. Nothing is deleted and nothing becomes conditional — each sentence is still emitted verbatim, once per message, and all seven message variants are byte-identical in length and character multiset to their previous spelling. No input changes acceptance: these maps only shape the text of an already-failing parse, and the `visibility.ts` alias tables are untouched. + +- d2b97c3: Give six `*.zod.ts` modules a true module-header doc block, so their reference pages open with their introduction again + + `postgres` / `mysql` / `sqlite` driver config, `cloud/template-manifest`, `system/doc` and + `api/error-code-ledger` each already carried a real module introduction — but written glued + to the module's first declaration, which under #5059's strict selection rule is that + symbol's TSDoc and therefore not the module's description. The prose was never the problem; + its attachment was. Each block is promoted verbatim to a top-level header that documents no + symbol, the shape 183 of the reference sources already use, and + `content/docs/references/**` is regenerated: six pages gain their opening paragraphs, no + other page changes and no schema byte moves. + +- 5d022a1: Correct the `HookContext.input` contract table on `input.options`: during + `before*` the slot holds the CALLER's engine options bag (`where` and `multi` + included), not `DriverOptions` — the engine merges the driver-facing keys onto + it only after the handlers return. The table's two `before` rows said + `DriverOptions`, a type that declares neither key, which reads as "a hook can + see no predicate at all"; the composed `ast` is what hooks cannot reach, while + the caller's raw predicate is right there and is an upper-bound approximation of + the row set (middleware only narrows) — the safe direction for the fail-closed + guards built on it. Pinned with a positive assertion in + `hook-input-shape-contract.test.ts`. +- 042b9ee: docs(spec): `session.positions` stops teaching a hook call that cannot run (#6001) + + The `positions` key on the hook session carries a deliberate boundary note: it is + **descriptive, never an authorization input**. That conclusion is unchanged. What + changed is the example standing next to it, which was itself defective: + + - FROM: "A hook may READ this … forwarding it as the sharing service's evaluation + context (`services.sharing.canEdit(..., { positions })`, the shape both + `content/docs/kernel/runtime-services/` pages teach)" + - TO: an example a hook can actually reach — tailoring a message, branching a + _business_ rule through the hook's own `ctx.api` channel, logging — plus an + explicit note that a hook context carries **no `services` key**. + + #5720 pinned this by measurement: hook contexts are assembled key by key, and + neither ObjectQL's `buildSession()` nor `buildSandboxContext()` + (`packages/runtime/src/sandbox/body-runner.ts` — `input` / `previous` / `user` / + `session` / `event` / `object` / `result` / `api` / `log` / `crypto`) ever sets + `services`. So `services.sharing.canEdit(…)` is `undefined()` inside a hook, and + the customary `if (!ok) throw` wrapped around it rejects **every** write. The + one shape the doc held up as correct practice was the exact shape that fails + closed on all traffic. + + The two cross-referenced doc pages had already reversed under PR #5938 — + `sharing-service.mdx` now carries "Enforcement is automatic — do not re-check it + in a hook", and `examples.mdx` teaches `ctx.api` — leaving this JSDoc as the last + site still teaching the withdrawn shape. The reference now points at that + section instead of at the pages generically. + + Both the JSDoc **and** the `.describe()` are updated; the `describe` is the copy + served through `/api/v1/meta/types/hook` and rendered in Studio's form, so + leaving it would have kept the defective example on the surface authors actually + read. Text only — no schema, validation, or runtime behaviour change. + +- 33e0385: fix(spec): `InboxListResult.unreadCount` no longer documents the window count it stopped being (#6438) + + `INotificationService` is a published contract — its JSDoc ships in the `.d.ts` and is + the sentence a TS SDK consumer reads in their editor. The `unreadCount` member said: + + > Unread count over the returned window. + + That recorded the implementation as it was _before_ #6363. After #6363 (Option A, + maintainer ruling 2026-08-07; PR #6439, merged as `17d095413`) `service-messaging` + counts the **total** unread across the user's whole matching inbox, and the window + bounds `notifications[]` only. The wire declaration one directory over had already said + the same thing all along — + `ListNotificationsResponseSchema.unreadCount.describe('Total number of unread +notifications')` (`api/protocol.zod.ts`) — so one package carried two opposite sentences + about one field, with the implementation standing on the `.describe()` side and this + JSDoc the last statement of the retired semantics. + + Left alone, it is the sentence that teaches the bug back. A consumer told the number is + "over the returned window" writes exactly the adaptation #6363 exists to delete: counting + `notifications` themselves, or clamping the badge to the page size. That holds double for + AI-written consumers, which are generated from this JSDoc and nothing else. + + Both members are now documented, because after #6363 their bounds differ **on purpose** + and the interface had never written that difference down anywhere: + + - `notifications` — the `limit`-bounded window, one page, implementations may clamp. + - `unreadCount` — the total across the whole matching inbox, explicitly NOT the window, + with the "do not re-derive, do not clamp" consequence spelled out for consumers. + + Text only. No schema, no value, no behavior: every input that validated before validates + byte-for-byte after, and the generated artifacts (`check:docs`, `check:authorable-surface`, + `check:skill-refs`, `check:api-surface`) are unchanged — the reference docs render from + Zod `.describe()` strings, none of which this touches. + +- d0a5ceb: fix(spec): 参考文档的内联形状摘要只展开一层,不再沿数组/Record/联合无预算下钻 (#6374) + + `content/docs/references/**` 的类型单元格里,最宽的一格是 `ui/page.mdx` 的 + `Page.slots`,**1538 字符** —— 而且是在 #5340 的枚举省略已经在这一格生效 8 次 + 之后的宽度。本次修完,这一格是 **122 字符**。 + + ## 机制 + + `format-type.ts` 一直只展开**一层** `{ … }` 形状:再往下的对象打印 `object`。 + 但这条预算写在键循环的三元表达式里,于是只有**直接对象子节点**受它约束。数组元素 + (`{ … }[]`)、`Record` 的值(`Record`)、联合的变体 + (`{ … } | { … }[]`)这三条路径都会重新进入对象分支,而预算不在作用域内 —— + 单元格宽度于是等于「每层键数 × 变体数 × 每个形状的宽度」,一层一层乘上去。 + + 同一个形状在同一个阅读深度上,**印全还是印 `object`,取决于作者有没有把它包在数组 + 里** —— 这是关于 Zod 写法的事实,不是关于读者怎么读的事实,和 #6225 拆掉的那种 + 不对称完全同类。 + + 新常量 `SHAPE_DEPTH_LIMIT` 把同一条预算移到对象分支本身,四条下钻路径都要过它。 + **阈值 1 不是选出来的,是取回来的** —— 它就是直接子节点路径上一直生效的那个值; + 全语料实测,任何大于 1 的取值都比不改还差(>200 字符的单元格 121 → 173+,因为 + 提高上限必然放松那条本来就是 1 的路径)。 + + ## 读者看到的变化(全语料 215 页 / 8499 个类型单元格) + + | | 修改前 | 修改后 | + | ---------- | --------- | ------------- | + | >200 字符 | 121 | **42** | + | >400 字符 | 9 | **1** | + | >900 字符 | 1 | **0** | + | p95 / p99 | 145 / 229 | **124 / 180** | + | 最宽单元格 | 1538 | **656** | + + 修改后仅剩的那个 >400 单元格是 `ui/page.mdx` 的 `PageComponent.type`(656), + 一个落在联合变体里的顶层词表 —— #6225 有意不收它,它也**不含任何嵌套形状**。 + 也就是说:**由形状深度带来的宽度已经从语料里消失了**。 + + ## 省略掉的信息去哪了 + + `object` 不是截断:它对键**什么都不声称**,所以不像前缀那样会被误读成完整列表 —— + 这正是 #5340 定下的原则用在形状上而不是枚举成员上。它也不是这些表格里的新省略 + 风格:嵌套形状本来就一直印 `object`。完整形状仍在原处 —— 生成器为它出页时是它 + 自己的 `## Schema` 一节,任何情况下都在 `json-schema/` 里。 + + #5340 / #6226 的两个标记都还活着,只是有些出现位置被上游的深度预算吸收了: + 枚举标记 178 → 156,变体标记 16 → 9。#6226 的旗舰样本 `App.navigation` 在深度 0, + 逐字未变。 + + ⛔ 所有 `.mdx` 均由 `gen:schema && gen:docs` 重生成,无一处手改。 + +- eb91eba: docs(spec): correct two published claims about the #4610 Notification retirement, and forward-note the stale `areas` caveat in the shipped rc.2 changelog (#5781, #5809) + + **What changes is what the repo SAYS, not what it does.** No schema byte, no + export, no conversion and no baseline moves; `@objectstack/spec/ui` still does + not publish `Notification(Schema)` / `NotificationConfig(Schema)` and + `@objectstack/spec/system` still does not publish `NotificationConfig(Schema)`. + Two of the three corrections are to live contract prose, one is a forward-note + on a shipped changelog section, and one unconsumed changeset (a v17 GA release + input) is corrected at the source. + + **1. "zero importers in all three repos" was false for objectui (#5781).** #4610 + justified removing the two `./ui` notification wrappers with a three-repo, + import-statement-level consumer scan. objectui#3310 measured the same names at + 17.0.0-rc.1 and found both alive: `packages/types/src/index.ts` re-exported them + with `export … from '@objectstack/spec/ui'`, and + `packages/core/src/protocols/NotificationProtocol.ts` consumed them through the + `@object-ui/types` barrel, in the public signatures of `resolveNotificationConfig` + / `specNotificationToToast`. The scan matched `import … from` statement text and + could see neither hop. + + ⛔ **The retirement is not reopened.** objectui does not ask for it back: the + `@object-ui/core` bridge had zero in-repo callers, the implementation that runs + is `@object-ui/react`'s locally-declared `NotificationSystemConfig`, and objectui + deleted the bridge to FOLLOW the retirement rather than re-declare vocabulary the + spec had just dropped. Only the sentence that justified the removal changes. + + **2. The FROM → TO would have sent an author to code that does not compile + (#5781).** #4610 published `from '@objectstack/spec/ui'` → `from +'@objectstack/spec/api'` for `Notification(Schema)`. `./api`'s `Notification` is + the REST inbox row (`id` / `type` / `title` / `body` / `read` / `data` / + `actionUrl` / `createdAt`); the removed `./ui` shape was a toast instance + (`message` / `severity` / `position` / `duration` / `dismissible` / `actions` + + ARIA). They share zero fields — same name, a different contract, which is the + dual-source trap #4610 closed rather than a new home for the old shape. The + guidance is now **no replacement**: keep `./ui`'s presentation enums and declare + the instance shape locally, as objectui does. Counted honestly while rewriting + it: **three** enums survive (`NotificationType` / `NotificationSeverity` / + `NotificationPosition`), not the four #4610 listed — #5015 retired + `NotificationAction` at 17.0.0-rc.3. + + **3. The methodology, written down beside the tombstone.** A cross-repo liveness + verdict must be read off the RESOLVED SYMBOL GRAPH, covering at minimum + `export … from` re-exports and consumption that reaches the spec indirectly + through a downstream barrel package — never off import-statement text. This was + the third miss of that class, after #4667 / #4709 (`app.homePageId`). + + **4. Shipped changelog sections get forward-notes, never rewrites (#5809).** A + changelog's value is in being a faithful record of what was shipped and said, so + the stale sentences stay and a clearly-marked correction is added inside the + section, pointing at where the corrected statement lives. Three notes land in + `## 17.0.0-rc.2`: two on the #4610 entry (`0a936ea`, the evidence and the + FROM → TO), and one on the #4651 app-area entry (`ad047d2`), whose caveat _"per-item + gating inside an area is enforced by the shell only, because the server does not + walk `areas`"_ has not held since #4722 — `filterAppForUser` runs the same + `filterNav` over every `areas[].navigation`. The same file's `## 17.0.0-rc.4` + entry `e4c8b6c` (#5337 / PR #5796) already carries the corrected statement, so + until now one `CHANGELOG.md` asserted a prescription in one section and refuted + it in another; a reader arriving by keyword search could be sent off to do an + unnecessary navigation-tree refactor. + + Corrected directly (live contract text and GA inputs, not shipped records): + `packages/spec/src/ui/notification.zod.ts`'s tombstone, + `packages/spec/src/migrations/registry.ts`'s protocol-17 `#5015` rationale (which + projects verbatim into `docs/protocol-upgrade-guide.md` and `spec-changes.json` + via `gen:upgrade-guide` / `gen:spec-changes` — both regenerated, never hand-edited), + and the still-unconsumed `.changeset/notification-dual-source-c3.md`, which is a + legal input to the v17 GA release notes while the repo is in changesets pre mode. + New pin tests keep both false premises from returning: the registry `reason` is + pinned in `migrations.test.ts`, the tombstone's four verdicts in + `notification.test.ts`. ⛔ `content/docs/releases/` is untouched. + +- 643b7c7: fix(spec): `object.form.ts` repeater predicates bind through `data`, like every other metadata form (#6254) + + The object metadata form's field-list repeater carried 16 `visibleWhen` + predicates written as **bare identifiers**: + + - FROM: `visibleWhen: "type == 'formula'"`, `visibleWhen: "type in ['lookup','master_detail']"`, … + - TO: `visibleWhen: "data.type == 'formula'"`, `visibleWhen: "data.type in ['lookup','master_detail']"`, … + + Metadata-editing forms (`*.form.ts`) bind the row under edit as `data` — which + is how the sibling `field.form.ts` has always written them (`data.type == 'text'`). + The bare spelling has no binding at all: `type` is an unbound identifier, the + predicate faults, and a faulted visibility predicate resolves to its fallback, + `true`. Every constrained sub-field — `maxLength`, `min`/`max`, `precision`, + `expression`, `returnType`, `reference`, `deleteBehavior`, `autonumberFormat`, + … — was therefore offered on **every** field row regardless of its type, which + is the exact opposite of what each rule asks for. + + **The repeater does rebind `data`, and that is worth writing down.** A sub-field + of a `type: 'record'` repeater is evaluated against its own row + (`evaluatePredicate(spec.visibleOn, { data: row })` in the metadata form + renderer), so `data.type` reads _this row's_ type — precisely what a per-entry + rule wants. What the repeater does **not** do is introduce an implicit row + scope: the root is spelled `data` at every depth. `FormField.visibleWhen`'s + JSDoc and `describe` now state both halves, so the next author does not have to + infer either one. + + `field.form.ts` is unchanged — it was already correct. + + Authoring-surface fix with no schema or validation change, hence patch. Note + that these predicates are not yet _observably_ restored: a separate defect + outside this package (the metadata form renderer reads the deprecated + `visibleOn` key, while the parse emits only the canonical `visibleWhen`) keeps + every metadata-form predicate inert today. That is tracked separately; this + change is a prerequisite for it, and correct on its own terms either way. + +- b70e534: Declaration sync for ADR-0117 D1: `owning_business_unit_id` is documented as INJECTED, + while the `business_unit` ownership tier is documented as still unauthorable. + + #5677 landed D1's execution surface in `packages/objectql`: `applySystemFields`' owner + decision became an allow-list and the `owning_business_unit_id` column is now injected on + every ownership-eligible object — i.e. under `ownership: 'user'` and when `ownership` is + omitted, withheld under `'org' | 'none'` and on `managedBy` / `sys_*` tables. The spec's + own prose had not followed: `SystemFieldName.OWNING_BUSINESS_UNIT_ID` still read + "**NOT injected by open-core** — nothing provisions this column today", which had become + false on the default tier every ordinary business object uses. + + The flip is deliberately PARTIAL, because the condition it was written against was + two-part and only one half landed. The JSDoc gated itself on both (a) the `ownership` + enum gaining a `business_unit` member and (b) the `wantOwner` deny-list becoming an + allow-list. Only (b) shipped. `ObjectSchema`'s `ownership` enum is still + `'user' | 'org' | 'none'`, so `ownership: 'business_unit'` remains deliberately rejected + (the enum member is tracked separately). A flat "INJECTED" would have deleted a true + sentence and implied an authorable tier that does not exist — declaring what the runtime + rejects, which is the dangerous direction of ADR-0049, and the inverse of the benign + runtime-ahead-of-docs gap this closes. Both facts are therefore stated together, in every + place that states either: + + - `SystemFieldName.OWNING_BUSINESS_UNIT_ID` — INJECTED, plus an explicit "the column + being injected does not mean the tier is authorable" paragraph, plus the + provisioned-but-inert note (the D2/D4 stamping middleware has not landed, so nothing + writes a value yet). + - `ObjectSchema.systemFields` — the injected-column list gains `owning_business_unit_id`, + with its governing property, its `organization_id`-shaped column definition, and the + same tier caveat. + - `resolveInjectedSystemColumns` — its per-tier table already matched D1; it gains one + note that the `business_unit` row is implemented ahead of the acceptance surface, so + the row is not misread as a claim that the tier is available. + + Documentation only: no schema, no value, and no injected column changes, so no metadata + document changes what it parses to. + +- 2c26040: fix(spec): 参考文档的模块标题改为声明式,`qa` 不再被渲染成 "Qa Protocol" (#5853) + + `build-docs.ts` 过去是**猜**模块标题的:默认首字母大写,再对 `['UI', 'AI', 'API']` + 这三个当初有人想到的缩写做全大写例外。`qa` 同样是缩写 —— `src/qa/index.ts` 自己的 + 文件头写的就是 "Quality Assurance (QA) Protocol" —— 但不在名单里,于是生成器单方面 + 把它降级成 **"Qa Protocol"**,一次发布到三处:分类页标题、`qa/meta.json` 里的侧边栏 + 标签,以及(#4759 把根索引纳入生成之后)`references/index.mdx` 的导航行与章节标题。 + + ## 为什么不是把 `QA` 加进名单就完事 + + - `packages/spec/src/` 下有 **17** 个模块目录,由 `readdirSync` 在运行时发现 —— + 没有任何东西提醒作者在新增目录时去补名单。 + - 这 17 个里 **4 个是缩写**(`ai`、`api`、`ui`、`qa`),名单覆盖了 3 个:在它唯一 + 服务的那一类上,漏报率 25%。 + - **任何门禁都不可能发现它。** `check:docs` 比对的是「生成结果 vs 已提交结果」, + 而一个错误的标题是**稳定的**,所以它永远是绿的。`Qa Protocol` 从 `src/qa/` 建立 + 那天起熬过了每一次重生成,直到 #4759 把 14 个标题并排印出来才被人眼看见。 + + 所以真正的缺陷不是「漏了一个缩写」,而是**猜出来的标题错得无法被发现**。 + + ## 现在的形状 + + 标题改为在 `scripts/lib/category-title.ts` 里逐个**声明**(`CATEGORY_TITLES`),并且 + 该表对磁盘上的目录是**全覆盖**的:没有兜底、没有推导,`resolveCategoryTitles()` 是 + 构造这张映射的唯一入口,双向缺口一律抛错。新增一个模块目录会让 `gen:docs` 直接失败 + 并指名道姓地告诉你补哪一行,而不是默默发布一个 "Iam Protocol"。这与旁边 `CATEGORY_BLURBS` + 用了一个数据项的既有写法(`blurbCoverage` / `formatBlurbCoverage`,#4759)是同一套惯例 + —— 标题只是最后一个还在靠猜的按模块数据项。 + + 面向读者的变化:参考文档三处落点现在都读作 **"QA Protocol"**。 + +- 35f7fb4: docs(spec): 参考文档表格单元格的两条残留宽度 —— 顶层长枚举移入 `### Allowed Values`,联合变体印数量 (#6225, #6226) + + #5340(PR #6211)把**内联摘要里**的长枚举压掉之后,`content/docs/references/**` 仍有 **27 个超过 + 400 字符的类型单元格**。它们不是一种毛病,是两种,机制不同,因此分两条修,一次重生成: + + **#6225 —— 9 个「整格就是一个 `Enum<…>`」的顶层词表。** + + `build-docs.ts` 一直有一条更适合长词表的渲染路径 —— `### Allowed Values` 加每个成员一行项目符号 + —— 但它只在**整个 schema** 是 `type: 'string'` + `enum` 时才走。同样 49 个成员,被 Zod 提升成具名 + schema(`data/FieldType`)的走项目符号,内联在**属性**上的(`Field.type`)得到一个 561 字符的表格 + 单元格,而 `ApiError.code` 的 261 个成员得到 **6092 字符**。一份词表落在哪种渲染上,取决于它有没有 + 被提升 —— 这是关于 Zod 的事实,不是关于读者怎么读它的事实。 + + 新增的 `formatPropertyType` 就是那条分支的镜像,**逐条件对齐**(`type === 'string'` 且 `enum` 是 + 数组):属性自己的类型节点就是那份词表。单元格印一个样本加数量,完整成员表由 `build-docs.ts` 印在 + 表格正下方 —— **信息一个字都没有离开页面**。省略与搬迁是同一个 `elideEnum` 调用的两个返回值,不可能 + 各说各话;预算只在会搬迁的那一个入口读取,`formatType` 本身一行没改,所以 #5340 立下的「词表自己那 + 一行永不省略」在 `formatType` 上原样成立。 + + 刻意**不**匹配的三种位置(各自都在单元格里渲染出 `Enum<…>`):`Enum<…>[]`、 + `Record>`、以及联合变体(`Enum<…> | string`,`ui/page.mdx` 的 `PageComponent.type`)。 + 对它们来说「本属性的允许值」不是实话 —— 成员是**元素 / 值 / 某一个变体**的词表 —— 表格下面挂一份项目 + 符号会宣称 schema 没说过的事。数字枚举同样不匹配:项目符号把每个成员渲染成 `` `x` ``,分不出 `2` 与 + `'2'`,那正是 #5729 修掉的错(镜像的整 schema 分支也要求 `type === 'string'`,所以这是对齐不是巧合)。 + + **#6226 —— 另外 18 个里的联合变体重复。** 维护者裁决:变体数上限 + 自报被省略的变体数。 + `formatType` 的 `anyOf` 分支原本是 `variants.map(...).join(' | ')`,对变体之间的相似度一无所知 —— + `ui/app.mdx` 的 `App.navigation` 把同一个 `{ id; label; icon?; order?; … }` 印了 9 遍,其中 7 遍逐字 + 相同,582 字符。现在最多印 4 个,其余印 `… +N more`。 + + **阈值全部实测,没有拍脑袋的常数。** 在真实语料上逐个候选值重生成再量: + + - 枚举预算:40 到 176 之间的**每一个**值都给出**完全相同**的宽度结果(>200 为 121、>400 为 18、 + > 900 为 1、p99 227、max 1538),所以这段区间里选哪个跟宽度无关,只跟**搬走多少份词表**有关 —— + > 从 227 份掉到 24 份。取 **160**:拿到能拿到的最好宽度,只搬 25 份而不是 227 份,距 184 的悬崖还 + > 留有余量;它恰好是内联预算 80 的两倍,把两个位置的不对称说明白了 —— 摘掉一份副本几乎免费,把词表 + > 搬出它自己那一行要在页面上多开一节。 + - 变体上限:语料共 353 个联合,2 个变体的占 256(72.5%),累计到 4 个变体是 92.6%,之后是薄尾。 + 上限 2 → 8 个宽单元格,3 → 8,**4 → 9**,5 → 11,6 → 18(与不设上限无异)。取 **4**:它是仍能 + 做完几乎全部工作的最松上限(收到 3 只多救回 1 个单元格,却要省略 44 个联合而不是 26 个),并且 + 与读者上一行刚见过的 `INLINE_KEY_LIMIT = 4` 同源 —— 同一个单元格里两个不同的「印几个之后 `…`」 + 才是裁决要避免的那种不一致。 + + **「标记必须挣回自己的位置」守卫**沿用 #5340 并被两条新省略共用。实测(把守卫改成无条件省略再重生成 + 对比):它拒绝了 248 个候选省略里的 **54 个**,那 54 个加起来只省 **328 字符** —— 平均每个 6 字符, + 而标记本身要花 12–15 字符。其中 **7 个是搬迁候选**,也就是七整节 `### Allowed Values` 本来会为了给 + 一个单元格削掉个位数字符而被加进页面。守卫在搬迁上比在 #5340 的内联省略上更要紧:那边一次拒绝省下 + 一个标记,这边省下一整节。 + + **全语料重新测量(216 页 / 8541 个单元格):** + + | | 之前 | 之后 | + | ------------- | -------- | --------- | + | 超过 200 字符 | 145 | 121 | + | 超过 400 字符 | 27 | **9** | + | 超过 900 字符 | 4 | **1** | + | p99 | 247 | 227 | + | p95 | 145 | 145(不动) | + | max | **6092** | 1538 | + + p95 纹丝不动,是这次改动的重点:普通单元格一个都没有移位。9 份被搬迁的顶层词表连同另外 16 份 160 + 字符档的,全部逐条核过完整成员表就在本页(25/25,0 缺失);新加的 25 个 `### Allowed Values: X.y` + 标题按 schema + 属性双重限定,全页面唯一(`api/errors.mdx` 上 `EnhancedApiError` 与 `FieldError` 都有 + `code`,只写属性名会撞锚点)。全语料 MDX 编译保持 **216/216**。 + + **没有被这次改动够到的、如实记录:** 剩下 9 个超过 400 字符的单元格里,`ui/page.mdx` 的 `Page.slots` + (1538,本轮新的 max)恰恰**不是**变体重复 —— 它的每个联合都只有 **2** 个变体(`T` 或 `T[]`),宽度 + 来自 `INLINE_KEY_LIMIT` 的 4 个键 × 2 个变体 × 每个约 176 字符,任何 ≥2 的变体上限都够不到它。另有 + 4 个(`Manifest.capabilities`、`PluginRegistryEntry.capabilities`、 + `GetTranslationsResponse.translations`、`PluginSecurityManifest.permissions`)**根本没有联合**,宽度 + 纯粹来自嵌套深度。这是第三种机制,已另立观察单,不在本 PR 范围内。 + +- 761a0ba: fix(spec): put the unknown-key fix before the surface history sentence (#5955) + + `strictUnknownKeyError` — the error map behind every `strictObject` authoring + surface — assembled its message as _front matter → history → fix_. The + `history` sentence a surface declares ("why this key used to be dropped + silently") therefore sat between the two things an author actually needs: the + name of the key that is wrong, and the key to write instead. + + That was tolerable while these rejections were warnings. It stopped being + tolerable when #5762 promoted `flow-time-relative-descriptor-invalid` to + **error**, because several consumers render a finding on ONE line — `os +validate`'s `• where: message`, CI logs, and `validateFlowTriggerReadiness`, + which deliberately flattens the newlines out of the schema's own text so the + CLI's bulleted list stays aligned. `TimeRelativeTriggerSchema`'s history + sentence is 224 characters, so on the descriptor from #5496 the words + `Did you mean` landed at character 443 of a 480-character line, behind a + sentence about 2026 that carries no instruction. The author — often an AI — + reads the front of that line and acts on it. + + The sentence now goes last: + + ```text + Unrecognized key(s) on {surface}: `k1`, `k2`. which keys are wrong + [ Did you mean `k1` → `canonical`? ] fix, channel 1 (renames) + [ newline + " • " + {guidance} ] fix, channel 2 (prescriptions) + {history} why it used to be silent + ``` + + Measured on `TimeRelativeTriggerSchema`, before and after: + + | case | length before | length after | `Did you mean` at | + | ---------------------------------------------------------------------- | ------------- | ------------ | ------------------ | + | #5496 descriptor (`field` + missing `dateField` + scalar `offsetDays`) | 480 | 480 | 443 → 219 | + | single misspelled key (`offsetDay`) | 366 | 366 | 329 → 105 | + | guidance hit (`schedule`) | 544 | 544 | n/a (bullet at 92) | + + **Nothing was deleted, nothing became conditional.** Every declared `history` + is still emitted, verbatim, exactly once per message — message lengths are + byte-identical, only the position moved. Both fix channels moved ahead of it: + a `guidance` prescription is as actionable as a rename, so it could not be left + behind the sentence either. + + **Migration.** No authoring change, and no schema change: `history` is still a + required option, spelled the same way, on all 62 `strictObject` surfaces and + 44 direct `strictUnknownKeyError` call sites. A test that asserts the full + message text in order needs its expectation reordered; a test that asserts + fragments with `toContain` is unaffected. The order itself is now pinned in + `strict-object.test.ts`, so it cannot silently regress. + +- be87153: fix(spec): the `systemFields.owner` rescue no longer tells authors that `ownership: 'org'` picks a different principal (#6365) + + `systemFields` has never declared an `owner` key, and the field doc above the + block names one — so an author (or an AI writing metadata) who follows that + prose lands on the block's `guidance.owner` prescription. That prescription + said: + + > `owner_id` injection is governed by the object-level `ownership` property + > (`ownership: 'none'` skips it; `'user'`/`'org'` choose the principal). + + The second half was false. `'org'` does not choose a different principal — it + injects **no** `owner_id` at all. The authority `applySystemFields` consumes, + `resolveInjectedSystemColumns` (`packages/spec/src/data/injected-system-columns.ts`), + admits exactly two spellings: + + ```ts + const owner = + ownershipEligible && (ownership === undefined || ownership === "user"); + ``` + + and the `ownership` property's own JSDoc, ~90 lines above the guidance, already + said so correctly (`org` / `none` — no per-record owner; `owner_id` is NOT + injected). The guidance was the wrong side of that contradiction. + + Why it was worth fixing rather than leaving as prose drift: this is the text an + author is handed at the exact moment they are already confused about where owner + injection is configured, and it sent them to `ownership: 'org'` expecting an + org-keyed owner column. Nothing rejects `ownership: 'org'`, so the mistake + ships silently and every owner-keyed feature quietly does nothing — + owner-scoped RLS, "My" views, owner reports, the first-admin bootstrap handoff. + That is the failure mode the `guidance` machinery exists to prevent, inverted: + a wrong-key rescue handing out a second wrong answer. + + The rescue now states the injection rule as the authority implements it — + `'user'` (or omitted) injects `owner_id`; `'org'` and `'none'` **both** skip it + and no `owner_id` is injected at all — while keeping the two skipping values + visibly distinct in intent (`'org'` for an org-wide catalog, `'none'` for a + junction/link table), since that distinction is the reason the enum carries + both. + + The sibling `guidance.ownership` message is widened in the same pass. It was not + wrong, only out of date: since #5677 / ADR-0117 D1 the `ownership` property + governs **both** record-ownership anchors, so the message now says it decides + whether `owner_id` **and** `owning_business_unit_id` are injected, rather than + naming only the first. + + Text only — no acceptance change. Every value `ObjectSchema` accepted before is + accepted now, every value it rejected is still rejected, and the injection + behaviour is untouched. The new pin tests assert the message's substance against + `resolveInjectedSystemColumns` rather than echoing the sentence, so the + prescription can only stay green while it still describes what the injection + pass really does. + +- 2598216: fix(spec): 墓碑老化时钟改按**确切 key** 起算 —— 无关簇的登记不再替一次退休提前放行 (#5898) + + `scripts/build-schemas.ts` 的检查 (c)(#4650)保证一件事:删掉一条 + `authorable-surface/` 基线行必须自带证明 —— 那个文件正是检查 (a)/(b) 赖以判定的 + 证据,行没了证据也就没了。它承认的三种证明里,第一种是「墓碑已老化」:base 里这条 + 是 `[RETIRED]`,且它的退休登记比当前 major 至少早 `TOMBSTONE_AGE_MAJORS`(= 2)。 + + #4659 把检查 (b) 收口到了 `RETIRED_KEYS_BY_MAJOR` 的确切 key,检查 (c) 的同一套 + **叶名匹配**原封不动地留了下来:拿 key 的叶名去和**全部 major** 的所有 conversion / + migration `surface` 子句做 `endsWith('.' + prop)`,再取 `Math.min`。两个后果都朝 + 「放行」的方向 —— 一条无关登记就能让一个从没被登记过的墓碑通过「有没有登记」这一关, + 而 `Math.min` 保证时钟一律从**最早**的那次巧合起算。 + + 实测:当前 97 条历史墓碑里有 **2** 条今天就可删,而且**两条都是误判**,机制还不一样: + + - `data/Index:type` 被 protocol 11 的 `flow.node.type` 定了年份 —— + `flow-node-http-callout-rename` 里一个 flow 节点的 `type`,和索引类型毫无关系。 + 它自己那条诚实的登记 `object.indexes[].type` 是 major 17,时钟被提前了六个 major。 + - `api/RestApiConfig:requireAuth` 被 major 12 的 `api.requireAuth` 定了年份 —— + 那是 `rest-requireauth-default-flip`,一次**安全默认值翻转**,该 step 自己写着 + 「No metadata shape changed」。它真正的退休是 protocol 17 的 conversion + `stack.api.requireAuth`(#3963)。同一个 surface,不同**种类**的变更,早了五个 major。 + + 现在检查 (c) 读的是检查 (b) 那张表,按确切 `` `${defKey}:${name}` `` 判定, + `build-schemas.ts` 里再没有任何叶名匹配。 + + **历史墓碑不回填,并且因此不可删。** 两条可机械推导的来源都无法诚实定年:叶名匹配 + 正是 #4659 拿掉的那种推断(上面两条误判即为实证);而 + `authorable-surface.json` 的 git 历史始于 `17.0.0-rc.0`,把 97 条全部定在 major 17 + —— 那是基线文件的**出生日期**,不是考据。所以这些行保持未登记,检查 (c) 对它们 + fail-closed:没有条目就无法证明年龄,基线行不许删。可删数从 **2 → 0**(按当前 + `authorable-surface/` 的 100 条墓碑口径同样是 2 → 0)。 + + 要删其中某一行,是一次有意的、可复核的动作:确定该 key 真正的退休 major,把确切 key + 写进 `RETIRED_KEYS_BY_MAJOR`,由检查 (b2) 复核该条目仍指向一个本次构建确实 + tombstone 的 key。⚠ 定不出年份的行不要写 —— 写进这张表的估算,对之后每一道门禁都 + 读作事实。 + + 导出值本身没有变化;`RETIRED_KEYS_BY_MAJOR` 的文档注释更新为它现在同时被检查 (b)/(b2)/(c) + 读取,以及历史墓碑的 fail-closed 口径。 + ## 17.0.0-rc.5 ### Major Changes @@ -7205,6 +9824,7 @@ stack?, code? }`). Neither side had any consumer outside spec; the `/api/v1/notifications`, implemented by `@objectstack/client`, and mirrored by `InboxNotification` in `@objectstack/spec/contracts` (ADR-0030: the bell reads this shape). + - FROM `import { NotificationSchema, type Notification } from '@objectstack/spec/ui'` → TO `import { NotificationSchema, type Notification } from '@objectstack/spec/api'`. **Shape change**: the api row is an inbox record, not a presentation diff --git a/packages/spec/package.json b/packages/spec/package.json index ceb65316de..a8114b00bb 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/spec", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "description": "ObjectStack Protocol & Specification - TypeScript Interfaces, JSON Schemas, and Convention Configurations", "license": "Apache-2.0", "main": "dist/index.js", diff --git a/packages/triggers/trigger-api/CHANGELOG.md b/packages/triggers/trigger-api/CHANGELOG.md index 6dc1cd871f..756d2e78b0 100644 --- a/packages/triggers/trigger-api/CHANGELOG.md +++ b/packages/triggers/trigger-api/CHANGELOG.md @@ -1,5 +1,67 @@ # @objectstack/trigger-api +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/triggers/trigger-api/package.json b/packages/triggers/trigger-api/package.json index 1b31439687..768781de2c 100644 --- a/packages/triggers/trigger-api/package.json +++ b/packages/triggers/trigger-api/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-api", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "Inbound HTTP/webhook flow trigger for ObjectStack — per-flow HMAC-verified endpoints with queue-backed ingestion (ADR-0041)", "main": "dist/index.js", diff --git a/packages/triggers/trigger-record-change/CHANGELOG.md b/packages/triggers/trigger-record-change/CHANGELOG.md index d07e3c9fb7..ef57195299 100644 --- a/packages/triggers/trigger-record-change/CHANGELOG.md +++ b/packages/triggers/trigger-record-change/CHANGELOG.md @@ -1,5 +1,67 @@ # @objectstack/plugin-trigger-record-change +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/triggers/trigger-record-change/package.json b/packages/triggers/trigger-record-change/package.json index 2dfed35dde..d038124708 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": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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)", "main": "dist/index.js", diff --git a/packages/triggers/trigger-schedule/CHANGELOG.md b/packages/triggers/trigger-schedule/CHANGELOG.md index 45e65acafd..d0359904be 100644 --- a/packages/triggers/trigger-schedule/CHANGELOG.md +++ b/packages/triggers/trigger-schedule/CHANGELOG.md @@ -1,5 +1,67 @@ # @objectstack/plugin-trigger-schedule +## 17.0.0-rc.6 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/triggers/trigger-schedule/package.json b/packages/triggers/trigger-schedule/package.json index 83bc76b7fa..1c18b5c86f 100644 --- a/packages/triggers/trigger-schedule/package.json +++ b/packages/triggers/trigger-schedule/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-schedule", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "Schedule flow trigger for ObjectStack — auto-launches flows on a cron/interval/once schedule via the IJobService (ADR-0018)", "main": "dist/index.js", diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index c18a268420..f808cbb174 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,120 @@ # @objectstack/types +## 17.0.0-rc.6 + +### Patch Changes + +- 88f9d94: fix(types,rest): one named unique-violation predicate — a MySQL conflict is 409 UNIQUE_VIOLATION, not 500 (#6250) + + **On MySQL, every unique-constraint conflict came back as `500 INTERNAL_ERROR`.** + The API contract registers `UNIQUE_VIOLATION` as a 409 code + (`packages/spec/src/api/error-code-ledger.zod.ts`), so a front end had no way to + tell "this email is already taken" from "the server fell over" — no retry advice, + no field to point at, and a 5xx in the operator's dashboards for what is an + ordinary client outcome. SQLite and Postgres deployments never saw it, which is + why it survived: their conflict prose happens to contain the words the mapping + looked for. + + **Cause: the conflict verdict was nested inside a leak heuristic.** REST's 409 + branch lived inside the true-branch of `looksLikeInternalErrorLeak()`, keyed on + the substrings `unique constraint` / `unique violation`. MySQL says + `ER_DUP_ENTRY: Duplicate entry '…' for key '…'`, which matches no limb of that + heuristic, so the conflict never reached the `if` at all and fell out of the + terminal `UNCLASSIFIED_FAULT`. Two unrelated questions — "is this a conflict?" + and "would echoing this text leak internals?" — had been fused into one, and + MySQL is where they disagree. + + Measured on the previous release, through the real error mapper: + + ``` + mysql, bare message 500 INTERNAL_ERROR → 409 UNIQUE_VIOLATION + mysql, knex-wrapped SQL 500 DATABASE_ERROR → 409 UNIQUE_VIOLATION + postgres, SQLSTATE only 500 INTERNAL_ERROR → 409 UNIQUE_VIOLATION + sqlite, message 409 UNIQUE_VIOLATION (unchanged) + postgres, message 409 UNIQUE_VIOLATION (unchanged) + ``` + + So the hole was never MySQL-only: the mapping read one of the two channels + drivers use. A Postgres error carrying SQLSTATE `23505` with unremarkable prose + was a 500 as well. + + **New: `isUniqueViolationError(error)`, exported from `@objectstack/types`.** One + named predicate replaces the substring test, reading every channel a driver + uses — `code` (`23505` / `ER_DUP_ENTRY` / `SQLITE_CONSTRAINT_UNIQUE`), `errno` + (`1062`), the message, and one step down the `cause` chain that pool and + query-builder layers wrap with. Its vocabulary is the union of the four + hand-written copies the repo already carried, so routing REST through it cannot + narrow any verdict clients rely on today; an unrecognised error is never a + conflict, because a false 409 tells an SDK not to retry and points the user at a + value that is fine. + + **The internal-leak classifier is byte-identical.** The fix hoists the conflict + question out of it rather than widening its criteria, so nothing else it guards + is reclassified as safe-to-expose. And the 409 body is fixed text: MySQL embeds + the offending user data in its message (`Duplicate entry 'a@b.com' …`) and + Postgres the index and column names, none of which reaches the client. The full + driver text still reaches the server log. + + No action needed. Clients that already handled `409 UNIQUE_VIOLATION` on SQLite + and Postgres now receive it on MySQL too. + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [ae31a19] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [59b794f] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [2f59da0] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [35f7fb4] +- Updated dependencies [85ec26d] +- Updated dependencies [d7e0b42] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [2fdb36e] +- Updated dependencies [761a0ba] +- Updated dependencies [be87153] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] + - @objectstack/spec@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/types/package.json b/packages/types/package.json index e496ca0bef..c99045a807 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/types", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "license": "Apache-2.0", "description": "Shared interfaces describing the ObjectStack Runtime environment", "main": "dist/index.js", diff --git a/packages/verify/CHANGELOG.md b/packages/verify/CHANGELOG.md index e46ec441e4..5e4177626d 100644 --- a/packages/verify/CHANGELOG.md +++ b/packages/verify/CHANGELOG.md @@ -1,5 +1,168 @@ # @objectstack/verify +## 17.0.0-rc.6 + +### Major Changes + +- d48aad5: refactor(driver-sql)!: `analyzeQuery` / `findWithWindowFunctions` 不再吃 `any`,窗口门自带扁平形类型 (#6212 批 A+E) + + #5181(PR #6076)收窄了 `IDataDriver` 声明的六个方法,#6075(PR #6210)让五个驱动的实现跟上。收尾漏下的是**驱动自有、不在 `IDataDriver` 上**的那批查询门:它们同样吃 query AST,签名却是 `any`。本次处理 SQL 驱动的两个。 + + `any` 在 query 参数上不是「对象名没检查」,而是**检查全关**:`where` 的 filter 方言、`orderBy` 的 sort node 形状、`limit`/`offset` 是不是数字,全部被抹掉——而这两个方法体读的恰恰就是这些字段。`$like` 当年就是从同一个口子活到运行时的(cloud#1030、cloud#1053 实测 20 处)。 + + **`analyzeQuery` → `DriverQuery`。** 它是 `explain()` 的实现体,而 `explain()` 本来就声明 `DriverQuery` 并一行转发过来——收窄前这一对是自相矛盾的:契约门声明 AST,它背后的实现声明 `any`。方法体只读 `fields` / `where` / `orderBy` / `limit` / `offset`,全在 `DriverQuery` 内,因此这是一次纯注解:driver-sql 与 driver-sqlite-wasm 实测零报错、零 fixture 改动。 + + **`findWithWindowFunctions` → 驱动本地的扁平形类型**,新导出 `SqlWindowFunctionQuery` / `SqlWindowFunctionSpec`: + + ```ts + import type { SqlWindowFunctionQuery } from "@objectstack/driver-sql"; + + const ranked = await sqlDriver.findWithWindowFunctions("employee", { + windowFunctions: [ + { + function: "rank", + alias: "salary_rank", + partitionBy: ["department"], + orderBy: [{ field: "salary", order: "desc" }], + }, + ], + }); + ``` + + 它**不能**标 `DriverQuery`:`query.windowFunctions` 在 spec 是 `retiredKey()` 墓碑(#4286),`QueryAST['windowFunctions']` 解析为 `undefined`,标上去会让这道门自己已发布文档里的载荷编译不过。类型因此写成 `Omit & { windowFunctions?: SqlWindowFunctionSpec[] }`——契约那一半照旧受检,驱动私有那一半由驱动自己声明。 + + 类型放在驱动层、**不进 `packages/spec`**,是接着 #4286 的判断往下走:那次删掉 `WindowFunctionNodeSchema` 的理由正是它声明了 `field` / `over` / `frame` 这些门从不读的成员;再往 spec 加一套窗口词汇就是反悔那个判断。spec 的删除注记与 `migrations/registry.ts` 的迁移处方里逐字写着的 `{ function, alias, partitionBy?, orderBy? }`,就是这个类型的出处,三处必须始终说同一句话。请求面的墓碑**没有**被重新打开:`analyzeQuery('o', { windowFunctions: [...] })` 依然是编译错误。 + + **顺带(#6212 批 F)**:`@objectstack/verify` 的 `BucketableDriver.aggregate` 从 `query: unknown` 收到 `DriverQuery`。这是一个**已发布**的结构替身,cloud 的 driver-turso 照着它实现——声明 `unknown` 不叫「最小」,叫没检查,并且放任该文件里两处 AST 字面量各自把对象名多写一遍(#5181 的那种冗余)。同时删掉一处 `as never`:那个 cast 只是因为字面量推断把 `'count'` 放宽成了 `string`,注上类型就不需要它了。这里**不预断**驱动自身 `aggregate` 参数类型的收窄(#6212 批 B,排在 #6203 之后)——方法参数按双变比较,驱动那边声明 `any`、`QueryAST` 还是收窄后的类型,都照样满足这个替身。 + + **零运行时改动**,全部是类型注解与两处冗余键的删除(实测全仓驱动无一读 `query.object`)。测试:driver-sql 935、driver-sqlite-wasm 254、driver-turso 804、verify 17、dogfood 520 全绿。 + + **迁移面**:直接调用这两道门的嵌入方,把内联字面量里编译器指出来的键改对即可(TS2353)。本仓实测非测试生产者为零,两道门只有各自驱动包的测试在用,零处需要改动。标 major 的依据与 #5181 / #6075 一致:**源码级破坏性**(调用点内联字面量与 `BucketableDriver` 的导出形状),运行时行为零变化;`check:api-surface` 只记录导出的存在与否、不记录签名,所以这条说明是该变更唯一的下游载体。 + +### Patch Changes + +- Updated dependencies [c2429b0] +- Updated dependencies [f6609e6] +- Updated dependencies [97e7e3c] +- Updated dependencies [53068c1] +- Updated dependencies [259459d] +- Updated dependencies [2bc1876] +- Updated dependencies [1d0faa7] +- Updated dependencies [ad878e7] +- Updated dependencies [6f6fec7] +- Updated dependencies [e8dc61e] +- Updated dependencies [d8e8d9c] +- Updated dependencies [94e749b] +- Updated dependencies [f7d80f4] +- Updated dependencies [ae31a19] +- Updated dependencies [10c4ea9] +- Updated dependencies [8e2bbba] +- Updated dependencies [5b4780b] +- Updated dependencies [8140915] +- Updated dependencies [7b48cf9] +- Updated dependencies [de6b7f1] +- Updated dependencies [01faeb1] +- Updated dependencies [d92ed03] +- Updated dependencies [6517448] +- Updated dependencies [84c86fb] +- Updated dependencies [2a2a9fb] +- Updated dependencies [a2e157c] +- Updated dependencies [95c4227] +- Updated dependencies [2a61116] +- Updated dependencies [d4df105] +- Updated dependencies [9c82b89] +- Updated dependencies [24122a9] +- Updated dependencies [b0d54bf] +- Updated dependencies [b7d3be4] +- Updated dependencies [2a0d65e] +- Updated dependencies [d9bef45] +- Updated dependencies [f549a0d] +- Updated dependencies [881a3cc] +- Updated dependencies [8a88885] +- Updated dependencies [a80302a] +- Updated dependencies [474f131] +- Updated dependencies [4d552af] +- Updated dependencies [bf0ae99] +- Updated dependencies [f09a2e7] +- Updated dependencies [cb3b6cd] +- Updated dependencies [d2b97c3] +- Updated dependencies [1fe436d] +- Updated dependencies [59b794f] +- Updated dependencies [db59e9c] +- Updated dependencies [69787f0] +- Updated dependencies [5d022a1] +- Updated dependencies [042b9ee] +- Updated dependencies [f549a0d] +- Updated dependencies [a36db28] +- Updated dependencies [53ef057] +- Updated dependencies [33e0385] +- Updated dependencies [d0a5ceb] +- Updated dependencies [465c5fc] +- Updated dependencies [c804f19] +- Updated dependencies [c51ffa5] +- Updated dependencies [babddf6] +- Updated dependencies [dbe92a7] +- Updated dependencies [49f208b] +- Updated dependencies [2f59da0] +- Updated dependencies [83a3b1f] +- Updated dependencies [1a53a02] +- Updated dependencies [2604d34] +- Updated dependencies [eb91eba] +- Updated dependencies [643b7c7] +- Updated dependencies [bfe689b] +- Updated dependencies [b70e534] +- Updated dependencies [2c26040] +- Updated dependencies [1fa224a] +- Updated dependencies [3cc8676] +- Updated dependencies [e15bf7e] +- Updated dependencies [3fb42d2] +- Updated dependencies [35f7fb4] +- Updated dependencies [82397b6] +- Updated dependencies [9bc846b] +- Updated dependencies [773f80a] +- Updated dependencies [72847c5] +- Updated dependencies [f8fe47e] +- Updated dependencies [a841151] +- Updated dependencies [85ec26d] +- Updated dependencies [4afdd3e] +- Updated dependencies [9566c38] +- Updated dependencies [d7e0b42] +- Updated dependencies [8e13ca8] +- Updated dependencies [3510e4a] +- Updated dependencies [54299ca] +- Updated dependencies [3264516] +- Updated dependencies [2fdb36e] +- Updated dependencies [cca11e9] +- Updated dependencies [cfb549d] +- Updated dependencies [761a0ba] +- Updated dependencies [d86815e] +- Updated dependencies [be87153] +- Updated dependencies [2bd4e5e] +- Updated dependencies [2598216] +- Updated dependencies [eb7613c] +- Updated dependencies [f7bd4e2] +- Updated dependencies [88f9d94] +- Updated dependencies [1818998] +- Updated dependencies [f549a0d] +- Updated dependencies [e8f435c] +- Updated dependencies [c9bf940] +- Updated dependencies [a682670] + - @objectstack/spec@17.0.0-rc.6 + - @objectstack/objectql@17.0.0-rc.6 + - @objectstack/service-analytics@17.0.0-rc.6 + - @objectstack/runtime@17.0.0-rc.6 + - @objectstack/rest@17.0.0-rc.6 + - @objectstack/service-datasource@17.0.0-rc.6 + - @objectstack/service-automation@17.0.0-rc.6 + - @objectstack/plugin-hono-server@17.0.0-rc.6 + - @objectstack/plugin-sharing@17.0.0-rc.6 + - @objectstack/service-settings@17.0.0-rc.6 + - @objectstack/plugin-auth@17.0.0-rc.6 + - @objectstack/types@17.0.0-rc.6 + - @objectstack/core@17.0.0-rc.6 + - @objectstack/platform-objects@17.0.0-rc.6 + - @objectstack/plugin-security@17.0.0-rc.6 + ## 17.0.0-rc.5 ### Patch Changes diff --git a/packages/verify/package.json b/packages/verify/package.json index db91458f16..9b33c6b30f 100644 --- a/packages/verify/package.json +++ b/packages/verify/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/verify", - "version": "17.0.0-rc.5", + "version": "17.0.0-rc.6", "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",