From 1b8ea6d9b679edfebc5e2763aa97917739507d7a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 2 Aug 2026 10:49:29 +0000 Subject: [PATCH] chore: version packages (rc) --- .changeset/pre.json | 108 +- examples/app-crm/CHANGELOG.md | 163 ++ examples/app-crm/package.json | 2 +- examples/app-showcase/CHANGELOG.md | 131 + examples/app-showcase/package.json | 2 +- examples/app-todo/CHANGELOG.md | 138 + examples/app-todo/package.json | 2 +- examples/embed-objectql/CHANGELOG.md | 71 + examples/embed-objectql/package.json | 2 +- packages/adapters/hono/CHANGELOG.md | 18 + packages/adapters/hono/package.json | 2 +- packages/apps/account/CHANGELOG.md | 65 + packages/apps/account/package.json | 2 +- packages/apps/setup/CHANGELOG.md | 65 + packages/apps/setup/package.json | 2 +- packages/apps/studio/CHANGELOG.md | 65 + packages/apps/studio/package.json | 2 +- packages/cli/CHANGELOG.md | 764 ++++++ packages/cli/package.json | 2 +- packages/client-react/CHANGELOG.md | 68 + packages/client-react/package.json | 2 +- packages/client/CHANGELOG.md | 185 ++ packages/client/package.json | 2 +- packages/cloud-connection/CHANGELOG.md | 70 + packages/cloud-connection/package.json | 2 +- .../connectors/connector-mcp/CHANGELOG.md | 65 + .../connectors/connector-mcp/package.json | 2 +- .../connectors/connector-openapi/CHANGELOG.md | 65 + .../connectors/connector-openapi/package.json | 2 +- .../connectors/connector-rest/CHANGELOG.md | 65 + .../connectors/connector-rest/package.json | 2 +- .../connectors/connector-slack/CHANGELOG.md | 65 + .../connectors/connector-slack/package.json | 2 +- packages/console/CHANGELOG.md | 2 + packages/console/package.json | 2 +- packages/core/CHANGELOG.md | 64 + packages/core/package.json | 2 +- packages/create-objectstack/CHANGELOG.md | 2 + packages/create-objectstack/package.json | 2 +- packages/formula/CHANGELOG.md | 64 + packages/formula/package.json | 2 +- packages/lint/CHANGELOG.md | 451 ++++ packages/lint/package.json | 2 +- packages/mcp/CHANGELOG.md | 67 + packages/mcp/package.json | 2 +- packages/metadata-core/CHANGELOG.md | 115 + packages/metadata-core/package.json | 2 +- packages/metadata-fs/CHANGELOG.md | 7 + packages/metadata-fs/package.json | 2 +- packages/metadata-protocol/CHANGELOG.md | 601 +++++ packages/metadata-protocol/package.json | 2 +- packages/metadata/CHANGELOG.md | 144 ++ packages/metadata/package.json | 2 +- packages/objectql/CHANGELOG.md | 498 +++- packages/objectql/package.json | 2 +- packages/observability/CHANGELOG.md | 98 + packages/observability/package.json | 2 +- packages/platform-objects/CHANGELOG.md | 170 ++ packages/platform-objects/package.json | 2 +- packages/plugins/driver-memory/CHANGELOG.md | 100 + packages/plugins/driver-memory/package.json | 2 +- packages/plugins/driver-mongodb/CHANGELOG.md | 179 ++ packages/plugins/driver-mongodb/package.json | 2 +- packages/plugins/driver-sql/CHANGELOG.md | 219 ++ packages/plugins/driver-sql/package.json | 2 +- .../plugins/driver-sqlite-wasm/CHANGELOG.md | 156 ++ .../plugins/driver-sqlite-wasm/package.json | 2 +- packages/plugins/embedder-openai/CHANGELOG.md | 64 + packages/plugins/embedder-openai/package.json | 2 +- .../plugins/knowledge-memory/CHANGELOG.md | 66 + .../plugins/knowledge-memory/package.json | 2 +- .../plugins/knowledge-ragflow/CHANGELOG.md | 66 + .../plugins/knowledge-ragflow/package.json | 2 +- .../plugins/plugin-approvals/CHANGELOG.md | 216 ++ .../plugins/plugin-approvals/package.json | 2 +- packages/plugins/plugin-audit/CHANGELOG.md | 66 + packages/plugins/plugin-audit/package.json | 2 +- packages/plugins/plugin-auth/CHANGELOG.md | 107 + packages/plugins/plugin-auth/package.json | 2 +- packages/plugins/plugin-dev/CHANGELOG.md | 87 + packages/plugins/plugin-dev/package.json | 2 +- packages/plugins/plugin-email/CHANGELOG.md | 123 + packages/plugins/plugin-email/package.json | 2 +- .../plugins/plugin-hono-server/CHANGELOG.md | 68 + .../plugins/plugin-hono-server/package.json | 2 +- .../plugins/plugin-pinyin-search/CHANGELOG.md | 19 + .../plugins/plugin-pinyin-search/package.json | 2 +- packages/plugins/plugin-reports/CHANGELOG.md | 66 + packages/plugins/plugin-reports/package.json | 2 +- packages/plugins/plugin-security/CHANGELOG.md | 157 ++ packages/plugins/plugin-security/package.json | 2 +- packages/plugins/plugin-sharing/CHANGELOG.md | 193 ++ packages/plugins/plugin-sharing/package.json | 2 +- packages/plugins/plugin-webhooks/CHANGELOG.md | 83 + packages/plugins/plugin-webhooks/package.json | 2 +- packages/qa/dogfood/CHANGELOG.md | 92 + packages/qa/dogfood/package.json | 2 +- packages/qa/downstream-contract/CHANGELOG.md | 64 + 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 | 175 +- packages/rest/package.json | 2 +- packages/runtime/CHANGELOG.md | 494 ++++ packages/runtime/package.json | 2 +- packages/sdui-parser/CHANGELOG.md | 2 + packages/sdui-parser/package.json | 2 +- .../services/service-analytics/CHANGELOG.md | 286 +++ .../services/service-analytics/package.json | 2 +- .../services/service-automation/CHANGELOG.md | 619 +++++ .../services/service-automation/package.json | 2 +- packages/services/service-cache/CHANGELOG.md | 67 + packages/services/service-cache/package.json | 2 +- .../service-cluster-redis/CHANGELOG.md | 65 + .../service-cluster-redis/package.json | 2 +- .../services/service-cluster/CHANGELOG.md | 65 + .../services/service-cluster/package.json | 2 +- .../services/service-datasource/CHANGELOG.md | 183 ++ .../services/service-datasource/package.json | 2 +- packages/services/service-i18n/CHANGELOG.md | 66 + packages/services/service-i18n/package.json | 2 +- packages/services/service-job/CHANGELOG.md | 66 + packages/services/service-job/package.json | 2 +- .../services/service-knowledge/CHANGELOG.md | 65 + .../services/service-knowledge/package.json | 2 +- .../services/service-messaging/CHANGELOG.md | 66 + .../services/service-messaging/package.json | 2 +- .../services/service-package/CHANGELOG.md | 66 + .../services/service-package/package.json | 2 +- packages/services/service-queue/CHANGELOG.md | 66 + packages/services/service-queue/package.json | 2 +- .../services/service-realtime/CHANGELOG.md | 66 + .../services/service-realtime/package.json | 2 +- .../services/service-settings/CHANGELOG.md | 67 + .../services/service-settings/package.json | 2 +- packages/services/service-sms/CHANGELOG.md | 65 + packages/services/service-sms/package.json | 2 +- .../services/service-storage/CHANGELOG.md | 69 + .../services/service-storage/package.json | 2 +- packages/spec/CHANGELOG.md | 2287 ++++++++++++++++- packages/spec/package.json | 2 +- packages/triggers/trigger-api/CHANGELOG.md | 65 + packages/triggers/trigger-api/package.json | 2 +- .../trigger-record-change/CHANGELOG.md | 65 + .../trigger-record-change/package.json | 2 +- .../triggers/trigger-schedule/CHANGELOG.md | 65 + .../triggers/trigger-schedule/package.json | 2 +- packages/types/CHANGELOG.md | 64 + packages/types/package.json | 2 +- packages/verify/CHANGELOG.md | 130 + packages/verify/package.json | 2 +- packages/vscode-objectstack/CHANGELOG.md | 2 + packages/vscode-objectstack/package.json | 2 +- 153 files changed, 12016 insertions(+), 83 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index f32e431261..5f3dc2c039 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -87,11 +87,13 @@ "action-crash-vs-rejection", "action-execute-target-precedence", "action-governance-engine-owned", + "action-no-placement-lint", "action-param-inline-lookup-reference", "action-param-strict-unknown-keys", "action-record-write-discarded-lint", "action-record-writes-runtime-report", "action-required-permissions-server-scope-docs", + "action-strict-envelope-zero", "action-undoable-liveness-corrected", "actions-empty-object-segment-route", "actions-failures-speak-http", @@ -99,6 +101,10 @@ "adapter-hono-auth-wildcard-yields", "adr-0044-revise-service-owned-note", "adr-0053-temporal-matrix-skewed-zone", + "adr-0072-nav-target-refs", + "adr-0078-completeness-gate", + "adr-0078-phase3-webhook-triggers", + "adr-0078-phase4-runtime-warns", "adr-0104-advertise-open-gates", "adr-0104-attestation-adr-note", "adr-0104-d1-media-strict-per-deployment", @@ -148,11 +154,17 @@ "adr-0115-plugin-dev-stub-table-verdict", "adr-0116-followups", "adr-0117-owning-business-unit", + "adr-0118-non-user-actor-contract", + "adr-anchors-authz-sweep", + "adr-anchors-guard", + "agent-code-is-the-record", "agent-knowledge-alias-and-experimental-markers", "agents-md-worktree-staleness", "agents-pd12-alias-retirement-path", "agents-pd12-shim-retired", + "agents-releases-freeze-merge-queue", "agents-spec-generated-artifacts-map", + "aggregate-bulk-dispatch-selected-ids", "aggregate-temporal-output", "aggregation-vocabulary-lockstep", "ai-agent-authoring-and-tools-removal", @@ -177,6 +189,7 @@ "analytics-order-by-display-label", "analytics-query-bare-shape-entry-validation", "analytics-read-scope-bridge-order", + "analytics-record-scoping-and-measure-fields", "analytics-timedimension-projection", "analytics-widget-query-options", "api-exposure-failopen-observability", @@ -194,8 +207,10 @@ "approval-attachment-descriptors", "approval-dead-run-ordering-invariant", "approval-dead-run-record-lock", + "approval-decision-survives-restart", "approval-empty-position-admin-override", "approval-lock-schedule-run-provenance", + "approval-override-audit-marker", "approval-participant-visibility", "approval-pending-approver-groups", "approval-reassign-structured-parties", @@ -203,10 +218,12 @@ "approval-vocabularies-derived", "approvals-expose-lock-record", "approvals-payload-labels", + "approvals-stranded-request-inspection", "approver-live-record-3447", "approver-value-sources-and-dead-slot-warning", "array-form-triggertype-not-silent", "attachment-read-visibility-real-filter-semantics", + "audit-anchor-and-lookup-integrity", "audit-provenance-and-import-vocabulary", "audit-test-static-imports", "auth-catchall-yields-unowned-paths", @@ -214,6 +231,7 @@ "auth-validationerror-4xx-mapping", "authorable-surface-ratchet", "authoring-key-lint-full-coverage", + "authoring-rule-command-coverage-registry", "authz-ledger-flow-runas", "automation-client-resume-screen-flow", "automation-resume-authority-gate", @@ -222,6 +240,8 @@ "batch-dropped-fields-observability", "better-auth-1-7-0-rc-2-and-prod-dep-batch", "better-auth-team-member-count", + "blueprint-formula-expression", + "blueprint-summary-operations", "body-write-lint-message-driver-truth", "boot-api-merge", "builtin-node-config-reconciliation", @@ -238,6 +258,7 @@ "ci-node-eol-guard", "ci-performance-optimization", "ci-rerun-safety-and-timeouts", + "ci-shard-test-core", "ci-test-completeness-guard", "cli-json-pipe-truncation-sweep", "cli-stale-dist-tests-and-project-root", @@ -246,6 +267,7 @@ "client-keys-sharelinks-security", "client-meta-automation-descriptors", "client-packages-lifecycle", + "client-readme-retired-validate-only", "client-retires-parking-spot-read", "client-url-conformance-capstone", "close-approvals-and-record-shares-gaps", @@ -254,8 +276,10 @@ "close-the-eight-reports-rest-gaps", "close-the-final-nine-rest-gaps", "close-the-nine-metadata-rest-gaps", + "cold-boot-flow-bind-read-decorations", "connector-authoring-guide", "connector-descriptors-meet-their-contract", + "connector-template-cluster-removed", "console-09c6a177bb4a", "console-1bb77aa24514", "console-2cb8d78e24ad", @@ -273,20 +297,30 @@ "conversion-notice-channel", "criteria-json-declaratively-required", "cross-object-batch-501-code", + "cross-repo-issue-closer", "current-user-endpoints-kernel-resolver", "d12-fake-inventory-gate", + "dashboard-strict", "data-path-object-existence-gate", "data-query-path-object", + "dataset-percent-scale-chain", "datasource-admin-503-names-its-own-service", "datasource-availability-observability", "datasource-bound-connect-failfast", + "datasource-config-driver-contract", + "datasource-mapping-is-routing", + "datasource-read-replicas-removed", "datasource-routes-catch-service-throws", "datasource-teardown-ownership", "date-bucket-parity-gate", "date-now-default-utc", "datetime-canonical-utc-storage", "datetime-storage-form-memory-mongodb", + "ddl-runtime-token-default", + "decision-branch-routing-enforced", + "decision-output-required-pin", "decision-outputs-surface-3447", + "declarative-cron-job-schedule-envelope", "declared-unique-index-not-legacy", "default-datasource-adopt-seam", "default-datasource-declared", @@ -300,6 +334,7 @@ "dev-plugin-production-hatch-brands", "dev-plugin-protocol-family-removed", "dev-plugin-security-stubs-and-prod-guard", + "discovery-cache-queue-job-no-route", "discovery-data-slot-computed", "discovery-metadata-slot-computed", "discovery-remedy-names-a-real-package", @@ -315,6 +350,7 @@ "docs-audit-changelog-non-exclusion", "docs-drift-nested-package-roots", "docs-drift-skip-test-files", + "docs-extend-faq-reversed-rot", "docs-fieldschema-extend-rot", "docs-plugin-spec-onupgrade-example", "docs-v17-plugin-retirement-notes", @@ -333,6 +369,14 @@ "drop-require-auth", "drop-undeclared-actions-valve", "dropped-fields-bulk-graphql-client", + "dual-source-contracts-convergence", + "dual-source-cross-form-convergence", + "dual-source-export-ratchet", + "duplicate-fix-guard", + "duplicate-package-flow-canonicalization", + "durable-suspended-screen-refetch", + "eighty-donkeys-repeat", + "email-template-materializer-bridge", "empty-capability-answers-501", "empty-group-bucket-key-null", "empty-state-gate-object-surface", @@ -355,21 +399,29 @@ "field-conditional-required-fold", "field-file-collection-dogfood-proof", "field-readonly-doc-preserveaudit", + "field-strict-guidance", "field-time-canonical-storage", "file-access-delegate", "filter-context-tokens-gate", + "filter-logic-conformance-mongodb-wasm", "filter-logic-conformance-single-source", "filter-no-silent-drop", "filter-tokens-runtime-resolver", "find-data-wire-context", + "findone-requires-a-predicate", + "fix-cross-repo-closer-octokit", + "fix-cross-repo-closer-require", "fix-stale-hono-changeset-ref", "fix-stale-scaffolder-changeset-refs", "fix-unmounted-local-file-url", "flow-action-record-id-seeding", + "flow-branch-gates-and-inert-condition", + "flow-condition-bare-string-is-cel", "flow-create-record-write-lint", "flow-error-object-serialization", "flow-executors-parse-config", "flow-filter-collapse-and-write-path-tokens", + "flow-function-declared-effect", "flow-lookup-expand", "flow-max-retries-single-default", "flow-nested-region-walk", @@ -382,8 +434,10 @@ "flow-template-lint-and-hydrate-guards", "flow-template-paths-into-reference-integrity-suite", "flow-trigger-unknown-event-lint", + "form-layout-lint-wired", "form-section-pane", "formview-buttons-defaults-live", + "govern-remaining-nine-metadata-types", "govern-report-dashboard-liveness", "govern-sys-member-writes", "govern-webhook-liveness", @@ -429,6 +483,8 @@ "isLikelyEmail-no-control-char", "job-placeholder-migrates-to-db-adapter", "job-retry-timeout-3494", + "job-runtime-create-closed", + "kernel-metadata-loader-envelope-removed", "keyset-batch-walks", "lazy-deps-dist-probe-timeout", "lifecycle-event-registry-enforced", @@ -440,6 +496,7 @@ "lint-unique-double-declaration", "list-column-prefix-summary-object", "liveness-evidence-path-resolution", + "liveness-governs-every-registered-type", "liveness-ledger-ai-scope-honesty", "liveness-orphan-row-gate", "liveness-register-orphan-proofs", @@ -447,6 +504,7 @@ "liveness-verified-at-clock", "localized-field-validation-messages", "manifest-bridge-arm-on-project-kernels", + "mapping-agent-page-strict", "marketplace-objects-bridge-metadata-service", "marketplace-rehydrate-seed-heal", "marketplace-seed-test-budget", @@ -456,11 +514,17 @@ "memory-datasource-ephemeral-per-pool", "memory-driver-opt-in-persistence", "messaging-declares-its-event-object", + "meta-canonical-type-segment", + "meta-migrate-stored-route", "meta-type-gate-plural", + "metadata-event-dual-source-kernel-side", "metadata-form-zod-reconciliation", "metadata-remove-artifact-api-source", "metadata-type-registration-names-a-real-hook", "metadata-unresolvable-posture-fail-closed", + "metadata-watch-event-canonical-enum", + "metadataformat-cachestrategy-single-declaration", + "migrate-meta-stored-rewrite", "migrate-occupancy-and-deferred-ddl", "migrate-occupancy-file-descriptor-signal", "migrate-plan-lists-datetime-convergence", @@ -472,6 +536,7 @@ "nav-item-input-type", "notifications-redos-fix", "notify-source-shape-conversion", + "object-parse-path-strict", "objectchart-aggregate-result-columns", "objectchart-contract-back-to-spec-shape", "objectql-crossobj-capability", @@ -493,10 +558,16 @@ "plugin-ordering-declared-contract", "plugin-ordering-provider-declarations", "plugin-page-i18n-drift-guard", + "pm-dispatch-agent-tooling", + "pm-dispatch-escalation-bar", + "pm-dispatch-round1-lessons", + "pm-dispatch-triage-lessons", + "pm-dispatch-triage-tooling", "preserveaudit-test-and-docs", "preview-omits-virtual-fields", "previous-null-on-create-leg", "prose-example-gate-covers-docs", + "protection-envelope-invariant-was-hollow", "prune-aspirational-config-3494", "prune-dead-audit-config-cluster", "prune-dead-capabilities-descriptor", @@ -516,8 +587,10 @@ "query-having-enforced", "rbac-objects-bulk-primitive", "react-block-field-props-lint", + "react-blocks-declaration-parity-not-conformance", "react-listview-searchable-fields-lint", "react-page-props-joins-the-suite", + "react-tier-record-blocks-withdrawn", "readme-fde-audience", "readonly-flow-write-json-warning", "readonly-flow-write-lint", @@ -529,6 +602,7 @@ "record-change-hydrate-formula-fields", "recursive-schema-input-pins", "recursive-schema-input-types", + "reference-id-embedded-record", "reference-integrity-object-and-action-names", "reference-integrity-wiring-guard", "regenerate-ui-action-reference-doc", @@ -536,6 +610,7 @@ "region-slots-single-declaration", "region-validator-coverage", "register-field-file-collection-proof", + "registered-types-batch-four", "reject-body-on-non-script-action", "reject-malformed-filter-array", "release-hotcrm-gate-premode", @@ -591,15 +666,21 @@ "runtime-domain-extraction-batch3", "runtime-domain-handler-registry", "runtime-meta-data-extraction", + "runtime-overlay-not-artifact", "runtime-packages-extraction", "runtime-share-links-extraction", "sandbox-structured-error-passthrough", + "save-flow-canonicalization-fallback-warning", + "save-meta-item-flow-canonicalization", "savemeta-persists-normalized-operators", "scaffolds-drop-memory-driver", "schemaless-node-config-contracts", + "schemaless-node-expression-ledger", "scim-provider-key-and-sso-scim-parity", "scoped-invitation-placement", "screen-field-visible-when-on-the-wire", + "screen-resume-declared-field-contract", + "script-branch-keys-retired", "searchable-fields-stale-declaration", "security-get-readable-fields", "security-props-liveness-recheck", @@ -630,6 +711,7 @@ "sharing-rule-criteria-required", "sharing-rule-recipient-reconcile", "sharing-rule-unknown-sort-and-stale-help", + "sharing-rule-withdrawal-and-delete", "showcase-action-disabled-specimen", "showcase-approver-and-picker-specimens", "showcase-bulk-actions-example", @@ -644,6 +726,7 @@ "slot-lookup-type-argument-ratchet", "slot-lookups-return-their-contract", "sort-dotted-path-rejected", + "spec-api-surface-baseline-percent-scale", "spec-changes-manifest-catchup", "spec-check-generated-aggregate", "spec-property-retirement-skill", @@ -661,13 +744,18 @@ "storage-adapter-swap-verdict", "storage-download-filename", "stored-metadata-replays-the-chain", + "stored-migration-covers-flows", + "strict-object-registered-types", "strictness-ledger-gate", "strictness-ledger-recursive-coverage", "strip-read-decorations-on-save", + "studio-strict", + "summary-index-registry-revision", "sweep-close-out", "sys-migration-ledger-platform-infra", "sys-secret-store-platform-infra", "sys-view-definition-default-open", + "system-field-name-injected-columns", "temporal-conformance-matrix", "temporal-conformance-stall-guard", "temporal-conformance-token-axis", @@ -676,17 +764,22 @@ "temporal-hooks-on-contract", "temporal-storage-form-axis-tests", "temporal-time-axis", + "tenancy-default-org-fail-closed", "tender-hats-brush", "test-core-stall-guard", "tests-off-memory-driver", + "tidy-eyes-shine", "tombstone-agent-tools", "tool-inert-keys-removed", "tool-requires-confirmation-not-enforced", "tool-requires-confirmation-removed", + "translation-groups-strict", + "trigger-registry-connector-cluster-removed", "two-factor-lockout-and-object-translations", "two-factor-lockout-extension", "two-factor-lockout-follows-settings", "type-blind-temporal-date-operands", + "type-bulk-action-defs", "typecheck-covers-the-test-layer", "typed-decision-outputs-3447", "ui-discovery-reads-the-protocol-service", @@ -702,25 +795,38 @@ "unordered-paged-read-determinism", "update-record-dropped-field-warnings", "url-field-accepts-relative-urls", + "user-field-implicit-target", "user-less-run-data-ops-refused", "user-level-export-axis", "v17-dissolve-protocol-alias", + "v17-docs-sweep-run-4", + "v17-metadata-options-pointer", "v17-page-console-gap-and-nav", "v17-page-rc1-window", "v17-rc-anchor", "v17-release-page", + "v17-rest-envelope-defects", + "v17-verification-defects-docs", + "v17-watch-event-raw-values-note", "vacuous-filter-carveouts", "validate-runs-build-authoring-lints", + "validation-kind-retired", + "validation-variants-strict", "variant-doc-drift-gate", "variant-docs-exemption-audit", + "verify-harness-durable-suspended-runs", "verify-multitenant-requests-isolated-posture", "view-ast-operator-parity", + "view-strict-final", "wait-loose-config-graduation", "wait-timeout-keys-retired", + "wasm-sqlite-returning-writes-persist", "webhook-authoring-surface-bridge", + "webhook-dual-source-api-side", "webhook-liveness-ledger-flip", "webhooks-drop-dead-delivery-i18n", "wildcard-fallthrough-guard", - "withdraw-adr-0107-drop-writes-proposal" + "withdraw-adr-0107-drop-writes-proposal", + "workflow-slot-retired" ] } diff --git a/examples/app-crm/CHANGELOG.md b/examples/app-crm/CHANGELOG.md index 380a9f1f3c..c967910e4a 100644 --- a/examples/app-crm/CHANGELOG.md +++ b/examples/app-crm/CHANGELOG.md @@ -1,5 +1,168 @@ # @objectstack/example-crm +## 4.0.92-rc.2 + +### Patch Changes + +- 5293114: fix(automation): a decision's three declared ways to route a branch are now one working model (#4414) + + A `decision` node advertised three mechanisms for splitting a path and only one + of them did anything. The other two were the ADR-0049 `declared ≠ enforced` + shape, and the pair of them shipped a guard that does not guard in + `examples/app-crm`. + + | mechanism | before | now | + | :--------------------------------------------------- | :----------------------------------------------------------------------------------------------------- | :---------------------------------------------------- | + | `edge.condition` | ✅ the only one that worked | unchanged | + | `edge.isDefault` | **zero readers** anywhere but the schema declaration | BPMN default flow, enforced in `traverseNext` | + | `decision.config.conditions[].label` → `branchLabel` | matched **0** out-edge labels across every example app, then fell back to the full edge set in silence | routes; an unclaimable label is logged, not swallowed | + + ## What was broken, end to end + + `crm_convert_lead_wizard` means "already converted → abort screen; otherwise → + the wizard". It ran **both**: an already-converted lead got + "This lead has already been converted" and then walked straight into the + conversion wizard behind it. Four independent silences stacked up: + + 1. the decision's first condition was authored `{lead_record.status} == +'converted'` — braces in a slot declared bare CEL, so it was string-compared + and never true; + 2. the second (`'true'`) therefore won, yielding `branchLabel: 'No — proceed'`; + 3. no out-edge carried that label (they were `'Yes'` / `'No'`), so traversal + discarded the branch and considered every out-edge; + 4. `e3b` was unconditional, so it ran regardless — and the natural fix, marking + it `isDefault: true`, was a dead key. + + ## The model + + `branchLabel` narrows the edge set → `condition` gates each edge → `isDefault` + catches whatever is left. Concretely: + + - **`isDefault` is enforced.** A default edge is traversed only when no + conditional sibling of the same source node matched, and it is no longer part + of the unconditional parallel fan-out — that distinction is the whole point of + the marker. Passed over because a real branch won, its target records the same + `skipped` step a closed gate does (#4354). + - **An unclaimable branch label warns.** Traversal still falls back to the full + edge set (a run mid-flight must not die on a metadata error) but says so, + naming the computed branch and the out-edge labels that exist. + - **A decision that declares no `conditions` reports no branch.** It used to + report `'default'` unconditionally — a label no out-edge in the repo ever + carried — which is why every decision node fell back to the full edge set. + The `'default'` sentinel survives for the case it actually describes (declared + conditions, none matched) and is now claimed by the `isDefault` edge as well + as by an edge literally labelled `'default'`. + - **`conditions[].expression` is evaluated as the bare CEL it is declared to + be.** The raw string went to the legacy `{var}` template path, where + `lead.status == 'converted'` cannot resolve and the branch is decided by + string comparison. Unlike `edge.condition` this slot carries no + `ExpressionInput` envelope — the decision descriptor is deliberately + schemaless — so the executor supplies the dialect. A brace-in-CEL predicate + now fails loudly (ADR-0032 §1c) instead of deciding `false`. + + ## Caught at authoring time too + + Four new `os build` / `os validate` warnings, because a wrong route is silent at + run time by nature (Prime Directive #12): + + `flow-branch-label-unmatched` (the shipped shape), + `flow-decision-unconditional-branch` (a guarded decision with an unconditional + sibling — the actual hole), `flow-default-edge-with-condition` and + `flow-multiple-default-edges`. + + Both of the first two fire on the pre-fix `convert-lead.flow.ts` and are silent + after it. + + ## Effect on flows that already exist + + Enforcing `isDefault` changes how a **stored** flow behaves, and the flows it + changes are mostly Studio's own. `objectui`'s flow edge inspector has always + written `isDefault: true` when you bind an out-edge to a decision's default/else + branch — into a key with zero readers, so that edge ran unconditionally, in + parallel with whichever branch actually matched. Those flows now take exactly + one branch. That is the fix, but it is a behaviour change on existing data + rather than only on newly authored metadata, so it is worth knowing before + upgrading: a flow that quietly ran two paths will now run one. + + Nothing changes for an edge that never carried the marker — `isDefault` defaults + to `false`, and an ordinary unconditional out-edge still fans out in parallel + exactly as before. + + ## The example app + + `crm_convert_lead_wizard`'s guard is now a plain exclusive gateway: the + redundant `config.conditions` is gone and `e3b` carries `isDefault: true`. One + mechanism per decision, and exactly one branch runs. + + Verified: 11 new engine/executor tests (including the reported repro in both + directions), 12 new linter tests; `@objectstack/service-automation` 577 tests + and `@objectstack/cli` 652 tests green, all three example apps build with no new + findings. + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [63b33e6] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [ff17642] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [8aacf94] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/runtime@17.0.0-rc.2 + ## 4.0.92-rc.1 ### Patch Changes diff --git a/examples/app-crm/package.json b/examples/app-crm/package.json index ced777f267..6ab24c3626 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.1", + "version": "4.0.92-rc.2", "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 876ff5e0af..ea31b0a30e 100644 --- a/examples/app-showcase/CHANGELOG.md +++ b/examples/app-showcase/CHANGELOG.md @@ -1,5 +1,136 @@ # @objectstack/example-showcase +## 0.3.14-rc.2 + +### Patch Changes + +- d449b0c: fix(cli): gate the two decision-routing shapes that can never work, and flag the inert `config.condition` (#4414) + + Two follow-ups to #4440, both about metadata that reads like a guard and is not + one. + + ## Two rules promoted to `error` + + `flow-branch-label-unmatched` and `flow-default-edge-with-condition` now FAIL the + build instead of warning. The bar for that — restated at the top of + `lint-flow-patterns.ts`, because the old one no longer described the set — is + **no reading of the author's metadata does what it says, deterministically, on + every run**. Both qualify: a branch label no out-edge carries cannot route, and + an edge that is both `isDefault` and conditional always lets the condition win, + so the marker routes nothing. Neither _fails_; both are wrong every time and + silently, which is worse. + + The other two stay advisory on purpose, and the policy now says why: + `flow-decision-unconditional-branch` is usually a guard that does not guard, but + one guarded plus one unconditional out-edge is also a legal "maybe notify, + always continue" fan-out, and `flow-multiple-default-edges` can genuinely mean + "when nothing matched, do both". The bar is about _provability_, not severity of + consequence — failing a customer's build on a shape we cannot prove wrong is the + worse trade. + + No wiring change was needed: `lintFlowPatterns` is already registered as + `tier: 'gating'` across all three commands (#4409), which is exactly the seam + `authoring-rule-wiring.test.ts` exists to guard. + + ## New rule: `flow-inert-node-condition` + + `config.condition` is the trigger gate on a `start` node and is read by **no + other node type** — the engine parse-validates it everywhere (so a malformed one + is caught) and then ignores it. On a `decision` the name makes it read as the + branch predicate, which is exactly how it got authored. + + Three of the three bundled apps had one. `app-todo`'s `check_recurring` and + `app-showcase`'s `needs_exec` both carried a predicate their out-edges were + already enforcing — a third copy doing nothing. The showcase even had a comment + next to it saying the node condition "is not evaluated by the engine", and kept + it anyway; that is the residue this rule exists to stop accumulating. Both are + now plain exclusive gateways. + + Advisory, not gating: the surrounding edges usually still route correctly, so + this is dead weight rather than a provable misroute. The node-type list is a + closed set of builtins we have actually read, not "everything that isn't + `start`" — ADR-0018 keeps `node.type` open and a plugin executor may legitimately + declare and read its own `config.condition`. + + ## Studio + + `objectstack-ai/objectui` carries the matching help-text fixes: the branch editor + said a `true` branch **is** the default/else path (it is how you _ask_ for one — + the marker goes on the out-edge), and the legacy single `Condition` field said + "prefer Branches above", which reads as "this works, but the other is better". + It does not work at all. + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [63b33e6] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [ff17642] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [8aacf94] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/runtime@17.0.0-rc.2 + - @objectstack/service-datasource@17.0.0-rc.2 + - @objectstack/driver-sql@17.0.0-rc.2 + - @objectstack/cloud-connection@17.0.0-rc.2 + - @objectstack/connector-mcp@17.0.0-rc.2 + - @objectstack/connector-openapi@17.0.0-rc.2 + - @objectstack/connector-rest@17.0.0-rc.2 + - @objectstack/connector-slack@17.0.0-rc.2 + ## 0.3.14-rc.1 ### Patch Changes diff --git a/examples/app-showcase/package.json b/examples/app-showcase/package.json index b2f9d3b936..9783b8fd40 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.1", + "version": "0.3.14-rc.2", "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 d73a0c11e3..a8487f5f2f 100644 --- a/examples/app-todo/CHANGELOG.md +++ b/examples/app-todo/CHANGELOG.md @@ -1,5 +1,143 @@ # @objectstack/example-todo +## 4.0.92-rc.2 + +### Patch Changes + +- d449b0c: fix(cli): gate the two decision-routing shapes that can never work, and flag the inert `config.condition` (#4414) + + Two follow-ups to #4440, both about metadata that reads like a guard and is not + one. + + ## Two rules promoted to `error` + + `flow-branch-label-unmatched` and `flow-default-edge-with-condition` now FAIL the + build instead of warning. The bar for that — restated at the top of + `lint-flow-patterns.ts`, because the old one no longer described the set — is + **no reading of the author's metadata does what it says, deterministically, on + every run**. Both qualify: a branch label no out-edge carries cannot route, and + an edge that is both `isDefault` and conditional always lets the condition win, + so the marker routes nothing. Neither _fails_; both are wrong every time and + silently, which is worse. + + The other two stay advisory on purpose, and the policy now says why: + `flow-decision-unconditional-branch` is usually a guard that does not guard, but + one guarded plus one unconditional out-edge is also a legal "maybe notify, + always continue" fan-out, and `flow-multiple-default-edges` can genuinely mean + "when nothing matched, do both". The bar is about _provability_, not severity of + consequence — failing a customer's build on a shape we cannot prove wrong is the + worse trade. + + No wiring change was needed: `lintFlowPatterns` is already registered as + `tier: 'gating'` across all three commands (#4409), which is exactly the seam + `authoring-rule-wiring.test.ts` exists to guard. + + ## New rule: `flow-inert-node-condition` + + `config.condition` is the trigger gate on a `start` node and is read by **no + other node type** — the engine parse-validates it everywhere (so a malformed one + is caught) and then ignores it. On a `decision` the name makes it read as the + branch predicate, which is exactly how it got authored. + + Three of the three bundled apps had one. `app-todo`'s `check_recurring` and + `app-showcase`'s `needs_exec` both carried a predicate their out-edges were + already enforcing — a third copy doing nothing. The showcase even had a comment + next to it saying the node condition "is not evaluated by the engine", and kept + it anyway; that is the residue this rule exists to stop accumulating. Both are + now plain exclusive gateways. + + Advisory, not gating: the surrounding edges usually still route correctly, so + this is dead weight rather than a provable misroute. The node-type list is a + closed set of builtins we have actually read, not "everything that isn't + `start`" — ADR-0018 keeps `node.type` open and a plugin executor may legitimately + declare and read its own `config.condition`. + + ## Studio + + `objectstack-ai/objectui` carries the matching help-text fixes: the branch editor + said a `true` branch **is** the default/else path (it is how you _ask_ for one — + the marker goes on the out-edge), and the legacy single `Condition` field said + "prefer Branches above", which reads as "this works, but the other is better". + It does not work at all. + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [257d97a] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [84b4a3a] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [63b33e6] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [ff17642] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [9b43ee2] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [8aacf94] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [1ee48bc] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [26bb053] +- Updated dependencies [04f1182] +- Updated dependencies [d6bd5a1] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [24915d2] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/objectql@17.0.0-rc.2 + - @objectstack/runtime@17.0.0-rc.2 + - @objectstack/client@17.0.0-rc.2 + - @objectstack/metadata@17.0.0-rc.2 + - @objectstack/driver-sqlite-wasm@17.0.0-rc.2 + - @objectstack/mcp@17.0.0-rc.2 + - @objectstack/knowledge-memory@17.0.0-rc.2 + - @objectstack/service-knowledge@17.0.0-rc.2 + ## 4.0.92-rc.1 ### Patch Changes diff --git a/examples/app-todo/package.json b/examples/app-todo/package.json index ec2bd80645..7caa51c353 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.1", + "version": "4.0.92-rc.2", "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 13e05656e1..e588270407 100644 --- a/examples/embed-objectql/CHANGELOG.md +++ b/examples/embed-objectql/CHANGELOG.md @@ -1,5 +1,76 @@ # @objectstack/example-embed-objectql +## 0.0.32-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [257d97a] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [63b33e6] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [1ee48bc] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [26bb053] +- Updated dependencies [04f1182] +- Updated dependencies [d6bd5a1] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/objectql@17.0.0-rc.2 + - @objectstack/driver-memory@17.0.0-rc.2 + ## 0.0.32-rc.1 ### Patch Changes diff --git a/examples/embed-objectql/package.json b/examples/embed-objectql/package.json index 3fbc732688..fd944c6976 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.1", + "version": "0.0.32-rc.2", "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 1a07281c48..cb5853ce55 100644 --- a/packages/adapters/hono/CHANGELOG.md +++ b/packages/adapters/hono/CHANGELOG.md @@ -1,5 +1,23 @@ # @objectstack/hono +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [2826d1e] +- Updated dependencies [63b33e6] +- Updated dependencies [ff17642] +- Updated dependencies [20bc357] +- Updated dependencies [ac471a0] +- Updated dependencies [eb4204b] +- Updated dependencies [8aacf94] +- Updated dependencies [5a84d41] +- Updated dependencies [ea90179] +- Updated dependencies [dadb43f] + - @objectstack/runtime@17.0.0-rc.2 + - @objectstack/plugin-hono-server@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/adapters/hono/package.json b/packages/adapters/hono/package.json index 6d91148f91..269c1f56b0 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.1", + "version": "17.0.0-rc.2", "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 7791582fa8..342dbc9228 100644 --- a/packages/apps/account/CHANGELOG.md +++ b/packages/apps/account/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/account +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/apps/account/package.json b/packages/apps/account/package.json index 4aeec45021..3bbb8f9c01 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.1", + "version": "17.0.0-rc.2", "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 8673baf977..d68de30b97 100644 --- a/packages/apps/setup/CHANGELOG.md +++ b/packages/apps/setup/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/setup +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/apps/setup/package.json b/packages/apps/setup/package.json index e4b55d1c77..a8368e1f93 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.1", + "version": "17.0.0-rc.2", "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 f1659df075..4800813ccb 100644 --- a/packages/apps/studio/CHANGELOG.md +++ b/packages/apps/studio/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/studio +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/apps/studio/package.json b/packages/apps/studio/package.json index c463701531..1334b66920 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.1", + "version": "17.0.0-rc.2", "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 5d8fb927cc..9f2c4f2927 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,769 @@ # @objectstack/cli +## 17.0.0-rc.2 + +### Minor Changes + +- 0800433: Lint an action nobody placed (ADR-0078 Phase 3, Tier-A `action-locations`). + + New advisory rule `action-no-placement`: an action that declares no + `locations` and that no list view places by name renders on **no** surface — + it parses, publishes, and appears in Setup, while no user can ever click it. + ADR-0078 names this shape in its opening paragraph and Phase 3 asks for + exactly this rule; the shared completeness predicate it envisioned was never + built, so this lands standalone, one verified shape at a time. + + What made it verifiable now: objectui#3142 collapsed four disagreeing + renderers onto one placement predicate. Before that, `action:bar` and the + record header rendered an _undeclared_ action anyway, so the shape only looked + inert on paper. As of objectui 17.1 it is measurably inert. + + Two things are deliberately **not** flagged: + + - **`locations: []`** — the documented headless action (callable over REST / + MCP / AI, no UI surface). ADR-0110 D3 refuses an undeclared handler, so a + headless declaration is the only legal way to expose one. The rule therefore + distinguishes "nowhere, deliberately" (`[]`) from an unstated placement (key + absent) and only reports the latter. + - **Actions a view places by name** — `bulkActions`, `bulkActionDefs` + (including `execution: 'aggregate'` defs, whose whole point is an action with + no single-record home) and `rowActions`, across all three list-view tiers: + `views[i].list`, `views[i].listViews.` and the object-embedded + `objects[i].listViews.`. + + Advisory, never fatal — a view in another installed package may be the one + placing the action, the same reason `validateSemanticRoles` and + `lintLivenessProperties` warn rather than gate. + + Also: the action form schema in `@objectstack/metadata-protocol` no longer + declares `shortcut` / `bulkEnabled`. Both were retired as `retiredKey()` + tombstones in spec 17, and this schema is what the Studio designer renders its + fallback form from — so advertising them handed authors two inputs that could + only ever produce an unsaveable draft (objectui#3145 removed the matching + dedicated controls). And `content/docs/ui/actions.mdx` now says which surface + is the exception to location filtering, instead of a blanket claim its own + showcase contradicted. + +- a7163ea: The ADR-0078 completeness gate ships: a Zod-valid metadata instance that silently does nothing now fails at author time, on every authoring surface. + + This closes the hole _between_ the platform's existing gates. An instance can be Zod-valid (gate 1 green), use only _live_ properties (gate 2 green), and a correctly-authored sibling can be proven to run (gate 3 green) — and still be dead, because it omits a config its consumer needs and the consumer silently no-ops. The founding case (cloud#687): an AI authored `{ type: 'summary' }` with no `summaryOperations`; the engine's index builder skips it, the field reads 0 forever, the dependent "occupancy rate" is stuck at 0 — and the agent reported the work done, because every gate it could see was green. + + **Why this is worse than the unknown-key hole #4001 just closed.** There, the author wrote a key we don't know, and the parse now rejects it with a prescription. Here every key is one we know, the schema is satisfied, nothing warns, and the author gets a success. It manufactures false completion without the author mistyping anything — and the review step that catches a human's bare summary (seeing the field render `0`) is exactly the step AI authoring removes. + + **One shared predicate, every surface — the ADR's core decision.** Instance-completeness checks previously existed _only_ in cloud's AI-build graph-lint, so a stack authored with `os` + a coding assistant, an MCP agent, `os validate` in CI, or by hand got none of them (`formula_without_expression` existed nowhere in the framework). The judgement now lives in `@objectstack/spec/kernel`'s `checkFieldCompleteness` / `checkViewCompleteness` — sibling of `isIncoherentAggregate`, the ADR-0019 pattern — consumed by the new `@objectstack/lint` `validate-functional-completeness` and registered as an author-time rule (28 → 29), so `os build` / `os validate` / `os lint` / MCP / hand authoring are all covered. Cloud graph-lint can re-home its duplicate rules onto the same predicate rather than drifting from it. + + **Every rule cites the runtime line that makes it true**, because the completeness audit's scariest candidate — a "sharing rule fails open and shares every record" — collapsed on a three-file read, and #4001's last two batches shipped four confidently wrong prescriptions before learning the same thing: + + | rule | the silent skip | severity | + | ------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -------- | + | `field/summary-without-operations` | `engine.ts` — `if (!d.summaryOperations) continue` | error | + | `field/formula-without-expression` | `engine.ts` builds the formula plan only from fields that HAVE one | error | + | `field/relationship-without-reference` | `$expand` — `if (!referenceObject) continue` | error | + | `field/choice-without-options` (`select`, `radio`) | `record-validator.ts` — an empty option list disables server-side value validation | error | + | `field/choice-without-options` (`checkboxes`) | same branch, but shared with free-form | warning | + | `view/layout-without-binding` (`kanban`, `calendar`, `gantt`) | renderer falls back to literal default field names | warning | + + **The deliberate NON-rules are pinned as hard as the rules.** `multiselect` without options is _not_ flagged: `record-validator.ts` says verbatim `// free-form (tags without options)`. The runtime blesses it as a mode, which makes it ADR-0078 case (3) "genuinely optional" — flagging it would be another false prescription, and the test is where that attempt fails first. `timeline` / `tree` views are likewise out of v1: they have config schemas, but their renderer behaviour has not had its verification pass. + + **It found a real one on its first run against a real app.** `showcase_field_zoo.f_summary` was a bare `Field.summary({ label: 'Roll-up Summary' })` — one line below an `f_formula` that _is_ complete, in the object whose entire job is to show what each field type looks like. So the canonical example of a roll-up in this repo computed nothing. It could not be fixed by adding `summaryOperations`: a roll-up aggregates a child into its parent, and the zoo is a leaf (`f_master_detail` makes it a child of `showcase_project`, and nothing is a child of the zoo). Removed, with the working examples named — `showcase_invoice.total` for the plain sum, `showcase_expense_report.total_amount` / `approved_amount` for the `summaryOperations.filter` variant. The rule it broke was the file's own: "relationship types point at the other showcase objects so they have REAL targets." + + Tracked in #4544. This is Phase 1; Phase 2 (the cloud authoring-path config-drop fix) is in the `cloud` repo, and Phase 3 lands the Tier-B shapes one verification pass at a time. + +- 4bee182: fix(cli): every author-time rule that can gate runs on all three commands (#4409) + + `os validate`, `os build` and `os lint` each hand-wired their own subset of the + author-time rules. Nothing connected the three lists, so "which rules run here?" + was answerable only by diffing three 800-line files by eye — and the answer + drifted every time a rule landed. The audit found 23 of 26 rules running on some + strict subset, nine of them able to emit `error`. + + The worst direction was the least obvious. `os build` — the command that + PUBLISHES — was the weakest gate of the three: a flow whose expression approver + does not parse (`approval-expression-invalid`) built and published green, and + only `os lint` stopped it, while CI usually runs the other two. `os lint` + disagreed in _both_ directions at once, running one gating rule neither other + command ran and missing six that both of them ran, which is worse than no + pre-flight — the remaining options are re-verifying everything or learning to + distrust the signal. + + This is the same failure mode's fifth appearance (#3583, #3782, #4384/#4394, + #4402). Each earlier repair removed an instance and left the MODE: a rule's + command coverage was whatever its author remembered to type, and forgetting was + silent. #4402's guard could not catch the rest — it filtered on the current + member names of one suite, so a rule hand-wired into two commands from outside + that suite passed it without a word. A name list only guards the names on it. + + **The registry.** `AUTHORING_RULES` declares all 26 rules as data: tier + (`gating`/`advisory`), which stack tier they read (pre-parse `normalized` vs + `parsed`), which commands run them, and a written reason for the one narrowing. + All three commands consume it through `runAuthoringRules()`, so adding a rule is + a one-line edit that reaches every command at once. The three command files + shrink by ~1000 lines between them. + + **The ratchet.** The wiring guard is no longer a name list: a `gating` rule on + fewer than three commands fails, a narrowed rule with no reason fails, a command + that calls or imports a registry rule directly fails, and an `advisory` claim is + checked against the rule's own source — so a gate cannot wear an advisory label + to buy itself partial coverage. That last check is the one #3760 needed, having + promoted a `lintFlowPatterns` rule from advisory to gating with nothing anywhere + asking whether its coverage should follow. Remaining direct calls are listed + with reasons, and a stale entry fails too, so the ratchet cannot rot into a + permanent permission slip. + + **The verdict, not just the wiring.** A separate test plants one defect per + previously-blind gating rule and asserts all three commands gate on it, plus the + issue's own repro driven end-to-end through the real CLI: exit 1 on all three + where it was 1/0/0. + + Two behaviour changes fall out of reporting every failing rule in one run + instead of exiting at the first failing gate: an author with three unrelated + problems now sees all three in one pass, and `--strict` covers every advisory + rather than the roughly half that happened to be printed inline. + + Also closes the same hole one gate over: `collectAndLintDocs` failed `os build` + and never ran on `os validate`, invisible because the parity guard keyed on the + `lint*`/`validate*` naming convention and that gate is called `collect*`. The + guard now names each shared non-registry gate explicitly instead of + pattern-matching for them. + + Cost is not what argued against any of this. The heavy dependencies + (`typescript` ~9 MB, `sucrase`) are already lazy and load only when a stack + carries the metadata that needs them, and the heaviest rule of the set has run + on all three commands as a reference-integrity suite member since #4340 without + anyone noticing. The one narrowed rule, `lintUniqueDeclarations`, is scoped + because `os lint` already reports it through `lintDataModel` — coverage + recorded, not coverage missing. + +- 5293114: fix(automation): a decision's three declared ways to route a branch are now one working model (#4414) + + A `decision` node advertised three mechanisms for splitting a path and only one + of them did anything. The other two were the ADR-0049 `declared ≠ enforced` + shape, and the pair of them shipped a guard that does not guard in + `examples/app-crm`. + + | mechanism | before | now | + | :--------------------------------------------------- | :----------------------------------------------------------------------------------------------------- | :---------------------------------------------------- | + | `edge.condition` | ✅ the only one that worked | unchanged | + | `edge.isDefault` | **zero readers** anywhere but the schema declaration | BPMN default flow, enforced in `traverseNext` | + | `decision.config.conditions[].label` → `branchLabel` | matched **0** out-edge labels across every example app, then fell back to the full edge set in silence | routes; an unclaimable label is logged, not swallowed | + + ## What was broken, end to end + + `crm_convert_lead_wizard` means "already converted → abort screen; otherwise → + the wizard". It ran **both**: an already-converted lead got + "This lead has already been converted" and then walked straight into the + conversion wizard behind it. Four independent silences stacked up: + + 1. the decision's first condition was authored `{lead_record.status} == +'converted'` — braces in a slot declared bare CEL, so it was string-compared + and never true; + 2. the second (`'true'`) therefore won, yielding `branchLabel: 'No — proceed'`; + 3. no out-edge carried that label (they were `'Yes'` / `'No'`), so traversal + discarded the branch and considered every out-edge; + 4. `e3b` was unconditional, so it ran regardless — and the natural fix, marking + it `isDefault: true`, was a dead key. + + ## The model + + `branchLabel` narrows the edge set → `condition` gates each edge → `isDefault` + catches whatever is left. Concretely: + + - **`isDefault` is enforced.** A default edge is traversed only when no + conditional sibling of the same source node matched, and it is no longer part + of the unconditional parallel fan-out — that distinction is the whole point of + the marker. Passed over because a real branch won, its target records the same + `skipped` step a closed gate does (#4354). + - **An unclaimable branch label warns.** Traversal still falls back to the full + edge set (a run mid-flight must not die on a metadata error) but says so, + naming the computed branch and the out-edge labels that exist. + - **A decision that declares no `conditions` reports no branch.** It used to + report `'default'` unconditionally — a label no out-edge in the repo ever + carried — which is why every decision node fell back to the full edge set. + The `'default'` sentinel survives for the case it actually describes (declared + conditions, none matched) and is now claimed by the `isDefault` edge as well + as by an edge literally labelled `'default'`. + - **`conditions[].expression` is evaluated as the bare CEL it is declared to + be.** The raw string went to the legacy `{var}` template path, where + `lead.status == 'converted'` cannot resolve and the branch is decided by + string comparison. Unlike `edge.condition` this slot carries no + `ExpressionInput` envelope — the decision descriptor is deliberately + schemaless — so the executor supplies the dialect. A brace-in-CEL predicate + now fails loudly (ADR-0032 §1c) instead of deciding `false`. + + ## Caught at authoring time too + + Four new `os build` / `os validate` warnings, because a wrong route is silent at + run time by nature (Prime Directive #12): + + `flow-branch-label-unmatched` (the shipped shape), + `flow-decision-unconditional-branch` (a guarded decision with an unconditional + sibling — the actual hole), `flow-default-edge-with-condition` and + `flow-multiple-default-edges`. + + Both of the first two fire on the pre-fix `convert-lead.flow.ts` and are silent + after it. + + ## Effect on flows that already exist + + Enforcing `isDefault` changes how a **stored** flow behaves, and the flows it + changes are mostly Studio's own. `objectui`'s flow edge inspector has always + written `isDefault: true` when you bind an out-edge to a decision's default/else + branch — into a key with zero readers, so that edge ran unconditionally, in + parallel with whichever branch actually matched. Those flows now take exactly + one branch. That is the fix, but it is a behaviour change on existing data + rather than only on newly authored metadata, so it is worth knowing before + upgrading: a flow that quietly ran two paths will now run one. + + Nothing changes for an edge that never carried the marker — `isDefault` defaults + to `false`, and an ordinary unconditional out-edge still fans out in parallel + exactly as before. + + ## The example app + + `crm_convert_lead_wizard`'s guard is now a plain exclusive gateway: the + redundant `config.conditions` is gone and `e3b` carries `isDefault: true`. One + mechanism per decision, and exactly one branch runs. + + Verified: 11 new engine/executor tests (including the reported repro in both + directions), 12 new linter tests; `@objectstack/service-automation` 577 tests + and `@objectstack/cli` 652 tests green, all three example apps build with no new + findings. + +- d449b0c: fix(cli): gate the two decision-routing shapes that can never work, and flag the inert `config.condition` (#4414) + + Two follow-ups to #4440, both about metadata that reads like a guard and is not + one. + + ## Two rules promoted to `error` + + `flow-branch-label-unmatched` and `flow-default-edge-with-condition` now FAIL the + build instead of warning. The bar for that — restated at the top of + `lint-flow-patterns.ts`, because the old one no longer described the set — is + **no reading of the author's metadata does what it says, deterministically, on + every run**. Both qualify: a branch label no out-edge carries cannot route, and + an edge that is both `isDefault` and conditional always lets the condition win, + so the marker routes nothing. Neither _fails_; both are wrong every time and + silently, which is worse. + + The other two stay advisory on purpose, and the policy now says why: + `flow-decision-unconditional-branch` is usually a guard that does not guard, but + one guarded plus one unconditional out-edge is also a legal "maybe notify, + always continue" fan-out, and `flow-multiple-default-edges` can genuinely mean + "when nothing matched, do both". The bar is about _provability_, not severity of + consequence — failing a customer's build on a shape we cannot prove wrong is the + worse trade. + + No wiring change was needed: `lintFlowPatterns` is already registered as + `tier: 'gating'` across all three commands (#4409), which is exactly the seam + `authoring-rule-wiring.test.ts` exists to guard. + + ## New rule: `flow-inert-node-condition` + + `config.condition` is the trigger gate on a `start` node and is read by **no + other node type** — the engine parse-validates it everywhere (so a malformed one + is caught) and then ignores it. On a `decision` the name makes it read as the + branch predicate, which is exactly how it got authored. + + Three of the three bundled apps had one. `app-todo`'s `check_recurring` and + `app-showcase`'s `needs_exec` both carried a predicate their out-edges were + already enforcing — a third copy doing nothing. The showcase even had a comment + next to it saying the node condition "is not evaluated by the engine", and kept + it anyway; that is the residue this rule exists to stop accumulating. Both are + now plain exclusive gateways. + + Advisory, not gating: the surrounding edges usually still route correctly, so + this is dead weight rather than a provable misroute. The node-type list is a + closed set of builtins we have actually read, not "everything that isn't + `start`" — ADR-0018 keeps `node.type` open and a plugin executor may legitimately + declare and read its own `config.condition`. + + ## Studio + + `objectstack-ai/objectui` carries the matching help-text fixes: the branch editor + said a `true` branch **is** the default/else path (it is how you _ask_ for one — + the marker goes on the out-edge), and the legacy single `Condition` field said + "prefer Branches above", which reads as "this works, but the other is better". + It does not work at all. + +- eb4204b: feat(automation): a `script` node's purity contract is declared, and a function that writes can say so (#4396) + + The `script` executor's contract — _the named function returns a value; data I/O + stays on the flow graph_ — existed only as a comment inside the executor, while + #4354's run summary depended on it. That summary reports no record metrics for a + `script` step precisely because a pure function's writes are downstream + `create_record` / `update_record` nodes counting themselves. A function that + wrote anyway made its run report `selected: 30, acted: 0` — indistinguishable + from the broken sweep the counters exist to detect, recorded permanently on + `sys_automation_run`. + + **The rule is now visible.** `ActionDescriptor` carries + `handlerContract: 'none' | 'pure'`, and the `script` descriptor publishes + `'pure'`, so the action catalog, the designer palette and the reference docs + state the rule an author has to follow instead of an executor holding it + privately. + + **And a legitimate writer can opt out honestly.** A `defineStack({ functions })` + entry may declare what it does, in either shape: + + ```ts + defineStack({ + functions: { + scoreLead: (ctx) => ({ score: 42 }), // pure — the default + syncBilling: { handler: syncBilling, effect: "writes" }, // declared writer + }, + }); + ``` + + A step calling a declared writer reports `unmeasuredEffect`, so the run's + `unmeasured` tally keeps the broken-sweep query + (`selected > 0 AND acted = 0 AND unmeasured = 0`) off that flow — and only that + flow. Marking _every_ `script` step unmeasured was rejected: it would blind the + detector on every flow that calls any function in order to cover the few that + break the rule. + + Nothing here is retired or renamed: a bare `functions: { fn }` entry is + unchanged and means `effect: 'pure'`. The declaration is carried end to end — + `ObjectQL.registerFunction` accepts `{ packageId, effect }` alongside the + existing `packageId` string and exposes `resolveFunctionEntry(name)`, + `objectstack build` lowers a declared entry without dropping it, and the + artifact loader re-attaches the module's callable to the declaration the JSON + carried. + + **Also fixed:** `bindHooksToEngine` returned before registering a bundle's + functions when the stack declared no hooks, so a flow-only app's + `defineStack({ functions })` reached the engine as nothing and every `script` + node calling one failed with "no function named 'x' is registered". + +- 63b33e6: Wire `validateFormLayout` into the authoring-rule registry, and close the + registry from the other direction (#4449). + + `validateFormLayout` was implemented, unit-tested, exported from + `@objectstack/lint` and given published rule ids (`form-field-unknown`, + `absolute-colspan-discouraged`) — and **no command ever called it**. It ran on + zero stacks for as long as it existed, so a form section referencing a field + that is not on the bound object, or pinning an absolute `colSpan` under a + per-surface derived column count, produced no output anywhere. It is now an + `advisory` entry in `AUTHORING_RULES`, so `os validate`, `os build` and + `os lint` all run it. It is a pure structured-metadata walk with no lazy + dependency, so all three commands pay nothing measurable. + + The wiring guard (#4409) could not have found this. Every one of its invariants + starts FROM a registry and looks at the commands, which is blind by construction + to a rule that never entered a registry — the same shape as #4402's name list + guarding only the names on it, one layer up. The guard now also runs the reverse + subtraction: every `validate*` / `lint*` symbol on `@objectstack/lint`'s public + barrel, minus `AUTHORING_RULES` ∪ `REFERENCE_INTEGRITY_RULES`, must be empty or + carry a written reason in `UNWIRED_RULE_LEDGER`. The ledger ships empty: today's + difference was exactly this one rule. + +### Patch Changes + +- 8aacf94: fix(metadata-protocol): `duplicatePackage` stops minting pre-protocol flow rows (#4498) + + `duplicatePackage` canonicalizes each source row before re-saving it, under a + stated guarantee: "duplication never mints new rows in a pre-protocol dialect." + It delivered that through `convertStoredItem`, which opens with + `if (singular === 'flow') return { item: data, notices: [] }` — so for flows the + guarantee was **not** delivered. + + It did not fail loudly either. `FlowNodeSchema.config` is an open `z.record`, so + a pre-17 body (a `delete_record` carrying `config.filters`) sails through + `saveMetaItem`'s schema gate and lands verbatim in a brand-new row. + + **Why this mattered more than an un-migrated row.** ADR-0087 justifies the whole + stored-metadata design on new writes always being canonical, _therefore_ the + stored pass being "a strictly shrinking concern". `duplicatePackage` was a live + producer contradicting that for flows: an operator could run + `os migrate meta --stored --apply`, get a clean report, duplicate a package, and + be back to having pre-protocol rows — with the report still saying protocol N + until the next run. + + **The capability was already reachable.** The reason for the flow skip is real — + flow-node conversions carry ADR-0078's open-namespace conflict guard, which needs + the automation engine's live executor registry to tell a rename from a clobber. + But the protocol is constructed with an accessor for the kernel's service table + (the same one `analytics` and `package` are read from), and the automation + service registers under `automation`. A new private `resolveFlowCanonicalizer` + reads `canonicalizeStoredFlow` (#4454) off it, so every caller running next to a + live engine gets flow coverage without threading anything. + + - **`duplicatePackage`** canonicalizes flow rows through it. A refused rename + fails that item into the existing `failed[]` naming the token — copying the + un-renamed body would mint exactly the row this fixes. A flow that cannot + canonicalize fails the same way. With no engine reachable (a control-plane or + metadata-only host) the source body is copied as-is: no worse than the source + row already is, and failing an unrelated duplication over it would be its own + regression. + - **`migrateStoredMetadata`'s `canonicalizeFlow` becomes an override.** It now + defaults to the resolver. The CLI stopped passing one — it boots its inert + engine into the same kernel, so both routes reached the same instance, and two + routes to one capability is how they drift. The parameter stays for callers + with no registry and for testing the flow branch without an engine. + - **Resolution is lazy, per call.** Plugin init order does not guarantee + `automation` is in the table when the protocol is assembled (the CLI adds it + after ObjectQL by design), so caching `undefined` from a too-early read would + disable flow canonicalization for the life of the process. + + Two smaller honesty fixes ride along: a source item that fails _conversion_ (a + tombstoned key throws) is now reported as such instead of as `unparseable +metadata`, and `migrateStoredMetadata`'s "no engine" skip reason says no + automation service is reachable rather than blaming the caller for not supplying + one. + + Reads are unchanged. `getMetaItems` / `getMetaItem` / `getMetaItemLayered` / + `loadMetaFromDb` still skip flows — they are reads, covered by `registerFlow` + canonicalizing at execution, and are not producing bad data. Duplication was the + one that writes. + +- 4f13be2: Liveness coverage is complete: the nine remaining registered metadata types are + governed (#4488) — `app`, `book`, `doc`, `email_template`, `job`, `mapping`, + `seed`, `translation`, `validation` — and `PENDING_GOVERNANCE` is empty. Every + type in the metadata-type registry now has a ledger with per-property verdicts, + evidence, and a `verifiedAt` stamp. + + Spec: + + - Nine new ledgers under `packages/spec/liveness/` (≈150 verdicts). Highlights: + the ENTIRE `email_template` authoring surface is dead (nothing materializes + metadata items into the `sys_email_template` rows `sendTemplate` reads — an + admin editing the password-reset mail in Studio changes nothing; #4509); + `app.areas[].visible` / `areas[].requiredPermissions` are fail-open dead + gates (item-level siblings ARE enforced); `translation.validationMessages` + is read by nothing while #3778's own migration table steers authors into it; + `job`/`validation` have runtime-authoring doors disconnected from their + execution points (#4509). `doc` and `seed` are fully live. + - `check-liveness.mts`: the walker now sees through `z.preprocess` pipes + (takes the OUT side when the IN side is a transform) — `translation`'s + registered schema was unwalkable before this. + - `liveness/README.md`: the per-type count table's method is now decided and + recorded (it mirrors `check-liveness.mts --json` `byStatus`, the number CI + enforces); all rows regenerated from one run, and the two-generations-stale + `webhook` row rewritten to the post-#3489/#3494 state. + + CLI: + + - `lint-liveness-properties` registers the six newly governed types that carry + `authorWarn` entries (`apps`, `books`, `jobs`, `emailTemplates`, `mappings`, + `translations`), so authors hear about the misleading keys at compile time. + +- 0f9faa2: The liveness gate now governs every registered metadata type (#4487) + + `GOVERNED` in `check-liveness.mts` was a hand-maintained list, and nothing ever + compared it against the registry it claims to cover. It governed **15 of 25** + registered metadata types while reporting itself complete. A type in the other + ten was authorable — served by `/api/v1/meta/types/:type`, editable in Studio — + and was never asked who reads its properties, so an inert key on it was + invisible to CI and its silence read as success. + + `datasource` was in that state for its entire life. #4410, #4465 and #4481 found + six inert keys on it **by hand**, two of them security-shaped: `schemaMode` was + dropped between the record and the connection spec, so a database ObjectStack + must never run DDL against was constructed as `managed`; `ssl` stopped at the + record, so a TLS block with a CA certificate in it configured nothing while + looking identical to one that worked. + + **The gate is now answerable to the registry.** Every registered type must be in + `GOVERNED` or in `PENDING_GOVERNANCE` with a reason and an issue. Registering a + type and forgetting the ledger fails CI with the entry to write. The reverse rots + too, so it also fails: a `PENDING_GOVERNANCE` row for a type that has since been + governed claims a debt that no longer exists. + + **`datasource` is now governed** — `liveness/datasource.json`, all 43 properties + classified with evidence. The result is the highest dead ratio of any governed + type: **20 of 43 have no runtime consumer.** + + | Dead cluster | Why | + | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | `capabilities.*` (11) | The engine gates pushdown on the runtime driver's own `supports.*` object — `autonumber`, `batchSchemaSync`, `queryDateGranularity` — a different mechanism whose vocabulary does not overlap this block at all. `having-filter.ts` says it outright: "SQL pushdown can come later behind a driver capability flag." | + | `healthCheck.*` (3) | Nothing schedules a datasource probe. Liveness is checked on demand through the driver handle's `ping()`. | + | `retryPolicy.*` (4) | No connect or query path retries. | + | `external.label`, `external.requirePermission` | No reader. | + + **One correction ships with this**, and it is the reason the audit was worth + doing rather than a bookkeeping exercise. `capabilities.readOnly` reads as a + safety switch and gates nothing — and **two shipped prescriptions pointed + authors at it**: the `externalSettingsUnknownKeyError` guidance in + `datasource.zod.ts` ("or `capabilities.readOnly` to describe the driver") and + the #4465 changeset's relocation table. Both now name `external.allowWrites: +false`, which is the write gate the ObjectQL engine actually checks. An author + who followed the old advice believed they had marked a datasource non-writable + and had not. The v17 release notes carried a matching false claim — that an + unregistered `capabilities` key made the engine stop pushing work down to the + driver — corrected in the same change. + + Two traps worth naming, because both nearly produced a wrong verdict here: + + - **`healthCheck` and `retryPolicy` are name collisions.** A bare grep for + either returns plenty of live readers — the plugin health monitor, `hook`, + `job` — none of which is this type. `hook.retryPolicy` even spells its delay + `backoffMs` where this declares `baseDelayMs`; the shape mismatch is the tell + that nothing reads both. + - **objectui's `DatasourcePreview` renders `pool`, `ssl`, `retryPolicy` and + `healthCheck` as panels**, and is cited as evidence for none of them. That is + the standing rule in `liveness/README.md`, and #4481 is the fresh precedent: + the only "consumer" of `readReplicas` in either repo was a preview pill. + + The CLI advisory lint picks the ledger up automatically, so `os compile` now + warns an author who sets any of the 20. That needed one line beyond the ledger — + `datasource` had to be added to `TYPE_COLLECTIONS`. Coverage grows by marking + entries `authorWarn` only _within_ a type the lint already walks; a newly + governed type needs its collection registered or its ledger warns nobody. + + Nine types remain ungoverned and are now enumerated rather than implied: + `app`, `book`, `doc`, `email_template`, `job`, `mapping`, `seed`, `translation`, + `validation` (#4488). + +- 83cf2d3: feat(migrate,metadata-protocol): `os migrate meta --stored` rewrites sys_metadata rows so the read-path chain has a finish line (#4327) + + #4317 closed the correctness gap from the read side: every stored-row + rehydration seam replays the full ADR-0087 conversion chain, retired entries + included, so a row written under any past protocol is _served_ canonical + forever. What it deliberately did not do is make the rows themselves canonical. + A pre-17 row keeps its legacy bytes, the chain re-lowers it on every load, and + each affected row logs one conversion notice per process — deduped, but back + every boot. Until now the only things that ever rewrote such a row were a Studio + re-save and `duplicatePackage`. + + **`os migrate meta --stored`** is the pass that ends it for a deployment that + runs it. It walks `sys_metadata` — `active` and `draft`, every organization — + replays the same `applyConversionsToStoredItem` chain, and re-saves each changed + body through the normal write path, so a rewritten row gets a + `sys_metadata_history` entry, a fresh checksum and the mutation projectors, + exactly like an author's save. The history row's `source` is `migrate-stored`, + so a later diff distinguishes an upgrade from somebody's edit. + + ```bash + os migrate meta --stored # preview: per-row report, writes nothing + os migrate meta --stored --apply # rewrite the rows (prompts) + os migrate meta --stored --apply --yes --json # CI / scripts + os migrate meta --stored --type view # restrict to a type (repeatable) + ``` + + **Preview is the default and `--apply` is the only writing mode** — the house + rule its siblings already keep (#3617's "a dry run changes nothing"), and it + applies with more force here because what moves is metadata: every affected + row's checksum and a history entry per row. An apply run also refuses to start + while another process holds the SQLite database, for the same reason + `os migrate files-to-references --apply` does. + + **Nothing gates on this having run.** #3855's conclusion stands — an + operator-run migration cannot be relied upon, so the read path remains the + guarantee for every deployment, and no `sys_migration` flag is recorded (a flag + would advertise enforcement that does not exist). What a run buys is hygiene — + rows stop carrying pre-protocol dialects, so diffs, exports and history are + clean going forward, and the recurring notices go quiet — plus one thing that + was previously unobtainable: **an operator can assert it.** A run with nothing + left to do exits `0`, a deployment with rows still on an old dialect exits `1`, + so "my metadata is on protocol N" becomes a CI check rather than a belief. + + Three things the pass declines, and reports rather than counting as done: + `flow` rows (their seam is `AutomationEngine.registerFlow`, which holds the + executor registry the node-type conflict guard needs), types with no repository + write path (`agent` — rewriting there would record no history and force a draft + live), and rows that still fail the current schema after conversion (a genuine + contract violation the write path is right to refuse; it keeps reading through + the chain and stays fixable in Studio). + + Also new, and usable without the CLI: `protocol.migrateStoredMetadata()` returns + the same structured report an admin route would render, and `saveMetaItem` + accepts an optional `source` for the history/audit rows. `source` is not + request-derived — the REST layer builds its save request field by field and + never forwards a client-supplied value, so provenance stays something the server + states rather than something a caller claims. + +- 304423e: feat(automation,migrate): `os migrate meta --stored` now covers flow rows too (#4454) + + #4327 gave the stored-metadata conversion chain a finish line for every + metadata type except `flow` — the one type where the most stored dialect + actually lives, since the graduated conversions `flow-node-crud-filter-alias`, + `flow-node-crud-object-alias`, `flow-node-notify-config-aliases` and + `flow-node-script-config-aliases` are all flow-node entries. Flow-node + conversions carry ADR-0078's open-namespace conflict guard, which has to consult + the _live_ executor registry to tell a rename from a clobber, and the metadata + layer has no way to obtain one. Flows were reported `skipped` with that reason. + They are now converted. + + **One canonicalization policy, two shapes.** + `AutomationEngine.canonicalizeStoredFlow` is the single implementation and + `registerFlow` calls it, so the load seam and the migration can never disagree + about what "canonical" means. It returns `parsed` (for execution — the + `FlowSchema.parse` + #4347 region output, schema defaults materialized) and + `storable` (for persistence). + + **`storable` excludes schema defaults, and that is the load-bearing decision.** + Measured rather than assumed: driving a pre-17 flow through all three steps + _removes_ nothing — `FlowSchema` is strict since #4001, so an unrecognized key + throws instead of being silently dropped, which means the + `graftNormalizedOperators` precedent (it exists because the _view_ parse strips + Studio-only auxiliary keys) does not transfer — and _adds_ only defaults: + `version`, `runAs`, per-edge `type` / `isDefault`. Persisting a default the + author never wrote would pin every migrated row to today's value while untouched + rows follow tomorrow's: two populations with different behaviour, which is + exactly the drift this pass exists to remove. So the write-back is the + conversion result plus the `{dialect, source}` envelopes the schema derives for + edge conditions, and nothing else. + + One subtlety worth knowing if you extend this: that envelope is a schema + transform, not a conversion, so it emits **no** notice while still changing the + body. Reading notices alone — correct for every other metadata type — would call + such a row canonical and leave it re-deriving on every boot. Both passes are + copy-on-write, so identity is the exact test for flows. + + **New: `AutomationServicePluginOptions.armRuntime`** (default `true`, so every + server, dev stack and test host is unaffected). Set `false` and the plugin + brings up the engine and the complete node registry — built-ins plus whatever + `automation:ready` contributes, because a _partial_ registry would make the + conflict guard read a live custom node type as unowned and rewrite over it — and + then stops before anything is armed: + + | Skipped when `armRuntime: false` | Why it must be | + | -------------------------------------------------------- | --------------------------------------------------------------------------------------------- | + | flow pull + `kernel:ready` / `metadata:reloaded` re-sync | `registerFlow` calls `activateFlowTrigger` — record triggers and scheduled jobs would go live | + | declarative connector materialization | opens real connections; an MCP provider spawns a child process | + | suspended-run wait-timer re-arm | would resume someone's paused approval mid-migration | + + `os migrate meta --stored` boots the plugin in that mode. A migration process + must not become a second server. + + A refused rename — the guard firing because the old node-type token is a live + name something else owns in this environment — fails that row loudly, naming the + token and its owner. Never a silent skip, never a clobber. A flow that cannot + canonicalize at all (a strict-schema violation, a malformed control-flow region) + is reported as failed with the parse message rather than persisted as a guess; + such a row cannot register today either, so the report is telling you about a + flow that is already broken at runtime. + +- Updated dependencies [0800433] +- Updated dependencies [80334c7] +- Updated dependencies [85a966f] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [257d97a] +- Updated dependencies [e6b1b69] +- Updated dependencies [328ccc5] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [c2a1134] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [84b4a3a] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [63b33e6] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [ff17642] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [8aacf94] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [9b43ee2] +- Updated dependencies [ec975f1] +- Updated dependencies [68c02c2] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [63b33e6] +- Updated dependencies [8aacf94] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [83cf2d3] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [1ee48bc] +- Updated dependencies [705e5c8] +- Updated dependencies [f61edce] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [ba5ff2f] +- Updated dependencies [304423e] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [26bb053] +- Updated dependencies [04f1182] +- Updated dependencies [c03108c] +- Updated dependencies [d6bd5a1] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [c2a1134] +- Updated dependencies [48fbacb] +- Updated dependencies [24915d2] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/lint@17.0.0-rc.2 + - @objectstack/metadata-protocol@17.0.0-rc.2 + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/objectql@17.0.0-rc.2 + - @objectstack/plugin-webhooks@17.0.0-rc.2 + - @objectstack/plugin-security@17.0.0-rc.2 + - @objectstack/service-analytics@17.0.0-rc.2 + - @objectstack/service-automation@17.0.0-rc.2 + - @objectstack/plugin-approvals@17.0.0-rc.2 + - @objectstack/rest@17.0.0-rc.2 + - @objectstack/runtime@17.0.0-rc.2 + - @objectstack/client@17.0.0-rc.2 + - @objectstack/service-datasource@17.0.0-rc.2 + - @objectstack/driver-sql@17.0.0-rc.2 + - @objectstack/observability@17.0.0-rc.2 + - @objectstack/metadata@17.0.0-rc.2 + - @objectstack/plugin-sharing@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + - @objectstack/plugin-email@17.0.0-rc.2 + - @objectstack/driver-mongodb@17.0.0-rc.2 + - @objectstack/driver-sqlite-wasm@17.0.0-rc.2 + - @objectstack/plugin-auth@17.0.0-rc.2 + - @objectstack/driver-memory@17.0.0-rc.2 + - @objectstack/verify@17.0.0-rc.2 + - @objectstack/account@17.0.0-rc.2 + - @objectstack/setup@17.0.0-rc.2 + - @objectstack/cloud-connection@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/formula@17.0.0-rc.2 + - @objectstack/mcp@17.0.0-rc.2 + - @objectstack/plugin-audit@17.0.0-rc.2 + - @objectstack/plugin-hono-server@17.0.0-rc.2 + - @objectstack/plugin-reports@17.0.0-rc.2 + - @objectstack/service-cache@17.0.0-rc.2 + - @objectstack/service-job@17.0.0-rc.2 + - @objectstack/service-messaging@17.0.0-rc.2 + - @objectstack/service-package@17.0.0-rc.2 + - @objectstack/service-queue@17.0.0-rc.2 + - @objectstack/service-realtime@17.0.0-rc.2 + - @objectstack/service-settings@17.0.0-rc.2 + - @objectstack/service-sms@17.0.0-rc.2 + - @objectstack/service-storage@17.0.0-rc.2 + - @objectstack/trigger-api@17.0.0-rc.2 + - @objectstack/trigger-record-change@17.0.0-rc.2 + - @objectstack/trigger-schedule@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + - @objectstack/plugin-pinyin-search@17.0.0-rc.2 + - @objectstack/console@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 253ad9351d..f6aec35c08 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cli", - "version": "17.0.0-rc.1", + "version": "17.0.0-rc.2", "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 e0dd1ddad2..3f73f707d0 100644 --- a/packages/client-react/CHANGELOG.md +++ b/packages/client-react/CHANGELOG.md @@ -1,5 +1,73 @@ # @objectstack/client-react +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [84b4a3a] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [8aacf94] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/client@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/client-react/package.json b/packages/client-react/package.json index 78e0165adb..f5fd2811d6 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.1", + "version": "17.0.0-rc.2", "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 0b99eebaaa..cdad33992e 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -1,5 +1,190 @@ # @objectstack/client +## 17.0.0-rc.2 + +### Major Changes + +- dadb43f: refactor(spec,client,metadata-protocol,runtime)!: retire the workflow service slot — declared end to end, implemented nowhere (#4451) + + The `workflow` slot was ADR-0078's silently-inert declaration at every layer at + once: a `CoreServiceName` nothing ever registered or resolved (ADR-0115 + Evidence 5 — "no code in this repository resolves either slot", verified across + both repositories), an `IWorkflowService` contract with zero implementations, a + `WorkflowProtocol` whose three methods no code ever provided, a discovery + `routes.workflow` field no builder could truthfully populate, and a + `/api/v1/workflow` advertisement for a path no host ever mounted (the + pre-#3586 `DEFAULT_DISPATCHER_ROUTES` already listed it among routes that + never existed). The capability it promised is live elsewhere and has been for + majors: record state machines are enforced by the `state_machine` validation + rule, approvals are first-class flow nodes on the approvals runtime + (ADR-0019), and record-triggered automation is lifecycle hooks + + `record_change` flows (`service-automation`). + + FROM → TO: + + - `CoreServiceName 'workflow'` / `ServiceRequirementDef.workflow` / + `CORE_SERVICE_PROVIDER['workflow']` → removed; there is no slot to fill. + - `IWorkflowService` (`@objectstack/spec/contracts`) → removed; no + implementation ever existed. Register nothing — use the mechanisms above. + - `WorkflowProtocol` + `GetWorkflowConfigRequest/Response`, + `WorkflowState`, `GetWorkflowStateRequest/Response`, + `WorkflowTransitionRequest/Response` (`@objectstack/spec/api`) → removed, + along with the seven published JSON schemas. Delete the import; nothing + ever answered these shapes. + - Discovery `routes.workflow` / `services.workflow` / `features.workflow` + (metadata-protocol + runtime builders) → absent. A reader keying on them + only ever saw `unavailable` / `false`; delete the read. + - `RouterConfig.mounts.workflow` → removed; there was never a surface to + mount at it. + - `RestApiRouteCategory 'workflow'` → removed; categorize automation-adjacent + routes as `'automation'`. + - `@objectstack/client` re-exports of the four workflow types → removed with + their source. (The `client.workflow.*` methods were already removed earlier + in the v17 cycle — this retires the types they returned.) + - Also removed: the stray `graphql` entry in `CORE_SERVICE_PROVIDER` and the + `graphql: { route: '/graphql' }` discovery entry — `graphql` was never a + `CoreServiceName`, and the dispatcher had already dropped `/graphql` as out + of the product plan (#2462 follow-on). + + The retirement kit: the `workflow-service-slot-retired` semantic migration + (major 17) carries this prescription into `spec-changes.json`, the generated + upgrade guide and the `spec_changes` MCP tool. These are TS/API surfaces and a + discovery response field — never stored in stack metadata — so there is no + load-path conversion and nothing for `os migrate meta` to rewrite; the + 21 `authorable-surface.json` baseline lines and 7 `json-schema.manifest.json` + entries for the deleted schemas are dropped deliberately in the same change + (the plugin-runtime precedent: a prescription nobody can receive is noise — + nothing parses these shapes any more). + +### Minor Changes + +- 8aacf94: feat(rest,runtime,client): `POST /meta/_migrate-stored` — run the stored-metadata migration without a shell (#4327) + + `os migrate meta --stored` (#4327) gave ADR-0087's stored-metadata chain a finish + line, but only for someone who can reach the deployment's database from a + terminal. A hosted operator cannot, so on a managed deployment the chain had no + finish line at all — just the per-read conversion, running forever, with no way + to assert what protocol the rows are on. + + The same pass is now reachable over HTTP: + + ```ts + const preview = await client.meta.migrateStored(); // writes nothing + const result = await client.meta.migrateStored({ apply: true }); + const flows = await client.meta.migrateStored({ types: ["flow"] }); + ``` + + It returns the same `StoredMigrationReport` the CLI renders, and takes the same + posture: + + - **Preview by default.** `apply` must be literally `true`; an empty body, a + missing body, and `"apply": "yes"` all preview. Nothing is inferred. + - **Gated on `manage_metadata`.** Unlike the single-item `PUT /meta/:type/:name` + next door, this rewrites every eligible row in the deployment, so it demands + the ADR-0066 D1 authoring capability rather than just a session, and answers + `403` otherwise. The gate runs before the protocol is probed, so an + unauthorized caller cannot use `403`-vs-`501` to learn which kernels can be + migrated. `/meta`'s anonymous-deny umbrella still closes it to anonymous + callers first. + - **Attributed to the caller.** The `actor` recorded on the history and audit + rows names the user who fired it — that is the question those rows exist to + answer. + + **Flows need no extra setup on this path.** The CLI has to boot an inert + automation engine to hold the executor registry ADR-0078's conflict guard needs; + a server already has a live one, and the protocol resolves it from the services + registry itself (#4498), so this route covers flow rows by simply running in the + process that owns them. + + Registered on both the REST server and the runtime dispatcher's `/meta` domain, + ledgered in both route ledgers, and mounted before `/:type` so the + leading-underscore segment is never captured as a metadata type name. + +### Patch Changes + +- 84b4a3a: docs(client): drop the retired `validateOnly` batch option from the README (#4052) + + The Batch Options section still documented `validateOnly` as a working dry-run — + "validate records without persisting changes" — but the key was retired in #4052 + precisely because nothing ever read it. Every batch surface (`updateManyData` / + `deleteManyData` / `batchData`) persisted regardless, so a caller who sent it to + preview a mutation got that mutation **executed**. + + `BatchOptionsSchema` has carried a `retiredKey(...)` tombstone since #4052, so the + schema already refuses the key loudly. The README was the last place still + promising it — declared-but-not-enforced in prose rather than in code, aimed at + exactly the readers who cannot see the tombstone. + + Released as a patch rather than declared release-nothing because `README.md` is in + this package's `files`: the corrected text only reaches the people who hit the + problem — readers on npmjs.com — if the package ships. + + Replaced with a pointer to `docs/protocol-upgrade-guide.md` + (`batch-options-validate-only-retired`). No behaviour change; there is no batch + dry-run today. Write-path validate-only was evaluated in #4372 and closed as not + planned — no current consumer justifies the surface. + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Major Changes diff --git a/packages/client/package.json b/packages/client/package.json index 465be696fa..f5e3328756 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client", - "version": "17.0.0-rc.1", + "version": "17.0.0-rc.2", "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 90dbc3cf04..82eaf7325c 100644 --- a/packages/cloud-connection/CHANGELOG.md +++ b/packages/cloud-connection/CHANGELOG.md @@ -1,5 +1,75 @@ # @objectstack/cloud-connection +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [63b33e6] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [ff17642] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [8aacf94] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/runtime@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/cloud-connection/package.json b/packages/cloud-connection/package.json index 4ae2254304..afb17ebfd9 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.1", + "version": "17.0.0-rc.2", "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 dacd5f27d8..2e6608bd4a 100644 --- a/packages/connectors/connector-mcp/CHANGELOG.md +++ b/packages/connectors/connector-mcp/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/connector-mcp +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/connectors/connector-mcp/package.json b/packages/connectors/connector-mcp/package.json index d95e4dc6fb..5dba73ca25 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.1", + "version": "17.0.0-rc.2", "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 7327016b33..6ef159bbaf 100644 --- a/packages/connectors/connector-openapi/CHANGELOG.md +++ b/packages/connectors/connector-openapi/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/connector-openapi +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/connectors/connector-openapi/package.json b/packages/connectors/connector-openapi/package.json index 958e50a739..929c2d198f 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.1", + "version": "17.0.0-rc.2", "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 ae7d2cde81..c791392b06 100644 --- a/packages/connectors/connector-rest/CHANGELOG.md +++ b/packages/connectors/connector-rest/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/connector-rest +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/connectors/connector-rest/package.json b/packages/connectors/connector-rest/package.json index 09dac35a3b..ed66a53fbc 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.1", + "version": "17.0.0-rc.2", "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 684947906d..f1813122a0 100644 --- a/packages/connectors/connector-slack/CHANGELOG.md +++ b/packages/connectors/connector-slack/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/connector-slack +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/connectors/connector-slack/package.json b/packages/connectors/connector-slack/package.json index 32c404ad38..05e4e608eb 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.1", + "version": "17.0.0-rc.2", "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 a77614a656..295245cad6 100644 --- a/packages/console/CHANGELOG.md +++ b/packages/console/CHANGELOG.md @@ -1,5 +1,7 @@ # @objectstack/console +## 17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/console/package.json b/packages/console/package.json index 8c76e4c183..8b0d1d1058 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/console", - "version": "17.0.0-rc.1", + "version": "17.0.0-rc.2", "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 67fab921b1..2de41c0c69 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,69 @@ # @objectstack/core +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index caf4f8aa7f..84b3b6f9aa 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/core", - "version": "17.0.0-rc.1", + "version": "17.0.0-rc.2", "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 3dd6f471dd..c804348a35 100644 --- a/packages/create-objectstack/CHANGELOG.md +++ b/packages/create-objectstack/CHANGELOG.md @@ -1,5 +1,7 @@ # create-objectstack +## 17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/create-objectstack/package.json b/packages/create-objectstack/package.json index be7fb72ed3..2e74d413da 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.1", + "version": "17.0.0-rc.2", "description": "Create a new ObjectStack project — npx create-objectstack", "bin": { "create-objectstack": "./bin/create-objectstack.js" diff --git a/packages/formula/CHANGELOG.md b/packages/formula/CHANGELOG.md index 26f79db6a2..6b6678ab7b 100644 --- a/packages/formula/CHANGELOG.md +++ b/packages/formula/CHANGELOG.md @@ -1,5 +1,69 @@ # @objectstack/formula +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/formula/package.json b/packages/formula/package.json index e78f7d210b..3a220d4a75 100644 --- a/packages/formula/package.json +++ b/packages/formula/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/formula", - "version": "17.0.0-rc.1", + "version": "17.0.0-rc.2", "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 458cf33134..80469a30f2 100644 --- a/packages/lint/CHANGELOG.md +++ b/packages/lint/CHANGELOG.md @@ -1,5 +1,456 @@ # @objectstack/lint +## 17.0.0-rc.2 + +### Minor Changes + +- 0800433: Lint an action nobody placed (ADR-0078 Phase 3, Tier-A `action-locations`). + + New advisory rule `action-no-placement`: an action that declares no + `locations` and that no list view places by name renders on **no** surface — + it parses, publishes, and appears in Setup, while no user can ever click it. + ADR-0078 names this shape in its opening paragraph and Phase 3 asks for + exactly this rule; the shared completeness predicate it envisioned was never + built, so this lands standalone, one verified shape at a time. + + What made it verifiable now: objectui#3142 collapsed four disagreeing + renderers onto one placement predicate. Before that, `action:bar` and the + record header rendered an _undeclared_ action anyway, so the shape only looked + inert on paper. As of objectui 17.1 it is measurably inert. + + Two things are deliberately **not** flagged: + + - **`locations: []`** — the documented headless action (callable over REST / + MCP / AI, no UI surface). ADR-0110 D3 refuses an undeclared handler, so a + headless declaration is the only legal way to expose one. The rule therefore + distinguishes "nowhere, deliberately" (`[]`) from an unstated placement (key + absent) and only reports the latter. + - **Actions a view places by name** — `bulkActions`, `bulkActionDefs` + (including `execution: 'aggregate'` defs, whose whole point is an action with + no single-record home) and `rowActions`, across all three list-view tiers: + `views[i].list`, `views[i].listViews.` and the object-embedded + `objects[i].listViews.`. + + Advisory, never fatal — a view in another installed package may be the one + placing the action, the same reason `validateSemanticRoles` and + `lintLivenessProperties` warn rather than gate. + + Also: the action form schema in `@objectstack/metadata-protocol` no longer + declares `shortcut` / `bulkEnabled`. Both were retired as `retiredKey()` + tombstones in spec 17, and this schema is what the Studio designer renders its + fallback form from — so advertising them handed authors two inputs that could + only ever produce an unsaveable draft (objectui#3145 removed the matching + dedicated controls). And `content/docs/ui/actions.mdx` now says which surface + is the exception to location filtering, instead of a blanket claim its own + showcase contradicted. + +- 85a966f: Nav targets that are not object names (`page` / `report` / `dashboard`) are now checked at author time — closing a hole _inside_ an existing check. + + `defineStack`'s `validateCrossReferences` already validates these three. But each arm is gated on the collection being non-empty: + + ```ts + if (nav.type === 'page' && typeof nav.pageName === 'string' + && pageNames.size > 0 && !pageNames.has(nav.pageName)) { … } + ``` + + So a stack that declares **no `pages` at all** has its page-nav check silently switched off, and `{ type: 'page', pageName: 'anything' }` sails through. That is exactly the state a stack is in when the target was never written — the most likely way to reach this bug, not the least. + + Note the asymmetry the guard creates. The `object` arm of the same block has no size gate: it errors unless the item carries `requiresObject`, an **explicit** opt-in to "another package provides this". Objects have to say so out loud; pages, reports and dashboards got an implicit exemption that depends on an unrelated property of the stack. + + `validateNavTargetRefs` joins `REFERENCE_INTEGRITY_RULES` (16 → 17), so it runs on `validate`, `lint` and `compile` with no CLI rewiring. It reports **warning**, not error, and that ceiling is deliberate: `validate-object-references` can say ERROR for an unresolved _object_ because it resolves against the curated `PLATFORM_PROVIDED_OBJECT_NAMES` registry and knows which cross-package names are real. No such registry exists for pages, reports or dashboards, so "unresolved" cannot honestly be distinguished from "provided by a package we cannot see". Fixing the guard by tightening the parse-time throw was the other option and was rejected: a throw has no escape hatch for a legitimately cross-package page, and ADR-0072 D1's rule is that one dead finding costs more than a missed one. When `defineStack`'s check _is_ live it still hard-fails first; this rule is what speaks when that check has switched itself off, and it says so in the message. + + **Three nav types are deliberately NOT covered, each verified rather than assumed:** + + - **`action`** — already owned by `validate-action-name-refs`, which walks app navigation explicitly. Adding it here would double-report. + - **`component`** — a verified NON-rule. An unregistered `componentRef` does _not_ fail silently: `ComponentNavView` renders a named diagnostic ("Component not registered … Ensure the plugin that provides this surface is installed and has called `registerAppComponent()`"), and the registry exists precisely so plugin-provided surfaces may legitimately be absent. Flagging it would break valid plugin nav and prescribe a fix for something already reported better at runtime. + - **`url`** — external by definition. + + Both NON-rules are pinned by tests, so "completing" the module by adding them fails there first. + + **Scope honesty:** all 35 authored nav page/report/dashboard targets in this repo resolve, so this closes a latent hole rather than a shipped bug. The rule was proven to go red and then green through the real `validateReferenceIntegrity` entry point on a known-bad stack, not only in unit tests — a green check that has never been made to fail is the recurring defect this campaign keeps finding in its own instruments. + +- a7163ea: The ADR-0078 completeness gate ships: a Zod-valid metadata instance that silently does nothing now fails at author time, on every authoring surface. + + This closes the hole _between_ the platform's existing gates. An instance can be Zod-valid (gate 1 green), use only _live_ properties (gate 2 green), and a correctly-authored sibling can be proven to run (gate 3 green) — and still be dead, because it omits a config its consumer needs and the consumer silently no-ops. The founding case (cloud#687): an AI authored `{ type: 'summary' }` with no `summaryOperations`; the engine's index builder skips it, the field reads 0 forever, the dependent "occupancy rate" is stuck at 0 — and the agent reported the work done, because every gate it could see was green. + + **Why this is worse than the unknown-key hole #4001 just closed.** There, the author wrote a key we don't know, and the parse now rejects it with a prescription. Here every key is one we know, the schema is satisfied, nothing warns, and the author gets a success. It manufactures false completion without the author mistyping anything — and the review step that catches a human's bare summary (seeing the field render `0`) is exactly the step AI authoring removes. + + **One shared predicate, every surface — the ADR's core decision.** Instance-completeness checks previously existed _only_ in cloud's AI-build graph-lint, so a stack authored with `os` + a coding assistant, an MCP agent, `os validate` in CI, or by hand got none of them (`formula_without_expression` existed nowhere in the framework). The judgement now lives in `@objectstack/spec/kernel`'s `checkFieldCompleteness` / `checkViewCompleteness` — sibling of `isIncoherentAggregate`, the ADR-0019 pattern — consumed by the new `@objectstack/lint` `validate-functional-completeness` and registered as an author-time rule (28 → 29), so `os build` / `os validate` / `os lint` / MCP / hand authoring are all covered. Cloud graph-lint can re-home its duplicate rules onto the same predicate rather than drifting from it. + + **Every rule cites the runtime line that makes it true**, because the completeness audit's scariest candidate — a "sharing rule fails open and shares every record" — collapsed on a three-file read, and #4001's last two batches shipped four confidently wrong prescriptions before learning the same thing: + + | rule | the silent skip | severity | + | ------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -------- | + | `field/summary-without-operations` | `engine.ts` — `if (!d.summaryOperations) continue` | error | + | `field/formula-without-expression` | `engine.ts` builds the formula plan only from fields that HAVE one | error | + | `field/relationship-without-reference` | `$expand` — `if (!referenceObject) continue` | error | + | `field/choice-without-options` (`select`, `radio`) | `record-validator.ts` — an empty option list disables server-side value validation | error | + | `field/choice-without-options` (`checkboxes`) | same branch, but shared with free-form | warning | + | `view/layout-without-binding` (`kanban`, `calendar`, `gantt`) | renderer falls back to literal default field names | warning | + + **The deliberate NON-rules are pinned as hard as the rules.** `multiselect` without options is _not_ flagged: `record-validator.ts` says verbatim `// free-form (tags without options)`. The runtime blesses it as a mode, which makes it ADR-0078 case (3) "genuinely optional" — flagging it would be another false prescription, and the test is where that attempt fails first. `timeline` / `tree` views are likewise out of v1: they have config schemas, but their renderer behaviour has not had its verification pass. + + **It found a real one on its first run against a real app.** `showcase_field_zoo.f_summary` was a bare `Field.summary({ label: 'Roll-up Summary' })` — one line below an `f_formula` that _is_ complete, in the object whose entire job is to show what each field type looks like. So the canonical example of a roll-up in this repo computed nothing. It could not be fixed by adding `summaryOperations`: a roll-up aggregates a child into its parent, and the zoo is a leaf (`f_master_detail` makes it a child of `showcase_project`, and nothing is a child of the zoo). Removed, with the working examples named — `showcase_invoice.total` for the plain sum, `showcase_expense_report.total_amount` / `approved_amount` for the `summaryOperations.filter` variant. The rule it broke was the file's own: "relationship types point at the other showcase objects so they have REAL targets." + + Tracked in #4544. This is Phase 1; Phase 2 (the cloud authoring-path config-drop fix) is in the `cloud` repo, and Phase 3 lands the Tier-B shapes one verification pass at a time. + +- e6e9379: ADR-0078 Phase 3: a webhook with no `triggers` now fails at author time — and the Tier-B candidate list is corrected to what verification actually supports. + + **The rule.** `webhook/without-triggers`, error severity, in the shared `@objectstack/spec/kernel` predicate alongside the Phase 1 rules, walked by `@objectstack/lint`'s `validate-functional-completeness` over `stack.webhooks` in both collection spellings. A webhook that declares no trigger materializes into `sys_webhook`, renders in Setup looking armed, and delivers nothing. + + **Why it needed two sources, and why the first one argued against it.** The runtime skip site reads: + + ``` + if (triggers.size === 0) { + // No dispatchable triggers (or a manual-only webhook with none) — + // skip auto-enqueue. + return null; + ``` + + That parenthetical _blesses_ the empty case as a deliberate mode — structurally identical to the `multiselect`-without-options NON-rule, where `record-validator.ts`'s `// free-form (tags without options)` is exactly why we do not flag it. On that evidence alone this candidate stays unenforced. + + The mode it names does not exist. `webhook.zod.ts`'s #3196 note records that the `api` (manual/programmatic fire) trigger was _removed_ because "no manual fire path exists — the only webhook HTTP surface re-queues already-failed deliveries". There is no way to fire a webhook the auto-enqueuer dropped. Inert on every path, so: `error`. + + > **The generalization, now written into the module and pinned by a test:** a runtime comment records what its author believed, and beliefs go stale when a sibling feature is deleted. A blessing has to be corroborated by something showing the blessed mode is still _reachable_ — otherwise it is a comment about a mode that no longer exists. The test asserts the finding carries both citations, so nobody demotes this rule on the strength of the comment alone. + + `triggers: []` is flagged identically to an omitted `triggers`. Unlike an action's `locations: []` — the documented headless spelling — an empty array here carries no "I meant it" signal, because turning a webhook off has its own key (`isActive`). The repo's one real webhook (`showcase_task_changed`) confirms it: shipped inactive via `isActive: false`, with a full trigger list. + + **The corrected Tier-B disposition.** Phase 3 was scoped from the 2026-06 audit's Tier-A/B catalog. Verifying each candidate before writing it — the discipline that caught four false prescriptions in #4001 — found most of the list already closed or misfiled: + + | candidate | disposition | + | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | + | A2 action without `locations` | **already shipped** — `validate-action-locations.ts`, which already exempts the documented `locations: []` | + | B approval empty/unresolvable approvers | **already shipped** — `validate-approval-approvers.ts` | + | B select/multiselect without options | shipped in Phase 1 | + | B write-side referential integrity | **not an authoring-lint item** — a runtime gap; no metadata omission to detect | + | B `unique:true` no-op on memory driver | **not an authoring-lint item** — a driver gap | + | B composite/repeater sub-field constraints | **not an authoring-lint item** — a runtime gap | + | B nav targets of type page/report/url/component/action | **genuine gap, different module** — the key is present but dangling, which is reference resolvability (ADR-0072), not completeness (ADR-0078) | + | B dataset with zero measures | **unverified — not shipped.** No runtime consumer in this repo; the dataset compiler lives elsewhere | + | B webhook without triggers | ✅ **this change** | + | B schedule trigger with invalid cron | **unverified — not shipped.** `normalizeSchedule` accepts any non-empty string, but the scheduler's behaviour on an invalid one was not traced | + + Two candidates are deliberately left unshipped rather than written on the audit's stated confidence, and one is left for the module that actually owns it. The audit's own lesson stands: it produces _candidates_, not confirmed bugs — the scariest one collapsed on a three-file read. + + Tracked in #4544. + +- ebb209c: fix(spec,lint): withdraw the `record:*` blocks from the react tier — no renderer read the props it published (#4413) + + The react-tier contract published `objectName` / `recordId` on + ``, ``, `` and + ``, and no renderer read either prop. All ten `record:*` renderers + take their record from `useRecordContext()`, which only the record route + (`RecordDetailView`) and the metadata editor's preview (`PagePreview`) ever + mount; the `kind:'react'` page renderer wraps the page in a + `SchemaRendererProvider` alone. So the blocks rendered their "bind a record to + preview" placeholder — or, for `record:related_list` (the one that does read + `schema.objectName`), refused to fetch because the parent id never arrived. A + page authored exactly to contract came back EMPTY with nothing reported + anywhere, including by `os validate`, which resolved those props' field names + against the object they named: lint standing guard over a binding that never + ran. + + Withdrawn rather than implemented. The contract was not merely unimplemented, + it was the wrong SHAPE: per-block bindings describe four independent fetches of + one record, which is exactly the coupling the shared record context exists to + prevent (`record:details` drops the fields a mounted `record:highlights` + registered; one inline-edit save bar commits them all under a single + `ifMatch`). Honoring the props would have fossilized that (Prime Directive + #12). The naming of that primitive — a record SCOPE an author wraps around the + family, one fetch, shared context — is the open design question, filed as #4444. + + `@objectstack/spec` drops the four blocks from `REACT_BLOCKS` and gains the + ledger for why, plus the working replacement per type. The family is derived + from `ComponentPropsMap`, so a record component added later is gated the day it + lands — including the six that were never in the contract but are just as + reachable through the registry-built react scope. + + `@objectstack/lint` gains `react-block-needs-record-context` (error), which + rejects them on a react page by tag and through `` + alike, quoting the block that does work: `', '=', +parentId]}>` for a related list, `` for a + field panel. A locally-declared component of the same name shadows the injected + scope and is left alone. + +- 97faca3: feat(spec,lint)!: give `bulkActionDefs` a shape, and lint the aggregate name it references (#4457) + + A selection-bar bulk action was declared as + `z.array(z.record(z.string(), z.any()))` — **no shape at all**. The real + contract lived in objectui's `BulkActionDef` interface and in the executor that + reads it, so every authoring mistake landed as a silent runtime downgrade: + `opeartion` parsed and the executor hit `Unknown operation: undefined` per row; + `excution: 'aggregate'` parsed and the def stayed per-record, so the endpoint + written for ONE `_selectedIds` call got N calls instead — the exact defect + objectui#3139 was filed to make expressible. That is ADR-0018's "second + vocabulary" smell (an action surface sharing none of `ActionSchema`'s checks) + crossed with ADR-0078's silently-inert metadata. + + `ui/bulk-action.zod.ts` types it, with the same treatment `ActionParamSchema` + got in #3746/#4001: a **strict** def whose unknown-key error names the offending + key and the canonical spelling. Beyond spelling, it refuses the combinations the + executor never reads — `patch` outside an `update`, `execution` outside a + `custom`, `params` on a `delete`, `batchSize` on an aggregate — and refuses a + hand-written `actionDef`, which is attached by the renderer when it resolves the + def's `name` and which authored by hand would smuggle an action definition past + the action registry. + + **One shape that parsed before is now rejected**: `operation: 'custom'` without + `execution: 'aggregate'`. `resolveBulkActions` attaches a dispatcher for exactly + one authored shape (the aggregate one); every other custom def falls to + `Promise.resolve()` per row — a button that reports success for every selected + record and does nothing. The error names both legal forms: `bulkActions: +['']` for per-record (promoted with the action's own label, params and + `visible`), `execution: 'aggregate'` for one call over the whole selection. + + Two things are deliberately left open: + + - **`params[]` is `.passthrough()`.** objectui's `BulkActionParam` declares a + `[key: string]: unknown` catch-all — widget config (min/max/step/format) + forwarded to the field renderer as-is. Locking it down would reject valid + config, so declared keys are typed and the rest rides through, the same call + `dashboard.zod.ts` makes for a widget's `config`. + - **The bulk-param / action-param spelling divergence** (`help`/`helpText`, + `default`/`defaultValue`, `object`/`reference`, plus `labelField`, which + `ActionParamSchema` has no counterpart for). objectui already owns a converter + for the promoted direction; converging the authored direction is a cross-repo + change with its own migration. Typing them as they are is what makes the + divergence visible rather than undocumented — the prerequisite for closing it. + + `label` and the param/option labels are `z.string()`, not `I18nLabelSchema`: + an authored def reaches the grid verbatim (nothing resolves an `{ en, zh }` map + on this path) and the bar renders `def.label` as a React child, so blessing the + map form would trade a parse error for a blank screen. Localize by declaring a + real action and naming it in `bulkActions` — that path runs through the i18n + resolver. + + **Lint**: `validate-action-name-refs` now covers `bulkActionDefs`. Only an + `execution: 'aggregate'` entry is a name reference (it is what + `resolveBulkActions` looks up); an `update`/`delete` def's `name` is a button id + and resolving it would be nonsense. The walk also reaches an **object's own + `listViews`** for the first time — an object has no top-level `list`, so that + tier had simply never been visited while the view-level ones were covered. And + the hint no longer tells a bulk-surface author to add a `locations` entry: the + selection bar is the one surface that does not filter on it, so naming the + action there is the whole placement. + + Verified zero new findings against `app-showcase` / `app-crm` / `app-todo`. + +### Patch Changes + +- f3141d8: fix(spec): a node that publishes no descriptor configSchema can now own an expression-ledger entry (#4439) + + `FLOW_NODE_EXPRESSION_PATHS` is the #4027 ledger that tells `registerFlow` and + `objectstack validate` which config keys hold expressions, and in which dialect. + Its ratchet (`config-expression-ledger.test.ts`) derives what it expects from + descriptor `configSchema` `xExpression` markers, and fails in **both** + directions — an undeclared marker, or a ledger entry nothing declares. + + `decision` / `script` / `subflow` publish **no** descriptor `configSchema` on + purpose: a published partial schema would drop the editors their hand-written + Studio forms need (the #4210 incident), so their contract lives in + `schemaless-node-config.zod.ts`. Those two rules compose into a hole — an + expression slot on a schemaless node is structurally unreachable by the ratchet, + and because the reverse direction rejects unclaimed entries, it cannot be + entered by hand either. + + `decision.conditions[].expression` sat in that hole. Its own schema says + _"Bare CEL predicate deciding this branch"_ and its own comment names `{…}` as + the #1491 trap, and no validator walked it — so `{lead_record.status} == +'converted'` passed `tsc`, passed `objectstack validate`, passed registration. + #4414 made that fail loudly at run time; this makes it fail at build time, + which is the delay #4027 exists to remove. + + ## The fix + + The ratchet now reads **both** declaration channels: + + - **descriptor `configSchema`** — unchanged, enumerated from the live registry; + - **`schemaless-node-config.zod.ts`** — the marker rides + `.meta({ xExpression })` through `z.toJSONSchema`, the same channel + `loop.collection` has used since objectui#2670. + + Spec hands the second channel over as JSON Schema + (`getSchemalessNodeConfigJsonSchemas()`, memoized, `input` mode — the shape a + descriptor's `configSchema` already is), so the ratchet walks both with the + _same_ function. No second notion of "a declared expression property", which is + the duplication a ledger exists to remove, and no `zod` dependency added to + `service-automation`. Each channel is separately asserted non-empty, so a broken + derivation on one side cannot hide behind the other's results. + + `SCHEMALESS_NODE_CONFIG_SCHEMAS` is also exported for anything else that needs + to reason about all node config contracts. Additive — objectui's + `flow-node-config` reconciliation imports each schema by name and is unaffected. + + ## The sweep + + The other schemaless slots were checked and deliberately carry no marker: + `script.template` is a template **id**, not a body; `script.inputs` / + `script.variables` / `subflow.input` are values that interpolate `{token}` — + text-with-holes, the shape essentially every node config string has, already + covered generically by `validate-flow-template-paths` and the CLI flow linter. + A `flow-template` ledger entry means something narrower: a _reference that must + resolve to a value_, like `loop.collection`. So `decision.conditions[] +.expression` is the only genuinely declared expression slot on the class — now + recorded in the ledger's header so it is not re-derived. + + ## Docs corrected + + The flows guide taught the **wrong dialect** for decision predicates in three + places (`'{order_amount} > 10000'`), plus a "braces missing in a decision + expression" warning that inverted after #4414 — and `FlowNodeSchema`'s own + `@example` did the same. All corrected to bare CEL, with the history stated so + an author with a braced predicate knows what changed and why their build now + fails. The dialect table drops from three dialects to two: predicates never take + braces, values always do. + + Verified: 13 new/updated tests across the ratchet, the engine's registration + pass and `@objectstack/lint` (including the exact app-crm predicate rejected at + both `registerFlow` and `objectstack validate`); `pnpm build`, `pnpm typecheck` + (122 tasks), `pnpm lint` and `check:docs` clean. + +- fd3013a: feat(spec,automation)!: converge `script` to a function call — retire the `actionType` branches — and parse `script` / `subflow` config at execute time (#4343) + + A `script` node had four ways to name what it ran and only one of them ran anything. + Protocol 17 keeps that one and retires the rest. + + - **`config.actionType: 'email' | 'slack'`** were **logger-backed stubs**. They wrote a + line, reported success, and delivered nothing — under any configuration, installed + messaging service or not. Every bundled example used one; none of them ever sent + anything. + - **`config.template` / `.recipients` / `.variables`** fed those stubs, so they addressed + a message no channel sent. (The examples did not even reach them: they passed the + payload in `inputs`, which the built-in branch never read.) + - **inline `config.script`** was recognized and **never executed** — the built-in runtime + has no server-side JS sandbox, so the node warned and completed as a no-op. + - **any other `actionType`** was shorthand for a registered-function name — a second + spelling of `config.function` — and `'invoke_function'` was a marker that named nothing + on its own. + + What remains is what worked: `config.function` (now **required**) names a registered + function, `config.inputs` feeds it, `config.outputVariable` binds its return value. + + **The replacements are three different mechanisms, not one rename.** + + | Retired | Use instead | + | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | + | `actionType: 'email'` (+ `template` / `recipients` / `variables`) | a `notify` node — it delivers through the messaging service: the in-app inbox by default, real email once `@objectstack/plugin-email` is installed | + | `actionType: 'slack'` | a `connector_action` node with the Slack connector, or an `http` node posting to an incoming webhook — `notify` has no Slack channel | + | `actionType: 'my_fn'` (shorthand) | `function: 'my_fn'` — the conversion moves it for you | + | `script: '…'` (inline JS) | move the logic into a registered function and call it via `config.function` | + + **Execute-time parse.** `script` and `subflow` now run their config through the contract + before executing, the seam #4277 gave the flat builtins — a violation refuses the node as + a **guard** (wrong metadata; no `fault` edge may route it, #3863). `script` could not join + that seam while its legal key set depended on `actionType`: a flat parse would either + reject valid shapes or wave everything through. Converging the node is what made the + contract fit. `subflow`'s hand-written `flowName` check became the same parse, so its + message is now `subflow 'n1': config does not satisfy the subflow contract — +config.flowName: …`. `decision` deliberately stays export-only: its one key is optional, + so a parse would check nothing. + + **Migration.** `os migrate meta --from 16` rewrites stored sources; authoring one of these + keys in TypeScript is a compile error carrying the same prescription. A shorthand + `actionType` **converts into `function`** — that is what it named — unless `function` is + already set, in which case it was dead metadata the executor never reached. The other four + keys are dropped outright: nothing read them, so there is no value to preserve, and + rebuilding the intent is an authoring decision (the table above) rather than something a + mechanical rewrite can guess. + + The keys leave the **load path** (`retiredFromLoadPath`) with the rest of the keys retired + for _misdescribing themselves_ rather than for being renamed: absorbing + `actionType: 'email'` silently would let an author keep believing the flow sends mail. The + one seam that still replays it is `registerFlow`, which rehydrates data at rest (#3903) — + a row in `sys_metadata` has no author for a tombstone to teach. So a stored email-stub node + arrives stripped of the keys nothing read and then **refuses for naming no callable**, + where it used to log a line and report success. That flip is the behavior change to expect. + + **A build gap this surfaced, fixed here.** `FlowFunctionEntrySchema` now also accepts a + **lowered handler ref** (a non-empty string), the form `objectstack build` produces: the + CLI lowers every inline callable to a serialisable ref _before_ the stack is parsed (it + must — `z.function()` wraps callables and would break the ref mapping), so a built + manifest holds `{ myFn: 'myFn' }`, which neither previous member accepted. The result was + that `defineStack({ functions })` — a documented, first-class mechanism — could not + survive a build at all. Nothing had noticed because no bundled example used it; #4343 + turns that from latent into blocking, since `config.function` becomes the only thing a + `script` node can run. `Hook.handler` already declared exactly this pair (`z.union([ +z.string(), ])`, "string, post-build / inline function, pre-build"), so this + brings `functions` onto the platform's established shape rather than inventing one. A + string carries no callable and `normalizeFlowFunctionEntry` still drops it by design — the + real functions ride in the sibling ESM module the build emits, merged by name — so + hand-authoring one registers nothing and fails loudly at execute ("no function named '…' + is registered"), never silently. + + Also in this change: the retired constants `SCRIPT_BUILTIN_ACTION_TYPES`, + `SCRIPT_INVOKE_FUNCTION_ACTION_TYPE` and the `ScriptBuiltinActionType` type are removed + (they described the dispatch set that no longer exists); `os validate` names a retired key + and its replacement instead of reporting a generic missing callable; and the `#3796` + alias fixture, which carried `actionType: 'invoke_function'` through both sides, no longer + describes an end state protocol 17 can reach — the rename itself is untouched. No liveness + ledger row moves: the gate walks `FlowSchema`, whose `nodes[].config` is + `z.record(z.unknown())`, so these keys were never governed by one. + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/formula@17.0.0-rc.2 + - @objectstack/sdui-parser@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/lint/package.json b/packages/lint/package.json index f7c75a7e42..2a6d82a51d 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/lint", - "version": "17.0.0-rc.1", + "version": "17.0.0-rc.2", "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 005c556dff..df55f0bc6f 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,5 +1,72 @@ # @objectstack/plugin-mcp-server +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/formula@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/mcp/package.json b/packages/mcp/package.json index b605f19c3d..3104020d28 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/mcp", - "version": "17.0.0-rc.1", + "version": "17.0.0-rc.2", "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 48332af280..9c7ad708ff 100644 --- a/packages/metadata-core/CHANGELOG.md +++ b/packages/metadata-core/CHANGELOG.md @@ -1,5 +1,120 @@ # @objectstack/metadata-core +## 17.0.0-rc.2 + +### Major Changes + +- ce92674: feat(spec)!: retire the standalone `validation` metadata kind (#4509, ADR-0088) + + A validation rule authored as its own artifact bound to nothing and gated no + write. `ValidationRuleSchema` carries **no object-binding key** — no `object`, + no `objectName` — and all six variants are `strictObject`, so an author could + not supply one either. No merge step existed. The only code that expected such a + key was a reference-tracker row scanning a field the schema would have stripped. + Meanwhile the engine evaluates exactly one shape: the object's own + `validations[]` array, on insert and on every matched update row. + + So a rule created through the standalone door — a `*.validation.ts` file, or + Studio's Validations list — parsed, saved, reported success, and intercepted + nothing. Including a `state_machine` rule, which ADR-0020 routes through this + same vocabulary: an author could believe they had locked down record state + transitions and have changed nothing at all. + + Under ADR-0088 the kind fails the admission test on its first clause: a rule has + no independent lifecycle, because it only means something against an object. And + unlike the sibling disconnects closed in this batch, it could not be bridged into + one — the shape has nowhere to name its object. + + **The rule vocabulary is untouched.** `ValidationRuleSchema` and all six + variants are unchanged and fully live; the engine's evaluation path is not + modified by this change. It is the _kind_ that was inert, not the schema. The + liveness ledger keeps governing it through the gate's `SPEC_ONLY_SCHEMAS` + override (alongside `webhook` and `query`), because an ungoverned live schema is + exactly how the next drift would hide. + + **Migration.** Move the rule into the owning object's `validations:` array — the + rule body is identical, same schema, same six variants: + + ```ts + // before — a standalone *.validation.ts, which never ran + export default defineValidation({ name: 'amount_positive', type: 'script', … }) + + // after — on the object, where rules are evaluated + ObjectSchema.create({ + name: 'invoice', + validations: [{ name: 'amount_positive', type: 'script', … }], + }) + ``` + + Removed: the registry entry (and its `*.validation.ts` / `*.validation.yml` + patterns), the `MetadataTypeSchema` member, the metadata-core lockstep enum + member, the schema-map entry, the create seed, Studio's Validations nav item and + its hand-crafted form, and the dangling reference-tracker row. Standalone rows + already in `sys_metadata` are left alone — they were never evaluated, so nothing + changes behaviorally. + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/metadata-core/package.json b/packages/metadata-core/package.json index 7a7c3eba87..30d0ba9212 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.1", + "version": "17.0.0-rc.2", "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 9ba6f066f0..ed9b290afc 100644 --- a/packages/metadata-fs/CHANGELOG.md +++ b/packages/metadata-fs/CHANGELOG.md @@ -1,5 +1,12 @@ # @objectstack/metadata-fs +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [ce92674] + - @objectstack/metadata-core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/metadata-fs/package.json b/packages/metadata-fs/package.json index 033752ac37..5d2ebda72b 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.1", + "version": "17.0.0-rc.2", "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 630dae5b5f..ce2cd05725 100644 --- a/packages/metadata-protocol/CHANGELOG.md +++ b/packages/metadata-protocol/CHANGELOG.md @@ -1,5 +1,606 @@ # @objectstack/metadata-protocol +## 17.0.0-rc.2 + +### Minor Changes + +- 0800433: Lint an action nobody placed (ADR-0078 Phase 3, Tier-A `action-locations`). + + New advisory rule `action-no-placement`: an action that declares no + `locations` and that no list view places by name renders on **no** surface — + it parses, publishes, and appears in Setup, while no user can ever click it. + ADR-0078 names this shape in its opening paragraph and Phase 3 asks for + exactly this rule; the shared completeness predicate it envisioned was never + built, so this lands standalone, one verified shape at a time. + + What made it verifiable now: objectui#3142 collapsed four disagreeing + renderers onto one placement predicate. Before that, `action:bar` and the + record header rendered an _undeclared_ action anyway, so the shape only looked + inert on paper. As of objectui 17.1 it is measurably inert. + + Two things are deliberately **not** flagged: + + - **`locations: []`** — the documented headless action (callable over REST / + MCP / AI, no UI surface). ADR-0110 D3 refuses an undeclared handler, so a + headless declaration is the only legal way to expose one. The rule therefore + distinguishes "nowhere, deliberately" (`[]`) from an unstated placement (key + absent) and only reports the latter. + - **Actions a view places by name** — `bulkActions`, `bulkActionDefs` + (including `execution: 'aggregate'` defs, whose whole point is an action with + no single-record home) and `rowActions`, across all three list-view tiers: + `views[i].list`, `views[i].listViews.` and the object-embedded + `objects[i].listViews.`. + + Advisory, never fatal — a view in another installed package may be the one + placing the action, the same reason `validateSemanticRoles` and + `lintLivenessProperties` warn rather than gate. + + Also: the action form schema in `@objectstack/metadata-protocol` no longer + declares `shortcut` / `bulkEnabled`. Both were retired as `retiredKey()` + tombstones in spec 17, and this schema is what the Studio designer renders its + fallback form from — so advertising them handed authors two inputs that could + only ever produce an unsaveable draft (objectui#3145 removed the matching + dedicated controls). And `content/docs/ui/actions.mdx` now says which surface + is the exception to location filtering, instead of a blanket claim its own + showcase contradicted. + +- 83cf2d3: feat(migrate,metadata-protocol): `os migrate meta --stored` rewrites sys_metadata rows so the read-path chain has a finish line (#4327) + + #4317 closed the correctness gap from the read side: every stored-row + rehydration seam replays the full ADR-0087 conversion chain, retired entries + included, so a row written under any past protocol is _served_ canonical + forever. What it deliberately did not do is make the rows themselves canonical. + A pre-17 row keeps its legacy bytes, the chain re-lowers it on every load, and + each affected row logs one conversion notice per process — deduped, but back + every boot. Until now the only things that ever rewrote such a row were a Studio + re-save and `duplicatePackage`. + + **`os migrate meta --stored`** is the pass that ends it for a deployment that + runs it. It walks `sys_metadata` — `active` and `draft`, every organization — + replays the same `applyConversionsToStoredItem` chain, and re-saves each changed + body through the normal write path, so a rewritten row gets a + `sys_metadata_history` entry, a fresh checksum and the mutation projectors, + exactly like an author's save. The history row's `source` is `migrate-stored`, + so a later diff distinguishes an upgrade from somebody's edit. + + ```bash + os migrate meta --stored # preview: per-row report, writes nothing + os migrate meta --stored --apply # rewrite the rows (prompts) + os migrate meta --stored --apply --yes --json # CI / scripts + os migrate meta --stored --type view # restrict to a type (repeatable) + ``` + + **Preview is the default and `--apply` is the only writing mode** — the house + rule its siblings already keep (#3617's "a dry run changes nothing"), and it + applies with more force here because what moves is metadata: every affected + row's checksum and a history entry per row. An apply run also refuses to start + while another process holds the SQLite database, for the same reason + `os migrate files-to-references --apply` does. + + **Nothing gates on this having run.** #3855's conclusion stands — an + operator-run migration cannot be relied upon, so the read path remains the + guarantee for every deployment, and no `sys_migration` flag is recorded (a flag + would advertise enforcement that does not exist). What a run buys is hygiene — + rows stop carrying pre-protocol dialects, so diffs, exports and history are + clean going forward, and the recurring notices go quiet — plus one thing that + was previously unobtainable: **an operator can assert it.** A run with nothing + left to do exits `0`, a deployment with rows still on an old dialect exits `1`, + so "my metadata is on protocol N" becomes a CI check rather than a belief. + + Three things the pass declines, and reports rather than counting as done: + `flow` rows (their seam is `AutomationEngine.registerFlow`, which holds the + executor registry the node-type conflict guard needs), types with no repository + write path (`agent` — rewriting there would record no history and force a draft + live), and rows that still fail the current schema after conversion (a genuine + contract violation the write path is right to refuse; it keeps reading through + the chain and stays fixable in Studio). + + Also new, and usable without the CLI: `protocol.migrateStoredMetadata()` returns + the same structured report an admin route would render, and `saveMetaItem` + accepts an optional `source` for the history/audit rows. `source` is not + request-derived — the REST layer builds its save request field by field and + never forwards a client-supplied value, so provenance stays something the server + states rather than something a caller claims. + +- 304423e: feat(automation,migrate): `os migrate meta --stored` now covers flow rows too (#4454) + + #4327 gave the stored-metadata conversion chain a finish line for every + metadata type except `flow` — the one type where the most stored dialect + actually lives, since the graduated conversions `flow-node-crud-filter-alias`, + `flow-node-crud-object-alias`, `flow-node-notify-config-aliases` and + `flow-node-script-config-aliases` are all flow-node entries. Flow-node + conversions carry ADR-0078's open-namespace conflict guard, which has to consult + the _live_ executor registry to tell a rename from a clobber, and the metadata + layer has no way to obtain one. Flows were reported `skipped` with that reason. + They are now converted. + + **One canonicalization policy, two shapes.** + `AutomationEngine.canonicalizeStoredFlow` is the single implementation and + `registerFlow` calls it, so the load seam and the migration can never disagree + about what "canonical" means. It returns `parsed` (for execution — the + `FlowSchema.parse` + #4347 region output, schema defaults materialized) and + `storable` (for persistence). + + **`storable` excludes schema defaults, and that is the load-bearing decision.** + Measured rather than assumed: driving a pre-17 flow through all three steps + _removes_ nothing — `FlowSchema` is strict since #4001, so an unrecognized key + throws instead of being silently dropped, which means the + `graftNormalizedOperators` precedent (it exists because the _view_ parse strips + Studio-only auxiliary keys) does not transfer — and _adds_ only defaults: + `version`, `runAs`, per-edge `type` / `isDefault`. Persisting a default the + author never wrote would pin every migrated row to today's value while untouched + rows follow tomorrow's: two populations with different behaviour, which is + exactly the drift this pass exists to remove. So the write-back is the + conversion result plus the `{dialect, source}` envelopes the schema derives for + edge conditions, and nothing else. + + One subtlety worth knowing if you extend this: that envelope is a schema + transform, not a conversion, so it emits **no** notice while still changing the + body. Reading notices alone — correct for every other metadata type — would call + such a row canonical and leave it re-deriving on every boot. Both passes are + copy-on-write, so identity is the exact test for flows. + + **New: `AutomationServicePluginOptions.armRuntime`** (default `true`, so every + server, dev stack and test host is unaffected). Set `false` and the plugin + brings up the engine and the complete node registry — built-ins plus whatever + `automation:ready` contributes, because a _partial_ registry would make the + conflict guard read a live custom node type as unowned and rewrite over it — and + then stops before anything is armed: + + | Skipped when `armRuntime: false` | Why it must be | + | -------------------------------------------------------- | --------------------------------------------------------------------------------------------- | + | flow pull + `kernel:ready` / `metadata:reloaded` re-sync | `registerFlow` calls `activateFlowTrigger` — record triggers and scheduled jobs would go live | + | declarative connector materialization | opens real connections; an MCP provider spawns a child process | + | suspended-run wait-timer re-arm | would resume someone's paused approval mid-migration | + + `os migrate meta --stored` boots the plugin in that mode. A migration process + must not become a second server. + + A refused rename — the guard firing because the old node-type token is a live + name something else owns in this environment — fails that row loudly, naming the + token and its owner. Never a silent skip, never a clobber. A flow that cannot + canonicalize at all (a strict-schema violation, a malformed control-flow region) + is reported as failed with the parse message rather than persisted as a guess; + such a row cannot register today either, so the report is telling you about a + flow that is already broken at runtime. + +- ea90179: fix(data,runtime,drivers): four ADR-0112 envelope defects found in the v17 verification sweep (#4431, #4435, #4436, #4483) + + Four independent surfaces where the answer a caller received contradicted the + contract the surface declares. All four were found driving a real showcase boot + against `17.0.0-rc.1` and are catalogued in the #4482 rollup. + + - **#4431 — a sandbox capability denial answered 400.** A denial is the sandbox + refusing to run untrusted code that asked for a capability it does not hold, + which is the crash contract's case (#3951), not a deliberate rejection of a + malformed request. It now answers 500, and the `SandboxError:` debug prefix + no longer reaches the client. + + - **#4435 — PATCH/DELETE of a nonexistent record answered 200 success.** The + write path returned `record: null` / `success: true` for an id that resolves + to nothing, while GET on the same id correctly 404s; `deleteMany` reported + every typo'd id as deleted. Both now answer `RECORD_NOT_FOUND`, so a caller + can no longer read a successful envelope as proof the write landed. + + - **#4436 — the unsupported-filter-operator refusal shipped without + `error.code`.** A refusal with no code is unmatchable by a client, and the + message leaked the internal `[sql-driver]` prefix. It now speaks + `INVALID_FILTER` without the driver prefix. + + - **#4483 — the `$search` auto field set admitted its lead field + unconditionally.** `nameField`/`name`/`title` were prepended without passing + `SEARCH_AUTO_EXCLUDED_FIELDS`, so a search could be aimed at the primary key. + The lead field now only ORDERS the set it is already a member of; it can no + longer admit one. + + These change responses that were observably wrong, so callers coded against the + buggy shapes — a 200 on a missing record, a 400 on a capability denial — will + see different status codes. Graded `minor` on that basis rather than `patch`. + +- ce92674: feat(spec)!: retire the standalone `validation` metadata kind (#4509, ADR-0088) + + A validation rule authored as its own artifact bound to nothing and gated no + write. `ValidationRuleSchema` carries **no object-binding key** — no `object`, + no `objectName` — and all six variants are `strictObject`, so an author could + not supply one either. No merge step existed. The only code that expected such a + key was a reference-tracker row scanning a field the schema would have stripped. + Meanwhile the engine evaluates exactly one shape: the object's own + `validations[]` array, on insert and on every matched update row. + + So a rule created through the standalone door — a `*.validation.ts` file, or + Studio's Validations list — parsed, saved, reported success, and intercepted + nothing. Including a `state_machine` rule, which ADR-0020 routes through this + same vocabulary: an author could believe they had locked down record state + transitions and have changed nothing at all. + + Under ADR-0088 the kind fails the admission test on its first clause: a rule has + no independent lifecycle, because it only means something against an object. And + unlike the sibling disconnects closed in this batch, it could not be bridged into + one — the shape has nowhere to name its object. + + **The rule vocabulary is untouched.** `ValidationRuleSchema` and all six + variants are unchanged and fully live; the engine's evaluation path is not + modified by this change. It is the _kind_ that was inert, not the schema. The + liveness ledger keeps governing it through the gate's `SPEC_ONLY_SCHEMAS` + override (alongside `webhook` and `query`), because an ungoverned live schema is + exactly how the next drift would hide. + + **Migration.** Move the rule into the owning object's `validations:` array — the + rule body is identical, same schema, same six variants: + + ```ts + // before — a standalone *.validation.ts, which never ran + export default defineValidation({ name: 'amount_positive', type: 'script', … }) + + // after — on the object, where rules are evaluated + ObjectSchema.create({ + name: 'invoice', + validations: [{ name: 'amount_positive', type: 'script', … }], + }) + ``` + + Removed: the registry entry (and its `*.validation.ts` / `*.validation.yml` + patterns), the `MetadataTypeSchema` member, the metadata-core lockstep enum + member, the schema-map entry, the create seed, Studio's Validations nav item and + its hand-crafted form, and the dangling reference-tracker row. Standalone rows + already in `sys_metadata` are left alone — they were never evaluated, so nothing + changes behaviorally. + +- dadb43f: refactor(spec,client,metadata-protocol,runtime)!: retire the workflow service slot — declared end to end, implemented nowhere (#4451) + + The `workflow` slot was ADR-0078's silently-inert declaration at every layer at + once: a `CoreServiceName` nothing ever registered or resolved (ADR-0115 + Evidence 5 — "no code in this repository resolves either slot", verified across + both repositories), an `IWorkflowService` contract with zero implementations, a + `WorkflowProtocol` whose three methods no code ever provided, a discovery + `routes.workflow` field no builder could truthfully populate, and a + `/api/v1/workflow` advertisement for a path no host ever mounted (the + pre-#3586 `DEFAULT_DISPATCHER_ROUTES` already listed it among routes that + never existed). The capability it promised is live elsewhere and has been for + majors: record state machines are enforced by the `state_machine` validation + rule, approvals are first-class flow nodes on the approvals runtime + (ADR-0019), and record-triggered automation is lifecycle hooks + + `record_change` flows (`service-automation`). + + FROM → TO: + + - `CoreServiceName 'workflow'` / `ServiceRequirementDef.workflow` / + `CORE_SERVICE_PROVIDER['workflow']` → removed; there is no slot to fill. + - `IWorkflowService` (`@objectstack/spec/contracts`) → removed; no + implementation ever existed. Register nothing — use the mechanisms above. + - `WorkflowProtocol` + `GetWorkflowConfigRequest/Response`, + `WorkflowState`, `GetWorkflowStateRequest/Response`, + `WorkflowTransitionRequest/Response` (`@objectstack/spec/api`) → removed, + along with the seven published JSON schemas. Delete the import; nothing + ever answered these shapes. + - Discovery `routes.workflow` / `services.workflow` / `features.workflow` + (metadata-protocol + runtime builders) → absent. A reader keying on them + only ever saw `unavailable` / `false`; delete the read. + - `RouterConfig.mounts.workflow` → removed; there was never a surface to + mount at it. + - `RestApiRouteCategory 'workflow'` → removed; categorize automation-adjacent + routes as `'automation'`. + - `@objectstack/client` re-exports of the four workflow types → removed with + their source. (The `client.workflow.*` methods were already removed earlier + in the v17 cycle — this retires the types they returned.) + - Also removed: the stray `graphql` entry in `CORE_SERVICE_PROVIDER` and the + `graphql: { route: '/graphql' }` discovery entry — `graphql` was never a + `CoreServiceName`, and the dispatcher had already dropped `/graphql` as out + of the product plan (#2462 follow-on). + + The retirement kit: the `workflow-service-slot-retired` semantic migration + (major 17) carries this prescription into `spec-changes.json`, the generated + upgrade guide and the `spec_changes` MCP tool. These are TS/API surfaces and a + discovery response field — never stored in stack metadata — so there is no + load-path conversion and nothing for `os migrate meta` to rewrite; the + 21 `authorable-surface.json` baseline lines and 7 `json-schema.manifest.json` + entries for the deleted schemas are dropped deliberately in the same change + (the plugin-runtime precedent: a prescription nobody can receive is noise — + nothing parses these shapes any more). + +### Patch Changes + +- 5b843fb: fix(automation,spec): the cold-boot flow bind must survive the read path's own annotations (cloud#971) + + `getMetaItems({ type: 'flow' })` decorates every served item with + `_diagnostics` (and `_draft` on a preview read). The cold-boot bind fed that + served document straight into `engine.registerFlow` → `FlowSchema.parse`, and + since #4001 closed the metadata schemas an unrecognized key **throws** instead + of being dropped — so every flow failed to register on every boot with + `unrecognized_keys: ["_diagnostics"]`. Not fatal only by luck: the + record-change plugin binds record flows a second way, so automations kept + firing behind one WARN per flow. A flow whose only binding path is this one + would have gone silently dead. + + Fixed at the read seam (`readFlowDefsFromProtocol`), not by loosening + `FlowSchema`: the payload is malformed because we decorated it, so the + producer's annotation is the producer's to remove. + + `@objectstack/spec` gains `METADATA_READ_DECORATIONS` / `stripReadDecorations` + (`kernel/metadata-read-decorations`) — the list moves out of + `metadata-protocol`, where it was module-private, so the producer and its + cross-layer consumers share one definition. `metadata-protocol` re-exports + `stripReadDecorations` unchanged; no public surface is removed. + +- 20bc357: fix(spec,metadata-protocol,runtime): discovery stops advertising routes for the kernel-internal cache/queue/job slots (#4318) + + The metadata-protocol discovery builder declared `/api/v1/cache`, `/api/v1/queue` + and `/api/v1/jobs` — three paths that existed nowhere else in the repository: no + dispatcher domain, no adapter mount, no plugin registration, and the shipped + providers (`service-cache`/`-queue`/`-job`) are in-process contracts that will + never mount one. Every default boot therefore advertised a route inside the same + `ServiceInfo` whose `handlerReady: false` said the opposite — a single record + contradicting itself (ADR-0076 D12). + + These slots are route-less now, like `realtime` — but unlike `realtime` an + unmarked real implementation stays `available`: the slot's contract is + in-process, so "no HTTP surface" is not reduced capability for it. `handlerReady` + is reported `false` on both discovery builders — for a route-less slot it is not + a proxy for anything, it is the fact itself (the dispatcher used to claim + `handlerReady: true` here for an unmarked occupant, a handler that does not + exist). The explanatory message is written once, as + `inProcessServiceMessage(slot)` in `@objectstack/spec/system`, so the two + builders cannot drift apart. + +- 8aacf94: fix(metadata-protocol): `duplicatePackage` stops minting pre-protocol flow rows (#4498) + + `duplicatePackage` canonicalizes each source row before re-saving it, under a + stated guarantee: "duplication never mints new rows in a pre-protocol dialect." + It delivered that through `convertStoredItem`, which opens with + `if (singular === 'flow') return { item: data, notices: [] }` — so for flows the + guarantee was **not** delivered. + + It did not fail loudly either. `FlowNodeSchema.config` is an open `z.record`, so + a pre-17 body (a `delete_record` carrying `config.filters`) sails through + `saveMetaItem`'s schema gate and lands verbatim in a brand-new row. + + **Why this mattered more than an un-migrated row.** ADR-0087 justifies the whole + stored-metadata design on new writes always being canonical, _therefore_ the + stored pass being "a strictly shrinking concern". `duplicatePackage` was a live + producer contradicting that for flows: an operator could run + `os migrate meta --stored --apply`, get a clean report, duplicate a package, and + be back to having pre-protocol rows — with the report still saying protocol N + until the next run. + + **The capability was already reachable.** The reason for the flow skip is real — + flow-node conversions carry ADR-0078's open-namespace conflict guard, which needs + the automation engine's live executor registry to tell a rename from a clobber. + But the protocol is constructed with an accessor for the kernel's service table + (the same one `analytics` and `package` are read from), and the automation + service registers under `automation`. A new private `resolveFlowCanonicalizer` + reads `canonicalizeStoredFlow` (#4454) off it, so every caller running next to a + live engine gets flow coverage without threading anything. + + - **`duplicatePackage`** canonicalizes flow rows through it. A refused rename + fails that item into the existing `failed[]` naming the token — copying the + un-renamed body would mint exactly the row this fixes. A flow that cannot + canonicalize fails the same way. With no engine reachable (a control-plane or + metadata-only host) the source body is copied as-is: no worse than the source + row already is, and failing an unrelated duplication over it would be its own + regression. + - **`migrateStoredMetadata`'s `canonicalizeFlow` becomes an override.** It now + defaults to the resolver. The CLI stopped passing one — it boots its inert + engine into the same kernel, so both routes reached the same instance, and two + routes to one capability is how they drift. The parameter stays for callers + with no registry and for testing the flow branch without an engine. + - **Resolution is lazy, per call.** Plugin init order does not guarantee + `automation` is in the table when the protocol is assembled (the CLI adds it + after ObjectQL by design), so caching `undefined` from a too-early read would + disable flow canonicalization for the life of the process. + + Two smaller honesty fixes ride along: a source item that fails _conversion_ (a + tombstoned key throws) is now reported as such instead of as `unparseable +metadata`, and `migrateStoredMetadata`'s "no engine" skip reason says no + automation service is reachable rather than blaming the caller for not supplying + one. + + Reads are unchanged. `getMetaItems` / `getMetaItem` / `getMetaItemLayered` / + `loadMetaFromDb` still skip flows — they are reads, covered by `registerFlow` + canonicalizing at execution, and are not producing bad data. Duplication was the + one that writes. + +- 63b33e6: One canonical type key at the `/meta` read/write/delete boundary (#4432). + + #3985 made the per-type gates accept both spellings of the `/meta` type segment + (`/meta/actions` and `/meta/action`). It did not FOLD them, so the two spellings + addressed two different namespaces and the layers below disagreed about which + one an item lived in. `saveMetaItem`, `getMetaItem`, `getMetaItems`, + `getMetaItemLayered`, `getMetaItemCached` and `deleteMetaItem` now fold the type + to its canonical singular (Prime Directive #3) as their first act, so every layer + below them reads one key. + + The damaging consequence was not the duplicate row — it was the shadowing. + `getMetaItems` hydrated overlay rows back into the SchemaRegistry under the + CALLER's spelling, so one plural-spelled read minted a plural registry entry; + from the next read on, `listItems('actions')` was no longer empty, the singular + fallback that had been supplying every code-authored action stopped running, and + a single overlay row hid the entire code-authored listing — on a spelling no + DELETE could address, because the delete path resolved the singular. Listing and + dispatch then disagreed about an item that had been deleted. + + Reads of data AT REST still try the other spelling as a fallback: rows written + under a plural `type` before this fix are real, and nothing rewrites them on + upgrade. What changed is that nothing WRITES or REGISTERS a non-canonical key any + more. + +- 1ee48bc: fix(objectql,metadata-protocol): a tenant-authored overlay must not read back as a code artifact + + `saveMetaItem` refuses to write an artifact-backed item of a type that has not + opted into overlay writes (`not_overridable`), and it asks + `registry.getArtifactItem` who is artifact-backed. That answer was "anything + whose `_packageId` is not the literal string `sys_metadata`" — a sentinel that + only holds on the save path. The boot-time rehydration of `sys_metadata` + registers each row under its REAL package id (`app.`), which every + runtime-authored item has carried since packages became mandatory. + + So an app the user had just built through Studio (or the AI build agent) came + back from the next kernel rebuild looking code-shipped, and the following edit + was refused with a 403 — permanently. Live capture: two identical `modify_field` + calls on the same object seconds apart, the first published LIVE and the second + `not_overridable`, because the first one's auto-publish triggered the rebuild in + between (cloud#970). + + Provenance is the axis that actually separates the two (ADR-0010 `_provenance`: + `'package'` for loader-introduced items, `'org'` for tenant-authored), so ask it: + the `sys_metadata` hydration now stamps `_provenance: 'org'`, and + `getArtifactItem` no longer treats such an item as an artifact. An item with no + provenance under a real package id is unchanged, so nothing that was protected + becomes writable. + +- 705e5c8: fix(metadata-protocol): a flow save that skipped canonicalization says so (#4580) + + `saveMetaItem` canonicalizes flow bodies before the schema gate (#4542). When the + canonicalizer throws — it is stricter than the gate: strict parse, cycle + detection, control-flow region validation — the save falls back to the raw body + so a work-in-progress draft with a temporary cycle stays saveable. That fallback + is correct and unchanged. It was also completely silent. + + Of the four postures at this seam, three announce themselves: a clean + canonicalization heals the row, a refused rename fails with `409 +FLOW_CONVERSION_CONFLICT` naming the token, and a host with no automation service + is reported by `os migrate meta --stored`. The throw-fallback said nothing, so a + save that skipped canonicalization was indistinguishable from one that healed the + row — and a body that is _both_ a legacy dialect and unparseable by the strict + canonicalizer re-persisted verbatim. That is the exact #4542 symptom, arriving + silently, while the boot warning for legacy stored rows tells the author that + re-saving is the remedy. + + The fallback now emits a `console.warn` naming the flow and the canonicalizer's + own error, deduped once per flow per process (the `convertStoredItem` pattern — + Studio autosaves the same draft repeatedly, and a WIP cycle throws on every + write). This aligns the write seam with ADR-0087 D2's "loud" posture, where + conversions emit notices, reads warn once per row, and `migrateStoredMetadata` + reports `failed` with the message. + + No behavior change: the body still saves, the schema gate stays the arbiter, and + `registerFlow` still refuses to arm a malformed flow. Refusing the save in + publish mode was considered and rejected — publish is the default mode, so it + would silently tighten validation for every existing caller, and it could only be + enforced on hosts that have an automation service, making the same body saveable + on a control-plane host and a 422 on an automation host. + +- f61edce: fix(metadata-protocol): `saveMetaItem` canonicalizes flow bodies on write — a Studio edit now heals a legacy flow row like every other type's (#4542) + + The once-per-boot stored-conversion warning promises that re-saving a row + ("Studio edit → save") persists the canonical shape. That held for every type + except `flow`: the read path serves stored flows verbatim (the ADR-0078 + open-namespace conflict guard needs the engine's live executor registry, so + `convertStoredItem` skips them), and `FlowNodeSchema.config` is an open + `z.record`, so the legacy dialect an author was served (`config.filters`, pre-17 + node aliases) sailed back through `saveMetaItem`'s schema gate and re-persisted + verbatim. A flow row stayed `pending` in `os migrate meta --stored` no matter + how many times an author edited it — only the migration itself could retire it. + + `saveMetaItem` now runs the #4498 resolver (`resolveFlowCanonicalizer`) on flow + bodies **before** the schema gate and persists `storable` — conversions plus the + derived condition envelopes, deliberately not the schema's defaults (ADR-0087). + The pass is copy-on-write, so already-canonical bodies (including the ones + `migrateStoredMetadata` and `duplicatePackage` hand in) are untouched. + + Failure postures, same as the duplication seam: + + - **A refused node-type rename** (the old token is a live name owned by a custom + executor here) refuses the save with `409 FLOW_CONVERSION_CONFLICT`, naming + the token and path — never a silent legacy persist. 409 rather than 422 + because the body may be perfectly valid: the refusal comes from environment + state, so resubmitting the same body cannot help. + - **A body the canonicalizer cannot parse** falls back to the raw save and + today's schema gate — in draft AND publish mode. `canonicalizeStoredFlow` is + stricter than the gate (cycle detection, control-flow regions), and a + work-in-progress draft with a temporary cycle must not become unsaveable; + `registerFlow` still refuses to arm a malformed flow either way. + - **No automation service reachable** (a control-plane or metadata-only host): + the save behaves exactly as before — a host must not start refusing flow + writes it accepted yesterday. `os migrate meta --stored` reports what it + could not canonicalize. + + Reads are still unchanged — served bodies keep the stored dialect ("reads + diagnose, never drop"); the heal happens on the way back in. + +- ad5fe25: fix(spec,objectql,metadata-protocol): a `user` field carries its target in the TYPE — bare `{type:'user'}` is not targetless + + `field.zod` defines `user` as "a lookup specialized to the `sys_user` system + object … target fixed to the `sys_user` system object", and `Field.user()` — + unlike `Field.lookup(reference, …)` — takes no target argument and writes + `reference: 'sys_user'` itself. The target is a constant of the type. + + Two callers read `field.reference` raw and so disagreed: the protocol's expand + gate refused `?expand=` with `400 INVALID_FIELD … declares no +target object`, and objectql's expand loop skipped it. Metadata authored without + the redundant `reference` — hand-written JSON, an AI author, a Studio form — was + read as under-specified when it was complete. Live capture (cloud#983): an + AI-built app's very first screen rendered an error page over that 400. + + New: `referenceTargetOf` in `@objectstack/spec/data` — the single arbiter of + "what does this reference field point at", next to `REFERENCE_VALUE_TYPES` (the + set those same two callers already share for "is this a reference at all"). Both + halves of the expand path read it, so the gate can no longer refuse a field the + engine would have expanded, nor bless one it skips. + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/metadata-core@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/formula@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Major Changes diff --git a/packages/metadata-protocol/package.json b/packages/metadata-protocol/package.json index 94ed232497..19a3189704 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.1", + "version": "17.0.0-rc.2", "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 11e5cbb999..9ab432bdbe 100644 --- a/packages/metadata/CHANGELOG.md +++ b/packages/metadata/CHANGELOG.md @@ -1,5 +1,149 @@ # @objectstack/metadata +## 17.0.0-rc.2 + +### Patch Changes + +- 3c7bcc0: feat(spec)!: converge the 11 contracts-vs-domain dual-source type names (#4538) + + `packages/spec/src/contracts/` hand-wrote parameter/result interfaces whose + names collided with same-named zod-derived types in the domains — the #4411 + trap, tracked as 11 rows of `dual-source-exports.baseline.json`. Each name was + judged individually against a three-repo import-level scan (framework, cloud, + objectui): which declaration actually flows at runtime decides the direction. + All 11 rows are deleted from the baseline; no name below is exported twice + anymore. + + **Converged — `./contracts` now re-exports the domain zod type (same + declaration on both entries, imports keep compiling from either):** + + - `NotificationChannel` → `system/notification.zod`'s + `z.infer` (member sets were identical). + - `ValidationResult` → `kernel/plugin-validator.zod` (shapes were identical). + - `HealthStatus` → `kernel/startup-orchestrator.zod` (`details` narrows + `Record` → `Record`). + - `PluginStartupResult` → `kernel/startup-orchestrator.zod`. FROM `plugin: +Plugin` (live object) and `error?: Error` TO the serializable projection + (`plugin: { name, version? }`-passthrough, `error?: { name, message, +stack?, code? }`). Neither side had any consumer outside spec; the + zod-validatable shape wins. + - `StartupOptions` → `kernel/startup-orchestrator.zod` — the PARSED tier + (defaults applied). `IStartupOrchestrator.orchestrateStartup` now takes + `StartupOptionsInput` (the caller-authored all-optional tier, also + re-exported from `./contracts`). Fix for callers typed to the old + all-optional `StartupOptions`: rename to `StartupOptionsInput`. + - `JobExecution` → `system/job.zod`. The system schema's `duration` field is + RENAMED `durationMs` — that is what every job adapter produces and what the + `sys_job_run.duration_ms` column round-trips; the schema described records + nothing ever wrote. Fix: `duration` → `durationMs` when parsing + `JobExecutionSchema` payloads. + - `AnalyticsQuery` → `data/analytics.zod`. The domain schema aligned to the + contract's semantics first: `timezone` LOST its `.default('UTC')` — absence + is meaningful (the engine resolves org timezone, #1982/#2018; the + `/analytics` entry always refused to apply that default). The schema is now + transform-free, so `AnalyticsQuery` ≡ `AnalyticsQueryInput` (both kept + exported). Fix for code that relied on `.parse()` injecting `timezone: +'UTC'`: pass the timezone explicitly or resolve it via the engine chain + (`selection.timezone ?? context.timezone ?? 'UTC'`). + + **Renamed — two genuinely different concepts were sharing one name (both + flow at runtime):** + + - `./contracts` `DriverCapabilities` → **`AnalyticsDriverCapabilities`** + (`{ nativeSql, objectqlAggregate, inMemory }`, the analytics strategy-chain + execution-path probe). The `DriverCapabilities` name now belongs solely to + the data domain's driver feature-flag record (`DriverCapabilitiesSchema`, + what `IDataDriver.supports` declares). Fix: importers of the trio from + `@objectstack/spec/contracts` (or `@objectstack/service-analytics`, whose + re-export is renamed in lockstep) rename the import; importers who meant + the driver flags import `DriverCapabilities` from `@objectstack/spec/data`. + + **Removed — the domain-side declaration was dead (zero import-level consumers + in framework/cloud/objectui; the #4411 family's last survivors):** + + - `system` `MetadataExportOptionsSchema` / `MetadataExportOptions` and + `MetadataImportOptionsSchema` / `MetadataImportOptions` (the + `output`/`source`-directory bags). The names now have ONE declaration each: + the `IMetadataService.exportMetadata` / `importMetadata` parameter + interfaces on `./contracts` (`types`/`namespaces`/`format` and + `conflictResolution`/`validate`/`dryRun`), which `MetadataManager` + implements. No tombstone/D2 conversion, deliberately — these are runtime + option-bag types, not authorable metadata (same reasoning as #4458). + `@objectstack/metadata` re-exports the two names from `./contracts` now + (it previously re-exported the dead system-side shapes its own manager + did not accept). + - `system` `JobSchedule` (the `= Schedule` back-compat alias). The name's one + declaration is the `IJobService.schedule` boundary shape on `./contracts` + (plain-string cron `expression`); the authored metadata type keeps its real + name `Schedule`. Fix: `import type { JobSchedule } from +'@objectstack/spec/system'` → `Schedule` (authoring tier) or the + `./contracts` `JobSchedule` (service boundary), whichever you meant. + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + - @objectstack/metadata-core@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + - @objectstack/metadata-fs@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Major Changes diff --git a/packages/metadata/package.json b/packages/metadata/package.json index addfe851c8..e594abdc99 100644 --- a/packages/metadata/package.json +++ b/packages/metadata/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata", - "version": "17.0.0-rc.1", + "version": "17.0.0-rc.2", "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 303582e03b..f0d9e6474b 100644 --- a/packages/objectql/CHANGELOG.md +++ b/packages/objectql/CHANGELOG.md @@ -1,5 +1,499 @@ # @objectstack/objectql +## 17.0.0-rc.2 + +### Major Changes + +- ec975f1: fix(objectql,driver-mongodb)!: `findOne` must say which record it wants, and executes every option it declares (#4419) + + `findOne` reads a single row, which makes its predicate the only thing between + the caller and _an arbitrary record_. When the predicate is missing the result is + not `null` — it is the object's **first row**: a real, plausible-looking record + with nothing to do with the request, which the `if (!row)` check every call site + already has cannot catch, and which then propagates into whatever is computed + next. Reported downstream: line items defaulting their price from the first + product in the catalog rather than the selected one, and "is this deal already + closed?" answered against an unrelated record while the write that followed + correctly targeted the intended id. A throw would have been caught in + development; a `null` would have been caught by the null-check. A valid-looking + wrong record defeats both. + + **Breaking — `findOne` now refuses a query that selects nothing in particular.** + + FROM → TO: + + | Was | Now write | Meaning | + | ----------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------ | + | `findOne(o)`, `findOne(o, {})`, `findOne(o, { where: {} })` | `findOne(o, { where: … })` | the record matching this predicate | + | | `findOne(o, { search: 'Acme' })` | the record this search finds | + | | `findOne(o, { orderBy: [{ field: 'created_at', order: 'desc' }] })` | the FIRST record in this order — the newest | + | | `find(o, { limit: 1 })` | any row will genuinely do, said at the call site | + + One-line fix: add the `where` you meant, or `orderBy` if you meant "the newest + one", or switch to `find(o, { limit: 1 })` if any row will do. The error names + all four. `find` and `count` are unchanged — returning or counting every row is + an honest answer; only `findOne`'s implicit "just one of them" turns a missing + predicate into a confidently wrong record. The guard reads the CALLER's + predicate, before RLS/sharing middleware injects its own: a tenant filter + narrows which rows are visible, it does not make "whichever comes first" + something the caller asked for. + + **Two silent drops that produced the same wrong record are fixed with it.** + + - **`findOne({ search })` applies the search.** The ADR-0061 `search` → + cross-field `$contains` expansion lived inline in `find` and nowhere else, + while `find` and `findOne` are checked against the SAME legal-key set — so + `search` passed the gate, rode onto the AST, and reached a driver. No driver + reads `ast.search`. The read therefore ran with no predicate at all and + `limit: 1` did the rest. The expansion is now one method both call. + - **`MongoDBDriver.findOne` applies `orderBy`, `fields` and `offset`.** It + translated `query.where` and dropped the rest, so `findOne({ orderBy })` did + not return the newest record — it returned whichever document the scan reached + first. `find` and `_findStream` in the same driver had always handled all + three. This one matters beyond Mongo: the guard above tells an unpredicated + caller to reach for `orderBy`, and an escape hatch one backend ignores is not + an escape hatch. No ordering is IMPOSED when the caller supplies none — both + drivers keep that carve-out (#4363), and `SqlDriver`'s comment about Mongo + "never sorting" is corrected, since it cited the dropped parameter as + agreement. + + **And a gate so the class does not come back.** A drift pin walks + `ENGINE_OPTION_KEY_SETS.findOne` and requires each declared key to have an + observable effect — on the AST the driver receives, on the driver options, or in + an explicit "not executed, and here is why" entry (only `limit`, which the + contract's `limit: 1` overrides). `search` sat declared-but-unexecuted through + two rounds of hardening because nothing asked that question. + + Together with #4346 (`filter` → `where` folds on every entry point) and #4400 + (unknown option keys throw), a read parameter the engine does not execute now + fails at the call site instead of quietly changing the answer. + +### Minor Changes + +- 257d97a: ADR-0078 Phase 4, decided rather than deferred: the silent skips stop being silent at runtime. The registry — the one choke point every metadata door goes through — now emits a functional-completeness diagnostic at registration, and the webhook enqueuer's zero-trigger skip warns instead of returning `null` wordlessly. + + **The Phase 4 ruling.** The phase had two halves, and they got opposite verdicts: + + - **Generative rule sweep: rejected — not deferred.** A generator can enumerate candidates ("which optional keys might be load-bearing?") but cannot verify runtime skip sites, and a rule without its skip-site citation is a false prescription — this campaign shipped four of those and every one was caught by the verification pass a generator would skip. The route is structurally wrong; no amount of waiting produces the evidence that would fix it. + - **Registration-time diagnostics: built now.** The evidence was already in hand, not pending: #3896 (Setup authoring inserted `sys_sharing_rule` rows directly, bypassing the schema that "required" `criteria`) and cloud's `rowColor.mapping` (an `as never` cast bypassed tsc) prove that doors which skip Zod and lint are real. The author-time gate only protects metadata that passes through `os build` / `validate` / `lint`; `SchemaRegistry.registerObject` is where _every_ door converges — declared stacks, plugin objects, `extend` contributions, `saveMetaItem`, raw `registerObject` calls. + + **Same predicate, same rule ids, different posture.** The registry calls the same `checkFieldCompleteness` that `validate-functional-completeness` uses, so the boot log carries the _same rule ids_ the lint reports (`field/summary-without-operations`, …) — an operator or an AI reading the log greps the id straight into the same docs and suppression story. But the registry **warns and never throws**: ADR-0078 §1's error severity means _the instance is dead_, not _the system is dead_ — an inert field must not kill a boot that thousands of healthy objects share. Errors block at author time; the registry's job is to make sure the silence never survives to runtime unobserved. + + One line per object with every finding aggregated (not per request — the hot path stays free; not per finding — a three-dead-field object is one greppable line). Follows `warnStrippedLegacyApiMethods` (#3543) exactly: module-level once-per-object dedup, injectable `warn`, pure observation that never mutates the schema. + + **The webhook skip now names itself.** `auto-enqueuer.ts`'s `if (triggers.size === 0) return null` sat under a comment blessing the empty case as "a manual-only webhook" — a mode #3196 removed (no manual fire path exists). The skip now warns with the author-time rule id (`webhook/without-triggers`), and the comment tells the truth. Only _active_ rows reach the parse (`where: { active: true }` — verified, not assumed), so a deliberately disabled webhook stays warning-free. + + **Scope honesty:** field rules and the webhook rule get the runtime twin. `view/layout-without-binding` stays author-time-only — views don't register through this choke point and the renderer half of the evidence lives in objectui. + + Tracked in #4544. This closes the ADR-0078 loop end to end: author-time error, runtime warning, one shared predicate deciding both. + +- 20b1a9e: fix(data): the audit anchor is engine-owned, and a lookup must resolve (#4447, #4441) + + Two write-path contract holes from the v17 verification sweep. + + **#4447 — `created_at` was client-writable on an ordinary PATCH.** Its two + siblings only looked protected: the audit hook force-advances `updated_at` / + `updated_by` on every update, so a forged value is overwritten. `created_at` is + insert-only, so nothing overwrote it. The root cause is a _declared_ audit + field shadowing the platform's: `applySystemFields` skips its injection when + the object already carries the name, and the merge lets the declared one win — + correct for an authored business field, wrong for the audit family. A built app + artifact ships a materialized `created_at` carrying only FieldSchema defaults + (`readonly: false`), which shadowed the engine-owned definition, so the + readonly strip had nothing to key off. The audit family's **governance** + (`readonly` / `system` / `type` / `reference`) is now forced by the platform + while presentation (label, description, hidden, group …) stays the author's. + Back-dating is unaffected: `preserveAudit` (#3479/#3493) and `isSystem` writes + still reinstate the original timeline. The strip now also reports through + `droppedFields`, giving the #3794 contract its first live producer on this axis. + + **#4441 — a `lookup` accepted an id that exists in no row of its target.** + Including `sys_position_permission_set.permission_set_id`, where a dangling row + is a security-surface record that resolves to nothing and the audience-anchor + gate has to resolve that very set to evaluate the grant. Writes are now refused + with `400 VALIDATION_FAILED` and a `fields[]` entry + (`code: 'reference_not_found'`, naming the field, the target and the + unresolvable id) — the catalogued `FieldErrorCode` that had no emitter until + now, with its message in the four platform locales. + + Scope for #4441 is deliberately narrow: caller-supplied keys only (so server + stamps are never reported as the caller's bad reference), non-system writes only + (seed replay and package install keep their ordering freedom), empty means "no + link", and it fails OPEN when the target cannot be checked. The existence probe + is unscoped, because existence is a fact about the database — whether the caller + may create the binding stays the RBAC/RLS layer's decision. + +- 63b33e6: A `datasourceMapping` rule is routing, not a hint — an object mapped to an + unreachable datasource no longer silently reads and writes the DEFAULT store + (#4462). + + **Observable behavior change; read this before upgrading.** Measured on `main` + during the v17 verification: map an object to a Postgres datasource with a bad + URL and the boot succeeds, `/ready` answers `200`, the datasource name appears in + **zero** log lines, `POST /api/v1/data/` returns `201` — and the + row is physically in the default store. The operator finds out by opening the + database they declared and finding it empty. ADR-0062 D2's phase-1 note called a + mapping-only datasource "decorative" to keep an example byte-for-byte unchanged; + what that bought was a silent data-placement bug. + + The fix is a pair, and each half is what makes the other correct: + + 1. **Routing stops falling through** (`@objectstack/objectql`). `getDriver` step + 2: a mapping rule that MATCHES and names a datasource with no live driver now + throws — `DatasourceUnavailableError` when the connect layer recorded a + verdict, otherwise an error naming the object, the datasource and the two + remedies. `default` still resolves onward: the default driver keeps its + natural name (#3826), so step 5 is how routing to it works. + 2. **ADR-0062 D2 grows gate (d)** (`@objectstack/service-datasource`, + `@objectstack/runtime`). A datasource a mapping rule routes at least one + object to is auto-connected at boot, and a boot-time connect failure is + **fatal** with an operator-readable reason — the same call gate (b) already + makes for an explicit `object.datasource` binding, now correct for (d) + because half 1 removed the fallback. `OS_ALLOW_DRIVER_CONNECT_FAILURE` still + degrades the boot instead, as for every other fatal connect. + + The mapped-object list is resolved by the boot path from the engine's own + matcher (`ObjectQLEngine.resolveMappedDatasource`, newly public) and passed to + `connectDeclared({ mappedObjects })`; the connection service never re-derives + rule matching. Two matchers drifting by one clause would connect a datasource + routing never uses, or route to one nothing connects — the defect again. + + **What to do if this breaks your boot.** It means a `datasourceMapping` rule in + your stack points at a datasource that cannot be connected. Either fix the + datasource configuration, or delete the rule — the second is what + `examples/app-crm` did in this change, and it is what keeps that example's + runtime behavior identical: its rules routed everything to an unconnected + `:memory:` datasource, i.e. to the default store by fall-through. + +- ce92674: feat(email): declared email templates reach the mail service (#4509) + + Authoring an `email_template` was a silent no-op. `EmailService.sendTemplate` + resolves `(name, locale)` against **`sys_email_template` rows**, and the only + writers of those rows were the built-in auth templates plus a code-constructed + `EmailServicePluginOptions.templates` that no bootstrapper ever passed. Every + door an author can actually use — a stack's `emailTemplates:`, an + `*.email-template.ts` file, Studio's metadata-admin list, `PUT /meta` — parked + items in a metadata store nothing read back. So an admin could "fix" the + password-reset email in Studio, get a success toast, and watch users keep + receiving the built-in copy: ADR-0078 false compliance on **authentication + mail**. This is the shape #3461 had for webhooks, closed the same way (ADR-0049 + enforce-or-remove, route: enforce). + + **`bootstrapDeclaredEmailTemplates`** now materializes declared templates into + `sys_email_template` at boot. Each item is validated through + `EmailTemplateDefinitionSchema.parse()` — the spec schema finally has a real + consumer, defaults and all — and projected with `mapTemplateToRow`, which is the + **same** mapping the built-in seeder uses, extracted and shared so the two doors + cannot drift apart. A malformed template warns and is skipped rather than + crashing boot. + + **Runtime writes take effect immediately.** Unlike `webhook`, `email_template` + is `allowRuntimeCreate: true`, so a boot-only bridge would have left a Studio + save inert until the next restart — the same bug, half-fixed. The plugin also + subscribes to `email_template` metadata changes and re-materializes the single + changed item; withdrawing a template deactivates its rows (across locales) + rather than deleting them. + + **Three breaks sat on this path, not one**, and closing any two of them would + still have shipped a template that never sent: + + - `@objectstack/objectql` never registered a manifest's `emailTemplates:` into + the metadata registry at all — the key was simply missing from the generic + ingestion list, so the bridge's own source was empty. + - The built-in seeder left `managed_by` at the column's `'admin'` default, which + made platform templates masquerade as admin-authored. Since the bridge refuses + to overwrite admin rows, a built-in would have permanently outranked the + template an app declared. Built-ins now stamp `managed_by: 'platform'`. + - Nothing materialized declared metadata into rows. + + **Seed-not-clobber** mirrors `sys_webhook` (#3489) and `sys_sharing_rule` + (#2909): `sys_email_template` gains `managed_by` / `customized`. Declared + templates re-seed every boot as `managed_by: 'package'`; a row an admin created + (`admin`) or edited (`customized`, stamped by a `beforeUpdate` hook) is never + overwritten, so reworded transactional mail survives redeploys. This is a + separate axis from `is_system`, which keeps its existing meaning for built-ins. + + The `email_template` liveness ledger flips from 13 dead properties to fully + live, with an ADR-0054 runtime proof bound on `subject` + (`email-template-materialization`): it boots a real stack, authors a template + that overrides a built-in auth template, and asserts the **authored** wording is + what reaches the transport. + +- eb4204b: feat(automation): a `script` node's purity contract is declared, and a function that writes can say so (#4396) + + The `script` executor's contract — _the named function returns a value; data I/O + stays on the flow graph_ — existed only as a comment inside the executor, while + #4354's run summary depended on it. That summary reports no record metrics for a + `script` step precisely because a pure function's writes are downstream + `create_record` / `update_record` nodes counting themselves. A function that + wrote anyway made its run report `selected: 30, acted: 0` — indistinguishable + from the broken sweep the counters exist to detect, recorded permanently on + `sys_automation_run`. + + **The rule is now visible.** `ActionDescriptor` carries + `handlerContract: 'none' | 'pure'`, and the `script` descriptor publishes + `'pure'`, so the action catalog, the designer palette and the reference docs + state the rule an author has to follow instead of an executor holding it + privately. + + **And a legitimate writer can opt out honestly.** A `defineStack({ functions })` + entry may declare what it does, in either shape: + + ```ts + defineStack({ + functions: { + scoreLead: (ctx) => ({ score: 42 }), // pure — the default + syncBilling: { handler: syncBilling, effect: "writes" }, // declared writer + }, + }); + ``` + + A step calling a declared writer reports `unmeasuredEffect`, so the run's + `unmeasured` tally keeps the broken-sweep query + (`selected > 0 AND acted = 0 AND unmeasured = 0`) off that flow — and only that + flow. Marking _every_ `script` step unmeasured was rejected: it would blind the + detector on every flow that calls any function in order to cover the few that + break the rule. + + Nothing here is retired or renamed: a bare `functions: { fn }` entry is + unchanged and means `effect: 'pure'`. The declaration is carried end to end — + `ObjectQL.registerFunction` accepts `{ packageId, effect }` alongside the + existing `packageId` string and exposes `resolveFunctionEntry(name)`, + `objectstack build` lowers a declared entry without dropping it, and the + artifact loader re-attaches the module's callable to the declaration the JSON + carried. + + **Also fixed:** `bindHooksToEngine` returned before registering a bundle's + functions when the stack declared no hooks, so a flow-only app's + `defineStack({ functions })` reached the engine as nothing and every `script` + node calling one failed with "no function named 'x' is registered". + +- d6bd5a1: Report stored `lookup` references that resolve to nothing (#4551) + + #4441 made the write path refuse an unresolvable reference id, but deliberately + exempted `isSystem` writes so seed replay, package install and boot-time + provisioning keep their ordering freedom. That exemption is unchanged — and it + left a residual: the platform itself could still write a reference into the void + with nothing saying so. + + New: `ObjectQL.inspectDanglingReferences()` — a **read-only** audit that walks + stored rows and reports every non-`readonly` `lookup` / `master_detail` / + `user` / `tree` value that names no row of its declared target. It runs as a leg + of the existing `LifecycleService` sweep, so the finding surfaces without an + operator knowing to go looking for it. + + - **It never rewrites.** The rows were genuinely written; auto-nulling a + dangling id would make the stored data disagree with what happened, and the + remedy (re-seed the target vs. clear the link) is an operator's call. + - **Unknown is not absent.** A probe that cannot run (target unregistered, no + driver, probe throws) counts as `undetermined`; an object whose rows cannot be + listed lands in `unreadableObjects`; a run that hits its row budget names the + object in `truncatedObjects`. So `dangling: []` can never be misread as + "everything is fine". + - **RBAC link tables are scanned first** (`sys_position_permission_set` and the + rest of `plugin-security`'s tables, derived from `PLATFORM_OBJECTS_BY_PACKAGE`): + a dangling row there is a security-surface record resolving to nothing, and + the audience-anchor gate must resolve exactly that permission set to evaluate + the grant. + + The existence oracle is the engine's own — the same predicate #4441's write-path + guard uses — so the report can never be stricter or looser than the rule it + reports on. + + Tuning: `ObjectQLPlugin`'s `lifecycle.referenceAudit` (`enabled`, `rowsPerObject`, + `maxRows`, `objects`). Nothing is authorable in metadata; no spec key was added. + +### Patch Changes + +- a52e2ef: fix(driver-sql,spec,objectql): a `defaultValue` runtime token never becomes a column DEFAULT (#4560) + + `Field.user({ defaultValue: 'current_user' })` is resolved by the **engine**, at + insert time, from the request's `ExecutionContext` — and with no authenticated + user (system / anonymous writes: seed replay, package install, boot + provisioning) `applyFieldDefaults` deliberately leaves the field **unset** + rather than stamp a bogus owner. + + The SQL DDL had never heard of the token. `createColumn` passed any non-object + `defaultValue` straight through to `col.defaultTo(dv)`, so the column was + created as `DEFAULT 'current_user'` and the **database** overrode the engine's + decision: every insert that omitted the field stored the literal string + `current_user` in a `lookup('sys_user')` column — a value that is not any user's + id. `?expand` resolves it to nothing, and on an owner / approver field it is a + silent mis-attribution. Found by #4551's dangling-reference audit on its first + run against a real boot; #4441's referential check could never have caught it, + because it inspects the values a **caller** supplied and here nobody supplied + one. + + **The token vocabulary is now declared once, in `@objectstack/spec/data`** + (`DEFAULT_VALUE_TOKENS`, `isRuntimeDefaultToken`, `isNowDefaultToken`, + `isCurrentUserDefaultToken`, `isAppResolvedDefaultToken`). The engine's + insert-time resolution and the driver's DDL read the same set, which is the + actual defect: `'NOW()'` was special-cased in the branch immediately above for + precisely this reason, and `current_user` — the same convention family — simply + had no entry anywhere the DDL could see. A token added to the set tomorrow is + excluded from literal column DEFAULTs automatically, rather than leaking its own + spelling into the database the way this one did. + + **DDL, in one place** (`applyDeclaredColumnDefault`, shared by column creation + and the SQLite table rebuild): + + - `'NOW()'` → the driver-native canonical default, exactly as before; + - any other runtime token → **no column default at all** (the engine owns it); + - Expression envelopes (`{ dialect, source }`) → unchanged, no default; + - a real literal → emitted verbatim, unchanged. + + **Existing databases carry the wrong DEFAULT**, so it is corrected through the + managed schema-drift path (#2186) rather than a bespoke migration: a new + `default_mismatch` finding with a `drop_column_default` op, categorised `safe` + (the statement cannot fail and touches no rows). Dev boots with + `autoMigrate: 'safe'` reconcile it automatically; everywhere else it is reported + with an actionable hint and applied by `os migrate apply`. Postgres/MySQL use + `ALTER COLUMN … DROP DEFAULT`; SQLite, which cannot alter a default in place, + goes through the existing table rebuild — which now re-materialises every + column's default from **metadata**, so a sibling `defaultValue: 'NOW()'` column + keeps the default it always had instead of losing it to the rebuild. + + **Rows already holding the bogus value are NOT rewritten.** That is #4551's + standing rule — report, never rewrite — so they stay visible to the + dangling-reference audit for operators to resolve deliberately. + +- 1ee48bc: fix(objectql,metadata-protocol): a tenant-authored overlay must not read back as a code artifact + + `saveMetaItem` refuses to write an artifact-backed item of a type that has not + opted into overlay writes (`not_overridable`), and it asks + `registry.getArtifactItem` who is artifact-backed. That answer was "anything + whose `_packageId` is not the literal string `sys_metadata`" — a sentinel that + only holds on the save path. The boot-time rehydration of `sys_metadata` + registers each row under its REAL package id (`app.`), which every + runtime-authored item has carried since packages became mandatory. + + So an app the user had just built through Studio (or the AI build agent) came + back from the next kernel rebuild looking code-shipped, and the following edit + was refused with a 403 — permanently. Live capture: two identical `modify_field` + calls on the same object seconds apart, the first published LIVE and the second + `not_overridable`, because the first one's auto-publish triggered the rebuild in + between (cloud#970). + + Provenance is the axis that actually separates the two (ADR-0010 `_provenance`: + `'package'` for loader-introduced items, `'org'` for tenant-authored), so ask it: + the `sys_metadata` hydration now stamps `_provenance: 'org'`, and + `getArtifactItem` no longer treats such an item as an artifact. An item with no + provenance under a real package id is unchanged, so nothing that was protected + becomes writable. + +- 26bb053: fix(objectql): a roll-up registered at RUNTIME must not need a restart to compute + + The engine's roll-up summary index had exactly one invalidation site — + `engine.registerApp` — and the runtime publish path does not go through it: it + registers straight into the registry (`protocol.saveMetaItem` → + `registry.registerObject`). So a kernel that had already performed a single + write — publishing itself writes `sys_metadata` rows — held a summary index + built before the new object existed. Every child write of a freshly published + roll-up then found no descriptor and silently skipped the recompute, leaving the + parent field null until the process restarted. + + That is how an AI-built app's "已完成任务数" shipped permanently empty over + completely correct metadata: the roll-up was configured, the child rows were + seeded with resolved foreign keys, and nothing recomputed (cloud#970). + + `SchemaRegistry` now carries a monotonic `objectRevision`, bumped on every + change to the registered object set, and the engine rebuilds its index whenever + that number has moved — so a registry-derived cache can no longer go stale + through a path that forgot to call the invalidator. + +- ad5fe25: fix(spec,objectql,metadata-protocol): a `user` field carries its target in the TYPE — bare `{type:'user'}` is not targetless + + `field.zod` defines `user` as "a lookup specialized to the `sys_user` system + object … target fixed to the `sys_user` system object", and `Field.user()` — + unlike `Field.lookup(reference, …)` — takes no target argument and writes + `reference: 'sys_user'` itself. The target is a constant of the type. + + Two callers read `field.reference` raw and so disagreed: the protocol's expand + gate refused `?expand=` with `400 INVALID_FIELD … declares no +target object`, and objectql's expand loop skipped it. Metadata authored without + the redundant `reference` — hand-written JSON, an AI author, a Studio form — was + read as under-specified when it was complete. Live capture (cloud#983): an + AI-built app's very first screen rendered an error page over that 400. + + New: `referenceTargetOf` in `@objectstack/spec/data` — the single arbiter of + "what does this reference field point at", next to `REFERENCE_VALUE_TYPES` (the + set those same two callers already share for "is this a reference at all"). Both + halves of the expand path read it, so the gate can no longer refuse a field the + engine would have expanded, nor bless one it skips. + +- Updated dependencies [0800433] +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [8aacf94] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [63b33e6] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [83cf2d3] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [1ee48bc] +- Updated dependencies [705e5c8] +- Updated dependencies [f61edce] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [304423e] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/metadata-protocol@17.0.0-rc.2 + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/metadata-core@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/formula@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Major Changes @@ -292,8 +786,8 @@ init(ctx) { … } })`; the engine is `ctx.getService('objectql')`, drivers being a hand-listed union and a bare `string` respectively and reference the catalog, so the three cannot drift apart. - Lowercase is deliberate, not an oversight against ADR-0112's SCREAMING_SNAKE: a - top-level code names the condition the _request_ hit, while a field-level code + Lowercase is deliberate, not an oversight against ADR-0112's SCREAMING*SNAKE: a + top-level code names the condition the \_request* hit, while a field-level code names the _constraint_ the value violated — and constraints are declared in the metadata's own snake_case, so `max_length` the code and `max_length: 50` the property are the same word on purpose. diff --git a/packages/objectql/package.json b/packages/objectql/package.json index 66bcf3c4cd..ad9b915f8b 100644 --- a/packages/objectql/package.json +++ b/packages/objectql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/objectql", - "version": "17.0.0-rc.1", + "version": "17.0.0-rc.2", "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 7eb024bd44..f354e2662b 100644 --- a/packages/observability/CHANGELOG.md +++ b/packages/observability/CHANGELOG.md @@ -1,5 +1,103 @@ # @objectstack/observability +## 17.0.0-rc.2 + +### Patch Changes + +- ff17642: fix(runtime): declarative `defineJob` cron jobs are actually scheduled (#4567) + + Every background job authored as `defineJob({ schedule: { type: 'cron', … } })` + was **silently never scheduled**. `JobSchema.parse` rewrites the cron + `expression` into the canonical expression envelope + (`{ dialect: 'cron', source: '0 1 * * *' }` — the authoring/persistence tier), + but `AppPlugin` handed `job.schedule` verbatim to `IJobService.schedule`, whose + boundary contract documents `expression` as a **bare cron string** because + `CronJobAdapter` passes it straight to croner. croner rejected the object + (`CronPattern: Pattern has to be of type string.`), the throw was swallowed by a + per-job `try/catch` that only `warn`ed, and the author saw a green build and a + green boot with the job never running. `interval` / `once` schedules and + flow `schedule` triggers were unaffected. + + **Fix (contract-first).** The authoring→boundary downgrade now happens at the one + place the two tiers meet — `AppPlugin`'s declarative-job registration, alongside + the existing `retryPolicy` / `timeout` threading — via + `toBoundaryJobSchedule()`. The adapters stay strict: no `typeof === 'object'` + tolerance was added downstream, so the boundary keeps exactly one shape. + A schedule that cannot be reduced to it (unknown type, AST-only or non-`cron` + expression envelope, missing `intervalMs` / `at`) is rejected by name. + + **The failure path is no longer silent.** A job that cannot be scheduled now logs + at **error** level with its own message (`Background job FAILED TO SCHEDULE — it +will never run`), plus a boot summary line when any job failed, and increments + the new `job_schedule_failures_total` counter + (`SEMCONV.jobScheduleFailuresTotal`, labels `app` / `job`) on the observability + metrics registry. "Failed to schedule" no longer shares the quiet `warn` used by + "handler not found in bundle.functions" — the first is an outage of declared + work, the second is a job that was never going to run. + + No authoring change is required: existing `defineJob` cron declarations start + working on upgrade. + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/observability/package.json b/packages/observability/package.json index e6a8ddfddc..39efd9ed25 100644 --- a/packages/observability/package.json +++ b/packages/observability/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/observability", - "version": "17.0.0-rc.1", + "version": "17.0.0-rc.2", "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 0dc9731c9b..a86ddfcb33 100644 --- a/packages/platform-objects/CHANGELOG.md +++ b/packages/platform-objects/CHANGELOG.md @@ -1,5 +1,175 @@ # @objectstack/platform-objects +## 17.0.0-rc.2 + +### Minor Changes + +- ce92674: feat(email): declared email templates reach the mail service (#4509) + + Authoring an `email_template` was a silent no-op. `EmailService.sendTemplate` + resolves `(name, locale)` against **`sys_email_template` rows**, and the only + writers of those rows were the built-in auth templates plus a code-constructed + `EmailServicePluginOptions.templates` that no bootstrapper ever passed. Every + door an author can actually use — a stack's `emailTemplates:`, an + `*.email-template.ts` file, Studio's metadata-admin list, `PUT /meta` — parked + items in a metadata store nothing read back. So an admin could "fix" the + password-reset email in Studio, get a success toast, and watch users keep + receiving the built-in copy: ADR-0078 false compliance on **authentication + mail**. This is the shape #3461 had for webhooks, closed the same way (ADR-0049 + enforce-or-remove, route: enforce). + + **`bootstrapDeclaredEmailTemplates`** now materializes declared templates into + `sys_email_template` at boot. Each item is validated through + `EmailTemplateDefinitionSchema.parse()` — the spec schema finally has a real + consumer, defaults and all — and projected with `mapTemplateToRow`, which is the + **same** mapping the built-in seeder uses, extracted and shared so the two doors + cannot drift apart. A malformed template warns and is skipped rather than + crashing boot. + + **Runtime writes take effect immediately.** Unlike `webhook`, `email_template` + is `allowRuntimeCreate: true`, so a boot-only bridge would have left a Studio + save inert until the next restart — the same bug, half-fixed. The plugin also + subscribes to `email_template` metadata changes and re-materializes the single + changed item; withdrawing a template deactivates its rows (across locales) + rather than deleting them. + + **Three breaks sat on this path, not one**, and closing any two of them would + still have shipped a template that never sent: + + - `@objectstack/objectql` never registered a manifest's `emailTemplates:` into + the metadata registry at all — the key was simply missing from the generic + ingestion list, so the bridge's own source was empty. + - The built-in seeder left `managed_by` at the column's `'admin'` default, which + made platform templates masquerade as admin-authored. Since the bridge refuses + to overwrite admin rows, a built-in would have permanently outranked the + template an app declared. Built-ins now stamp `managed_by: 'platform'`. + - Nothing materialized declared metadata into rows. + + **Seed-not-clobber** mirrors `sys_webhook` (#3489) and `sys_sharing_rule` + (#2909): `sys_email_template` gains `managed_by` / `customized`. Declared + templates re-seed every boot as `managed_by: 'package'`; a row an admin created + (`admin`) or edited (`customized`, stamped by a `beforeUpdate` hook) is never + overwritten, so reworded transactional mail survives redeploys. This is a + separate axis from `is_system`, which keeps its existing meaning for built-ins. + + The `email_template` liveness ledger flips from 13 dead properties to fully + live, with an ADR-0054 runtime proof bound on `subject` + (`email-template-materialization`): it boots a real stack, authors a template + that overrides a built-in auth template, and asserts the **authored** wording is + what reaches the transport. + +- ce92674: feat(spec)!: retire the standalone `validation` metadata kind (#4509, ADR-0088) + + A validation rule authored as its own artifact bound to nothing and gated no + write. `ValidationRuleSchema` carries **no object-binding key** — no `object`, + no `objectName` — and all six variants are `strictObject`, so an author could + not supply one either. No merge step existed. The only code that expected such a + key was a reference-tracker row scanning a field the schema would have stripped. + Meanwhile the engine evaluates exactly one shape: the object's own + `validations[]` array, on insert and on every matched update row. + + So a rule created through the standalone door — a `*.validation.ts` file, or + Studio's Validations list — parsed, saved, reported success, and intercepted + nothing. Including a `state_machine` rule, which ADR-0020 routes through this + same vocabulary: an author could believe they had locked down record state + transitions and have changed nothing at all. + + Under ADR-0088 the kind fails the admission test on its first clause: a rule has + no independent lifecycle, because it only means something against an object. And + unlike the sibling disconnects closed in this batch, it could not be bridged into + one — the shape has nowhere to name its object. + + **The rule vocabulary is untouched.** `ValidationRuleSchema` and all six + variants are unchanged and fully live; the engine's evaluation path is not + modified by this change. It is the _kind_ that was inert, not the schema. The + liveness ledger keeps governing it through the gate's `SPEC_ONLY_SCHEMAS` + override (alongside `webhook` and `query`), because an ungoverned live schema is + exactly how the next drift would hide. + + **Migration.** Move the rule into the owning object's `validations:` array — the + rule body is identical, same schema, same six variants: + + ```ts + // before — a standalone *.validation.ts, which never ran + export default defineValidation({ name: 'amount_positive', type: 'script', … }) + + // after — on the object, where rules are evaluated + ObjectSchema.create({ + name: 'invoice', + validations: [{ name: 'amount_positive', type: 'script', … }], + }) + ``` + + Removed: the registry entry (and its `*.validation.ts` / `*.validation.yml` + patterns), the `MetadataTypeSchema` member, the metadata-core lockstep enum + member, the schema-map entry, the create seed, Studio's Validations nav item and + its hand-crafted form, and the dangling reference-tracker row. Standalone rows + already in `sys_metadata` are left alone — they were never evaluated, so nothing + changes behaviorally. + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/metadata-core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/platform-objects/package.json b/packages/platform-objects/package.json index 79d8e44d3f..8f4a21ca6a 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.1", + "version": "17.0.0-rc.2", "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/driver-memory/CHANGELOG.md b/packages/plugins/driver-memory/CHANGELOG.md index 739c90d19f..bf8ccaef3e 100644 --- a/packages/plugins/driver-memory/CHANGELOG.md +++ b/packages/plugins/driver-memory/CHANGELOG.md @@ -1,5 +1,105 @@ # @objectstack/driver-memory +## 17.0.0-rc.2 + +### Minor Changes + +- ea90179: fix(data,runtime,drivers): four ADR-0112 envelope defects found in the v17 verification sweep (#4431, #4435, #4436, #4483) + + Four independent surfaces where the answer a caller received contradicted the + contract the surface declares. All four were found driving a real showcase boot + against `17.0.0-rc.1` and are catalogued in the #4482 rollup. + + - **#4431 — a sandbox capability denial answered 400.** A denial is the sandbox + refusing to run untrusted code that asked for a capability it does not hold, + which is the crash contract's case (#3951), not a deliberate rejection of a + malformed request. It now answers 500, and the `SandboxError:` debug prefix + no longer reaches the client. + + - **#4435 — PATCH/DELETE of a nonexistent record answered 200 success.** The + write path returned `record: null` / `success: true` for an id that resolves + to nothing, while GET on the same id correctly 404s; `deleteMany` reported + every typo'd id as deleted. Both now answer `RECORD_NOT_FOUND`, so a caller + can no longer read a successful envelope as proof the write landed. + + - **#4436 — the unsupported-filter-operator refusal shipped without + `error.code`.** A refusal with no code is unmatchable by a client, and the + message leaked the internal `[sql-driver]` prefix. It now speaks + `INVALID_FILTER` without the driver prefix. + + - **#4483 — the `$search` auto field set admitted its lead field + unconditionally.** `nameField`/`name`/`title` were prepended without passing + `SEARCH_AUTO_EXCLUDED_FIELDS`, so a search could be aimed at the primary key. + The lead field now only ORDERS the set it is already a member of; it can no + longer admit one. + + These change responses that were observably wrong, so callers coded against the + buggy shapes — a 200 on a missing record, a 400 on a capability denial — will + see different status codes. Graded `minor` on that basis rather than `patch`. + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Major Changes diff --git a/packages/plugins/driver-memory/package.json b/packages/plugins/driver-memory/package.json index c8b144e987..93a2a527ec 100644 --- a/packages/plugins/driver-memory/package.json +++ b/packages/plugins/driver-memory/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-memory", - "version": "17.0.0-rc.1", + "version": "17.0.0-rc.2", "license": "Apache-2.0", "description": "In-Memory Driver for ObjectStack (Reference Implementation)", "main": "dist/index.js", diff --git a/packages/plugins/driver-mongodb/CHANGELOG.md b/packages/plugins/driver-mongodb/CHANGELOG.md index 029099850e..6771e4c41d 100644 --- a/packages/plugins/driver-mongodb/CHANGELOG.md +++ b/packages/plugins/driver-mongodb/CHANGELOG.md @@ -1,5 +1,184 @@ # @objectstack/driver-mongodb +## 17.0.0-rc.2 + +### Patch Changes + +- 9b43ee2: test(drivers): the filter-logic standard now covers the backend it was counted without (#4405) + + `FILTER_LOGIC_CASES` (#3774) opens by calling itself the standard "the four + independent FilterCondition backends are each checked against". Five backends + exist. `driver-mongodb`'s `translateFilter` was missed, not excluded — an + independent implementation whose `$and`/`$or`/`$not` translation shares no line + of code with the SQL compiler or the in-memory matcher, and the only one whose + target language cannot spell the standard directly: MongoDB has no + document-level `$not` at all (the server answers `unknown top level operator: +$not`), so a negation has to leave as `$nor`, and a branch's own keys have to + stay in one document while `$and`/`$or` clauses are lifted beside them. That + route was never checked against the shared cases. Both DEBT rows the #4363 gate + recorded are now cleared, and `scripts/check-driver-conformance.mjs` reports + `ok` for every cell of the matrix. + + **`driver-mongodb` runs the table twice, and the split is deliberate.** + `mongodb-filter-logic-translation.test.ts` drives every shared case through + `translateFilter` and evaluates the emitted MongoDB _document_ over the shared + fixture — a pure function, no server, so it always runs. That matters here more + than anywhere: `mongodb-memory-server` downloads a ~123 MB binary from + fastdl.mongodb.org, and a defect only a downloadable binary can catch is a + defect nobody catches on a restricted network. Its in-process reader is strict + by construction — every shape it does not model throws instead of evaluating to + true, a document-level `$not` included — and its own discrimination is pinned by + cases that require a widened document to FAIL the case it widens, so "all green" + cannot mean "the reader says yes to everything". + `mongodb-filter-logic-conformance.test.ts` runs the same table against a real + mongod and answers the one question the first half cannot — does MongoDB agree? + — skipping cleanly (never silently) when the binary is unreachable. + + **`driver-sqlite-wasm` runs the table through its own engine.** It inherits + `SqlDriver`'s filter compiler, so nothing is re-implemented; what the suite pins + is that a nested `(… AND …) OR (… AND …)` survives the custom sql.js dialect + that compiles, binds and marshals it — the same seam its temporal and pagination + suites cover for their clauses. Tracked as DEBT rather than EXEMPT because + "inherits, therefore fine" is the assumption those suites exist to disprove; the + suite is what disproves it. + + **No divergence was found.** `translateFilter` answers all seventeen shared + cases correctly today, `$not`-inside-a-branch and nested `$and`-inside-`$or` + included, so no translation change ships here — what changes is that the next + edit to it cannot quietly widen a filter. Both suites were verified to be + discriminating rather than decorative by reintroducing the #3774 miscompile + (propagating `or` into a branch's own contents): 15 of the mongodb translation + suite's 26 tests fail, and 13 of the wasm suite's 18. + + `packages/spec`'s `filter-logic-conformance.ts` header now says five and names + the fifth — a code comment; no schema, export or generated artifact moved. + +- ec975f1: fix(objectql,driver-mongodb)!: `findOne` must say which record it wants, and executes every option it declares (#4419) + + `findOne` reads a single row, which makes its predicate the only thing between + the caller and _an arbitrary record_. When the predicate is missing the result is + not `null` — it is the object's **first row**: a real, plausible-looking record + with nothing to do with the request, which the `if (!row)` check every call site + already has cannot catch, and which then propagates into whatever is computed + next. Reported downstream: line items defaulting their price from the first + product in the catalog rather than the selected one, and "is this deal already + closed?" answered against an unrelated record while the write that followed + correctly targeted the intended id. A throw would have been caught in + development; a `null` would have been caught by the null-check. A valid-looking + wrong record defeats both. + + **Breaking — `findOne` now refuses a query that selects nothing in particular.** + + FROM → TO: + + | Was | Now write | Meaning | + | ----------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------ | + | `findOne(o)`, `findOne(o, {})`, `findOne(o, { where: {} })` | `findOne(o, { where: … })` | the record matching this predicate | + | | `findOne(o, { search: 'Acme' })` | the record this search finds | + | | `findOne(o, { orderBy: [{ field: 'created_at', order: 'desc' }] })` | the FIRST record in this order — the newest | + | | `find(o, { limit: 1 })` | any row will genuinely do, said at the call site | + + One-line fix: add the `where` you meant, or `orderBy` if you meant "the newest + one", or switch to `find(o, { limit: 1 })` if any row will do. The error names + all four. `find` and `count` are unchanged — returning or counting every row is + an honest answer; only `findOne`'s implicit "just one of them" turns a missing + predicate into a confidently wrong record. The guard reads the CALLER's + predicate, before RLS/sharing middleware injects its own: a tenant filter + narrows which rows are visible, it does not make "whichever comes first" + something the caller asked for. + + **Two silent drops that produced the same wrong record are fixed with it.** + + - **`findOne({ search })` applies the search.** The ADR-0061 `search` → + cross-field `$contains` expansion lived inline in `find` and nowhere else, + while `find` and `findOne` are checked against the SAME legal-key set — so + `search` passed the gate, rode onto the AST, and reached a driver. No driver + reads `ast.search`. The read therefore ran with no predicate at all and + `limit: 1` did the rest. The expansion is now one method both call. + - **`MongoDBDriver.findOne` applies `orderBy`, `fields` and `offset`.** It + translated `query.where` and dropped the rest, so `findOne({ orderBy })` did + not return the newest record — it returned whichever document the scan reached + first. `find` and `_findStream` in the same driver had always handled all + three. This one matters beyond Mongo: the guard above tells an unpredicated + caller to reach for `orderBy`, and an escape hatch one backend ignores is not + an escape hatch. No ordering is IMPOSED when the caller supplies none — both + drivers keep that carve-out (#4363), and `SqlDriver`'s comment about Mongo + "never sorting" is corrected, since it cited the dropped parameter as + agreement. + + **And a gate so the class does not come back.** A drift pin walks + `ENGINE_OPTION_KEY_SETS.findOne` and requires each declared key to have an + observable effect — on the AST the driver receives, on the driver options, or in + an explicit "not executed, and here is why" entry (only `limit`, which the + contract's `limit: 1` overrides). `search` sat declared-but-unexecuted through + two rounds of hardening because nothing asked that question. + + Together with #4346 (`filter` → `where` folds on every entry point) and #4400 + (unknown option keys throw), a read parameter the engine does not execute now + fails at the call site instead of quietly changing the answer. + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/plugins/driver-mongodb/package.json b/packages/plugins/driver-mongodb/package.json index eb0c26d5bc..70ed155370 100644 --- a/packages/plugins/driver-mongodb/package.json +++ b/packages/plugins/driver-mongodb/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-mongodb", - "version": "17.0.0-rc.1", + "version": "17.0.0-rc.2", "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/plugins/driver-sql/CHANGELOG.md b/packages/plugins/driver-sql/CHANGELOG.md index 5d3c509de4..07ad20894d 100644 --- a/packages/plugins/driver-sql/CHANGELOG.md +++ b/packages/plugins/driver-sql/CHANGELOG.md @@ -1,5 +1,224 @@ # @objectstack/driver-sql +## 17.0.0-rc.2 + +### Minor Changes + +- ea90179: fix(data,runtime,drivers): four ADR-0112 envelope defects found in the v17 verification sweep (#4431, #4435, #4436, #4483) + + Four independent surfaces where the answer a caller received contradicted the + contract the surface declares. All four were found driving a real showcase boot + against `17.0.0-rc.1` and are catalogued in the #4482 rollup. + + - **#4431 — a sandbox capability denial answered 400.** A denial is the sandbox + refusing to run untrusted code that asked for a capability it does not hold, + which is the crash contract's case (#3951), not a deliberate rejection of a + malformed request. It now answers 500, and the `SandboxError:` debug prefix + no longer reaches the client. + + - **#4435 — PATCH/DELETE of a nonexistent record answered 200 success.** The + write path returned `record: null` / `success: true` for an id that resolves + to nothing, while GET on the same id correctly 404s; `deleteMany` reported + every typo'd id as deleted. Both now answer `RECORD_NOT_FOUND`, so a caller + can no longer read a successful envelope as proof the write landed. + + - **#4436 — the unsupported-filter-operator refusal shipped without + `error.code`.** A refusal with no code is unmatchable by a client, and the + message leaked the internal `[sql-driver]` prefix. It now speaks + `INVALID_FILTER` without the driver prefix. + + - **#4483 — the `$search` auto field set admitted its lead field + unconditionally.** `nameField`/`name`/`title` were prepended without passing + `SEARCH_AUTO_EXCLUDED_FIELDS`, so a search could be aimed at the primary key. + The lead field now only ORDERS the set it is already a member of; it can no + longer admit one. + + These change responses that were observably wrong, so callers coded against the + buggy shapes — a 200 on a missing record, a 400 on a capability denial — will + see different status codes. Graded `minor` on that basis rather than `patch`. + +### Patch Changes + +- a52e2ef: fix(driver-sql,spec,objectql): a `defaultValue` runtime token never becomes a column DEFAULT (#4560) + + `Field.user({ defaultValue: 'current_user' })` is resolved by the **engine**, at + insert time, from the request's `ExecutionContext` — and with no authenticated + user (system / anonymous writes: seed replay, package install, boot + provisioning) `applyFieldDefaults` deliberately leaves the field **unset** + rather than stamp a bogus owner. + + The SQL DDL had never heard of the token. `createColumn` passed any non-object + `defaultValue` straight through to `col.defaultTo(dv)`, so the column was + created as `DEFAULT 'current_user'` and the **database** overrode the engine's + decision: every insert that omitted the field stored the literal string + `current_user` in a `lookup('sys_user')` column — a value that is not any user's + id. `?expand` resolves it to nothing, and on an owner / approver field it is a + silent mis-attribution. Found by #4551's dangling-reference audit on its first + run against a real boot; #4441's referential check could never have caught it, + because it inspects the values a **caller** supplied and here nobody supplied + one. + + **The token vocabulary is now declared once, in `@objectstack/spec/data`** + (`DEFAULT_VALUE_TOKENS`, `isRuntimeDefaultToken`, `isNowDefaultToken`, + `isCurrentUserDefaultToken`, `isAppResolvedDefaultToken`). The engine's + insert-time resolution and the driver's DDL read the same set, which is the + actual defect: `'NOW()'` was special-cased in the branch immediately above for + precisely this reason, and `current_user` — the same convention family — simply + had no entry anywhere the DDL could see. A token added to the set tomorrow is + excluded from literal column DEFAULTs automatically, rather than leaking its own + spelling into the database the way this one did. + + **DDL, in one place** (`applyDeclaredColumnDefault`, shared by column creation + and the SQLite table rebuild): + + - `'NOW()'` → the driver-native canonical default, exactly as before; + - any other runtime token → **no column default at all** (the engine owns it); + - Expression envelopes (`{ dialect, source }`) → unchanged, no default; + - a real literal → emitted verbatim, unchanged. + + **Existing databases carry the wrong DEFAULT**, so it is corrected through the + managed schema-drift path (#2186) rather than a bespoke migration: a new + `default_mismatch` finding with a `drop_column_default` op, categorised `safe` + (the statement cannot fail and touches no rows). Dev boots with + `autoMigrate: 'safe'` reconcile it automatically; everywhere else it is reported + with an actionable hint and applied by `os migrate apply`. Postgres/MySQL use + `ALTER COLUMN … DROP DEFAULT`; SQLite, which cannot alter a default in place, + goes through the existing table rebuild — which now re-materialises every + column's default from **metadata**, so a sibling `defaultValue: 'NOW()'` column + keeps the default it always had instead of losing it to the rebuild. + + **Rows already holding the bogus value are NOT rewritten.** That is #4551's + standing rule — report, never rewrite — so they stay visible to the + dangling-reference audit for operators to resolve deliberately. + +- ec975f1: fix(objectql,driver-mongodb)!: `findOne` must say which record it wants, and executes every option it declares (#4419) + + `findOne` reads a single row, which makes its predicate the only thing between + the caller and _an arbitrary record_. When the predicate is missing the result is + not `null` — it is the object's **first row**: a real, plausible-looking record + with nothing to do with the request, which the `if (!row)` check every call site + already has cannot catch, and which then propagates into whatever is computed + next. Reported downstream: line items defaulting their price from the first + product in the catalog rather than the selected one, and "is this deal already + closed?" answered against an unrelated record while the write that followed + correctly targeted the intended id. A throw would have been caught in + development; a `null` would have been caught by the null-check. A valid-looking + wrong record defeats both. + + **Breaking — `findOne` now refuses a query that selects nothing in particular.** + + FROM → TO: + + | Was | Now write | Meaning | + | ----------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------ | + | `findOne(o)`, `findOne(o, {})`, `findOne(o, { where: {} })` | `findOne(o, { where: … })` | the record matching this predicate | + | | `findOne(o, { search: 'Acme' })` | the record this search finds | + | | `findOne(o, { orderBy: [{ field: 'created_at', order: 'desc' }] })` | the FIRST record in this order — the newest | + | | `find(o, { limit: 1 })` | any row will genuinely do, said at the call site | + + One-line fix: add the `where` you meant, or `orderBy` if you meant "the newest + one", or switch to `find(o, { limit: 1 })` if any row will do. The error names + all four. `find` and `count` are unchanged — returning or counting every row is + an honest answer; only `findOne`'s implicit "just one of them" turns a missing + predicate into a confidently wrong record. The guard reads the CALLER's + predicate, before RLS/sharing middleware injects its own: a tenant filter + narrows which rows are visible, it does not make "whichever comes first" + something the caller asked for. + + **Two silent drops that produced the same wrong record are fixed with it.** + + - **`findOne({ search })` applies the search.** The ADR-0061 `search` → + cross-field `$contains` expansion lived inline in `find` and nowhere else, + while `find` and `findOne` are checked against the SAME legal-key set — so + `search` passed the gate, rode onto the AST, and reached a driver. No driver + reads `ast.search`. The read therefore ran with no predicate at all and + `limit: 1` did the rest. The expansion is now one method both call. + - **`MongoDBDriver.findOne` applies `orderBy`, `fields` and `offset`.** It + translated `query.where` and dropped the rest, so `findOne({ orderBy })` did + not return the newest record — it returned whichever document the scan reached + first. `find` and `_findStream` in the same driver had always handled all + three. This one matters beyond Mongo: the guard above tells an unpredicated + caller to reach for `orderBy`, and an escape hatch one backend ignores is not + an escape hatch. No ordering is IMPOSED when the caller supplies none — both + drivers keep that carve-out (#4363), and `SqlDriver`'s comment about Mongo + "never sorting" is corrected, since it cited the dropped parameter as + agreement. + + **And a gate so the class does not come back.** A drift pin walks + `ENGINE_OPTION_KEY_SETS.findOne` and requires each declared key to have an + observable effect — on the AST the driver receives, on the driver options, or in + an explicit "not executed, and here is why" entry (only `limit`, which the + contract's `limit: 1` overrides). `search` sat declared-but-unexecuted through + two rounds of hardening because nothing asked that question. + + Together with #4346 (`filter` → `where` folds on every entry point) and #4400 + (unknown option keys throw), a read parameter the engine does not execute now + fails at the call site instead of quietly changing the answer. + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [ff17642] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/observability@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Major Changes diff --git a/packages/plugins/driver-sql/package.json b/packages/plugins/driver-sql/package.json index 0b22214418..b56c80886d 100644 --- a/packages/plugins/driver-sql/package.json +++ b/packages/plugins/driver-sql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-sql", - "version": "17.0.0-rc.1", + "version": "17.0.0-rc.2", "license": "Apache-2.0", "description": "SQL Driver for ObjectStack - Supports PostgreSQL, MySQL, SQLite via Knex", "main": "dist/index.js", diff --git a/packages/plugins/driver-sqlite-wasm/CHANGELOG.md b/packages/plugins/driver-sqlite-wasm/CHANGELOG.md index 09eff5c68b..ee4afaf1bd 100644 --- a/packages/plugins/driver-sqlite-wasm/CHANGELOG.md +++ b/packages/plugins/driver-sqlite-wasm/CHANGELOG.md @@ -1,5 +1,161 @@ # @objectstack/driver-sqlite-wasm +## 17.0.0-rc.2 + +### Patch Changes + +- 9b43ee2: test(drivers): the filter-logic standard now covers the backend it was counted without (#4405) + + `FILTER_LOGIC_CASES` (#3774) opens by calling itself the standard "the four + independent FilterCondition backends are each checked against". Five backends + exist. `driver-mongodb`'s `translateFilter` was missed, not excluded — an + independent implementation whose `$and`/`$or`/`$not` translation shares no line + of code with the SQL compiler or the in-memory matcher, and the only one whose + target language cannot spell the standard directly: MongoDB has no + document-level `$not` at all (the server answers `unknown top level operator: +$not`), so a negation has to leave as `$nor`, and a branch's own keys have to + stay in one document while `$and`/`$or` clauses are lifted beside them. That + route was never checked against the shared cases. Both DEBT rows the #4363 gate + recorded are now cleared, and `scripts/check-driver-conformance.mjs` reports + `ok` for every cell of the matrix. + + **`driver-mongodb` runs the table twice, and the split is deliberate.** + `mongodb-filter-logic-translation.test.ts` drives every shared case through + `translateFilter` and evaluates the emitted MongoDB _document_ over the shared + fixture — a pure function, no server, so it always runs. That matters here more + than anywhere: `mongodb-memory-server` downloads a ~123 MB binary from + fastdl.mongodb.org, and a defect only a downloadable binary can catch is a + defect nobody catches on a restricted network. Its in-process reader is strict + by construction — every shape it does not model throws instead of evaluating to + true, a document-level `$not` included — and its own discrimination is pinned by + cases that require a widened document to FAIL the case it widens, so "all green" + cannot mean "the reader says yes to everything". + `mongodb-filter-logic-conformance.test.ts` runs the same table against a real + mongod and answers the one question the first half cannot — does MongoDB agree? + — skipping cleanly (never silently) when the binary is unreachable. + + **`driver-sqlite-wasm` runs the table through its own engine.** It inherits + `SqlDriver`'s filter compiler, so nothing is re-implemented; what the suite pins + is that a nested `(… AND …) OR (… AND …)` survives the custom sql.js dialect + that compiles, binds and marshals it — the same seam its temporal and pagination + suites cover for their clauses. Tracked as DEBT rather than EXEMPT because + "inherits, therefore fine" is the assumption those suites exist to disprove; the + suite is what disproves it. + + **No divergence was found.** `translateFilter` answers all seventeen shared + cases correctly today, `$not`-inside-a-branch and nested `$and`-inside-`$or` + included, so no translation change ships here — what changes is that the next + edit to it cannot quietly widen a filter. Both suites were verified to be + discriminating rather than decorative by reintroducing the #3774 miscompile + (propagating `or` into a branch's own contents): 15 of the mongodb translation + suite's 26 tests fail, and 13 of the wasm suite's 18. + + `packages/spec`'s `filter-logic-conformance.ts` header now says five and names + the fifth — a code comment; no schema, export or generated artifact moved. + +- 24915d2: fix(driver-sqlite-wasm): a `RETURNING` write is a write — persist it (#4518) + + A file-backed `sqlite-wasm` database flushed its schema at boot and then + recorded nothing else. Every table was on disk; every row written after schema + sync lived only in the WASM heap and died with the process. Reopening the file + found a complete, empty database. + + **Cause.** The Knex dialect picked its execution branch from _"does this + statement return rows"_ — and then marked the database dirty only on the other, + row-less branch. `INSERT … RETURNING *` returns rows, so it executed on the + row-returning branch and never set the flag. Since the `on-disconnect` flush is + gated on the same flag, nothing rescued it afterwards either: **both** persist + strategies dropped the write. ObjectQL writes through `RETURNING *` (it hands + the stored row back to the caller), so this covered essentially all business + data, along with `knex.raw('INSERT …')` and any other mutation arriving without + a Knex `method`. + + **Fix.** "Does this statement change the database?" is now one exported + predicate — `statementMutatesDatabase(sql, method)` — classifying by Knex method + _and_ SQL text, applied at a single funnel after execution. It is independent of + which branch executed the statement, so a mutation can no longer slip through by + returning rows, by arriving without a method, or by taking a branch that forgot + to say so. Transaction control still routes to `noteTransactionControl`, which + keeps deferring flushes until the transaction closes (#1494), and mutating + `PRAGMA` assignments (`auto_vacuum`, `user_version`) now count as writes too. + + **What changes for you.** Nothing to author. File-backed wasm SQLite now + actually persists under `on-write` / `debounced:*`, and `disconnect()` is a real + durability boundary: when it returns, committed data is on disk. This is what + `bootStack({ databaseFile })` in `@objectstack/verify` needed to make `stop()` → + second `bootStack` a genuine cold boot — the suspended-run restart proof + ADR-0019 promises is now asserted end to end in the dogfood gate. Expect more + disk writes than before on a file-backed dev database, because previously there + were almost none. + + **One internal signature moved.** `WasmSqliteConnection.markDirty(method?)` is + now `markDirty()`. It used to re-filter the caller's Knex method against its own + allowlist, which made "did this mutate?" a decision taken in two places that + could — and did — disagree. If you call it directly, drop the argument; the + dialect classifies, the connection obeys. + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/driver-sql@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/plugins/driver-sqlite-wasm/package.json b/packages/plugins/driver-sqlite-wasm/package.json index ff0d138e34..19e28680e7 100644 --- a/packages/plugins/driver-sqlite-wasm/package.json +++ b/packages/plugins/driver-sqlite-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-sqlite-wasm", - "version": "17.0.0-rc.1", + "version": "17.0.0-rc.2", "license": "Apache-2.0", "description": "WASM SQLite Driver for ObjectStack — runs in browser/WebContainer (StackBlitz) without native bindings", "keywords": [ diff --git a/packages/plugins/embedder-openai/CHANGELOG.md b/packages/plugins/embedder-openai/CHANGELOG.md index c836e59dd5..0eea3a35a4 100644 --- a/packages/plugins/embedder-openai/CHANGELOG.md +++ b/packages/plugins/embedder-openai/CHANGELOG.md @@ -1,5 +1,69 @@ # @objectstack/embedder-openai +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/plugins/embedder-openai/package.json b/packages/plugins/embedder-openai/package.json index 2bac6583a5..623c98d688 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.1", + "version": "17.0.0-rc.2", "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 62202c4fcb..bce7b0a843 100644 --- a/packages/plugins/knowledge-memory/CHANGELOG.md +++ b/packages/plugins/knowledge-memory/CHANGELOG.md @@ -1,5 +1,71 @@ # @objectstack/knowledge-memory +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/service-knowledge@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/plugins/knowledge-memory/package.json b/packages/plugins/knowledge-memory/package.json index e792e773b6..511f932bee 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.1", + "version": "17.0.0-rc.2", "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 dee3ab38b8..c9cbe6e154 100644 --- a/packages/plugins/knowledge-ragflow/CHANGELOG.md +++ b/packages/plugins/knowledge-ragflow/CHANGELOG.md @@ -1,5 +1,71 @@ # @objectstack/knowledge-ragflow +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/service-knowledge@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/plugins/knowledge-ragflow/package.json b/packages/plugins/knowledge-ragflow/package.json index abb1af34fd..3b408b351c 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.1", + "version": "17.0.0-rc.2", "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 c231bcf3e0..a82ffc5165 100644 --- a/packages/plugins/plugin-approvals/CHANGELOG.md +++ b/packages/plugins/plugin-approvals/CHANGELOG.md @@ -1,5 +1,221 @@ # @objectstack/plugin-approvals +## 17.0.0-rc.2 + +### Minor Changes + +- 2826d1e: fix(automation,approvals): an approval decision can no longer succeed while its flow stays parked (#4420) + + A flow paused at an `approval` node, a deploy, then an approver clicking + Approve: the request row flipped to `approved`, the UI toasted success — and + the flow never moved. No next-stage request, no error, the record's mirrored + status frozen mid-workflow. Approval flows pause for days by design, so a + restart mid-flight is the normal case: every release could quietly zombify + every in-flight approval, with the approvers none the wiser. + + Durable suspended runs (#1518) had shipped and were not the missing piece. Two + other things were. + + **The wiring could enable a store over a table nobody had created.** Object + registration and store activation resolve different services in different + phases — `manifest` at `init()`, `objectql` at `start()` — and the plugin + declared no ordering. Composed ahead of ObjectQL, `init()` found no `manifest`, + warned, and continued; `start()` then attached the DB-backed store anyway. Every + suspend failed with `no such table: sys_automation_run` into a log line nobody + read, pauses silently stayed in memory, and the next restart lost them all. + Now: `AutomationServicePlugin` declares `optionalDependencies: +['com.objectstack.engine.objectql']` (order-if-present, per ADR-0116 — an + engine-less kernel must still boot); a registration missed at `init()` is + retried at `start()`, which still lands before ObjectQL's schema sync; the + store is never attached when registration did not happen, and says so at + **error** level instead of warning; the table is probed once at boot so a + broken setup surfaces there rather than one failed write at a time; and a + failed durable write of a paused run is logged at error — it is data loss in + waiting, not a warning. + + **A reported resume failure read as success.** `AutomationEngine.resume()` + answers a lost run by _returning_ `{ success: false }`, never by throwing. + `ApprovalService` discarded that return value, and `decide()` counted only a + thrown error as failure — so a decision against a dead run came back + `resumed: true`, HTTP 200. Resume failures are now classified + (`RUN_NOT_FOUND`, `STORE_UNAVAILABLE`, `RESUME_IN_PROGRESS`, joining + `PERMISSION_DENIED` / `INVALID_SIGNAL`), so a run that is gone for good is + distinguishable from a store that is merely unreachable, and the raw resume + route maps them to 404 / 503 / 409. + + Approvals acts on them. A new `AutomationEngine.hasSuspendedRun(runId)` — which + reads the suspension store, unlike `getRun()`, and throws rather than answering + `false` when the store is unreadable — pre-flights every flow-advancing + operation (`decide`, `sendBack`, `resubmit`) **before its first write**, so the + zombie half-state is never created rather than merely reported: the decision + fails with `RESUME_TARGET_LOST` (HTTP 409) and the request stays actionable. A + resume that fails after the decision is durable can no longer be undone, but it + now throws `RESUME_FAILED` (HTTP 500) naming the stranded run instead of + reporting success. A concurrent duplicate resume stays benign — the engine's + idempotency guard is doing its job — and reports through the new optional + `resumeError` field. Recall and revise-window cancellation stay non-fatal by + design (they abandon the request), but log at error with the reason instead of + swallowing it. Compositions with no automation engine attached are unaffected. + + Existing zombie requests from affected deployments (already `approved`, run + stranded) are not repaired by this change — `releaseDeadRunRequests` only + sweeps requests that are still `pending`. + +### Patch Changes + +- 5a84d41: fix(approvals): record an admin override of a staffed approver slate AS an override (#4466) + + An admin who is not in a request's `pending_approvers` may still act on it — the + `#3424` privileged-override path exists so a request routed to an unstaffed + position, or to approvers who have all left, is not undecidable forever. The + override is defensible; what was not is what the audit trail recorded. + + `sys_approval_action` had no override column at all. So an admin overriding a + properly-staffed slate wrote a row **byte-for-byte identical** to the designated + approver approving normally: a reader of the timeline saw `approve` by the admin + and could not tell whether the admin _was_ an approver or _overrode_ the ones who + were, and the bypassed approver's later `409 INVALID_STATE` was the only trace — + existing only if they happened to try. The platform knows at decision time (it + took the `isOverrideActor` branch to admit the call at all), so this was dropped + information, not unavailable information. The whole point of an approval record + is to answer "who authorized this, and were they entitled to?". + + `sys_approval_action` now carries **`via_override`** (boolean, optional), set on + exactly the actions admitted by that branch — `decideNode`'s approve/reject and + `reassign`'s admin rescue. It is surfaced on `ApprovalActionRow.via_override` + (`@objectstack/spec/contracts`), returned by `listActions`, and added to the + object's `highlightFields` and two grid list views so a timeline can say + "overrode the approver slate" instead of rendering it as an ordinary approval. + + Three distinctions the column keeps apart deliberately: + + - **`true`** — the actor held no slot in the slate and was admitted only by the + override branch. + - **`false`** — checked, and it was not an override. An admin who _is_ a + designated approver is approving normally and records `false`: the marker is + about which branch admitted the call, not about whether the actor holds admin + rights. + - **absent** — a row written before this column existed. "Not recorded" is not + the same claim as "not an override", so `rowFromAction` maps `null` to + `undefined` rather than to `false`. + + Additive and nullable, so this needs no data migration: existing rows keep + working and simply read as unrecorded. Levelled `patch` rather than `minor` + because nothing an author writes changes — but note it _is_ an observable + behaviour change on a read surface: `listActions` responses and the + `sys_approval_action` grid views now carry a field consumers did not see before, + and `sys_approval_action` gains a column on next schema sync. + +- c2a1134: fix(approvals): find the zombie requests nothing was looking at (#4469) + + #4460 stopped new zombies being produced; the rows already stuck had no mechanism + to find or release them. The failure shape (#4420) is a request flipped to + `approved` / `rejected` / `returned` whose `flow_run_id` points at a run that no + longer exists — the decision landed, the flow never moved. Any deployment on + 17.0.0-rc.1 that hit the wiring hole and crossed a restart mid-approval can be + carrying these rows. + + `releaseDeadRunRequests` could not see them, and the reason is worth stating + plainly: it scans `status: 'pending'`, and the very step that zombifies a request + is the one that takes it OUT of `pending`. The act of breaking it removed it from + the only sweeper's field of view — a large part of why this class of failure + stayed silent. It could not have answered the question even if it had looked: its + liveness oracle is `getRun`, which reads the execution LOG and returns `null` for + a perfectly ALIVE suspended run after a restart. It treats `null` as alive + (conservative, and correct for what it does) — which is exactly why it has no way + to say "this run is really gone". + + Adds `ApprovalService.inspectStrandedRequests()`, which uses BOTH oracles and + reports only rows that fail both: + + - `hasSuspendedRun(runId) === false` — the suspension store itself says no live + pause exists. It THROWS when the store cannot be read, and that case is + SKIPPED and counted as `undetermined`, never condemned: an unreadable store + means "unknown", and a storage outage must not be published as a lost run. + - `getRun(runId) == null` — no terminal history row either. A run that merely + finished is not stranded; a request whose run neither waits nor ever completed + is. + + **It reports; it never rewrites.** No status is changed and no run is cancelled. + The decision genuinely happened — a human approved or rejected — and silently + rolling it back would make the audit trail disagree with the facts. The report + carries what an operator needs to decide: which requests are stuck at which step, + and what the mirrored status field on the business record still reads (usually + the stale value the user is staring at). Whether to re-run the downstream actions + or re-open the approval is a judgement call this cannot make. + + It rides the existing escalation/dead-run sweep clock, so the finding surfaces in + the logs without an operator knowing to go looking for it. `recalled` is + deliberately out of scope: a recall abandons its run on purpose, and reporting + those would bury the real findings under expected ones. + + New export: `StrandedApprovalRequest` (the report row shape). + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + - @objectstack/metadata-core@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/formula@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/plugins/plugin-approvals/package.json b/packages/plugins/plugin-approvals/package.json index b5aeab6afa..682d2e166c 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.1", + "version": "17.0.0-rc.2", "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 1457b3ca77..93627b397e 100644 --- a/packages/plugins/plugin-audit/CHANGELOG.md +++ b/packages/plugins/plugin-audit/CHANGELOG.md @@ -1,5 +1,71 @@ # @objectstack/plugin-audit +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/plugins/plugin-audit/package.json b/packages/plugins/plugin-audit/package.json index 25aa225c5d..a7bf0bd191 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.1", + "version": "17.0.0-rc.2", "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 1490b6a8b1..20eb70aced 100644 --- a/packages/plugins/plugin-auth/CHANGELOG.md +++ b/packages/plugins/plugin-auth/CHANGELOG.md @@ -1,5 +1,112 @@ # Changelog +## 17.0.0-rc.2 + +### Patch Changes + +- c03108c: fix(auth): a degraded tenancy posture must not hand out a default organization + + `TenancyService.defaultOrgId()` documented "returns `null` under any walled + posture", but the implementation keyed on the posture actually **in force** + (`isolationActive()`) rather than the one the operator **requested**. Those two + disagree in exactly one state — DEGRADED: a deployment that asked for `group` + or `isolated` and could not enforce it (the enterprise `@objectstack/organizations` + package is absent) reports `posture: 'single'`, and the resolver then happily + answered with "the `slug='default'` org, or the only org that exists". + + Everything downstream of that resolver binds new users to whatever it returns. + The membership reconciler (ADR-0093 D2) runs on `user.create.after` — the seam + every creation path flows through — so in a degraded deployment **every fresh + signup, admin-created user and SSO JIT user was auto-bound as a `member` of + whichever organization happened to be resolvable**, and `backfillMemberships` + (D6) would sweep the pre-existing member-less ones in on the next + `kernel:ready`. + + This reached production. ObjectStack Cloud's control plane runs + `OS_MULTI_ORG_ENABLED=true` while deliberately not mounting the enterprise + package — it enforces its own control-plane org wall instead — so the + `org-scoping` probe missed, the posture resolved degraded, and self-serve + signups landed inside a stranger's organization with read access to that org's + environments (cloud#957). + + `defaultOrgId()` now keys on `requestedPosture`: any walled request, enforced or + degraded, returns `null` and the framework never guesses. This is the same + judgement D6 already applies to the backfill — "a wrong org in a tenant-isolated + deployment is a data-exposure bug, not a convenience" — applied to the resolver + those consumers share. It also makes the resolver agree with the default-org + bootstrap in `AuthPlugin.start()`, which was already gated on the requested + posture. + + Single-org deployments are unaffected: nothing about `requested: 'single'` + changes. A degraded deployment loses the auto-bind, which is the point — and + ADR-0093 D5 already refuses to boot that deployment at all unless the operator + sets `OS_ALLOW_DEGRADED_TENANCY=1`. + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [8aacf94] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/rest@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/plugins/plugin-auth/package.json b/packages/plugins/plugin-auth/package.json index e7c304e2be..8168e0a785 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.1", + "version": "17.0.0-rc.2", "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 f5d29d004e..0f6c1a8549 100644 --- a/packages/plugins/plugin-dev/CHANGELOG.md +++ b/packages/plugins/plugin-dev/CHANGELOG.md @@ -1,5 +1,92 @@ # @objectstack/plugin-dev +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [257d97a] +- Updated dependencies [e6b1b69] +- Updated dependencies [328ccc5] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [63b33e6] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [ff17642] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [8aacf94] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [1ee48bc] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [26bb053] +- Updated dependencies [04f1182] +- Updated dependencies [c03108c] +- Updated dependencies [d6bd5a1] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/objectql@17.0.0-rc.2 + - @objectstack/plugin-security@17.0.0-rc.2 + - @objectstack/rest@17.0.0-rc.2 + - @objectstack/runtime@17.0.0-rc.2 + - @objectstack/plugin-auth@17.0.0-rc.2 + - @objectstack/driver-memory@17.0.0-rc.2 + - @objectstack/account@17.0.0-rc.2 + - @objectstack/setup@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/plugin-hono-server@17.0.0-rc.2 + - @objectstack/service-i18n@17.0.0-rc.2 + - @objectstack/service-realtime@17.0.0-rc.2 + - @objectstack/service-storage@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/plugins/plugin-dev/package.json b/packages/plugins/plugin-dev/package.json index e3d982a7e6..698b6eaaa5 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.1", + "version": "17.0.0-rc.2", "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 1b1ac0f785..f5e410de4b 100644 --- a/packages/plugins/plugin-email/CHANGELOG.md +++ b/packages/plugins/plugin-email/CHANGELOG.md @@ -1,5 +1,128 @@ # @objectstack/plugin-email +## 17.0.0-rc.2 + +### Minor Changes + +- ce92674: feat(email): declared email templates reach the mail service (#4509) + + Authoring an `email_template` was a silent no-op. `EmailService.sendTemplate` + resolves `(name, locale)` against **`sys_email_template` rows**, and the only + writers of those rows were the built-in auth templates plus a code-constructed + `EmailServicePluginOptions.templates` that no bootstrapper ever passed. Every + door an author can actually use — a stack's `emailTemplates:`, an + `*.email-template.ts` file, Studio's metadata-admin list, `PUT /meta` — parked + items in a metadata store nothing read back. So an admin could "fix" the + password-reset email in Studio, get a success toast, and watch users keep + receiving the built-in copy: ADR-0078 false compliance on **authentication + mail**. This is the shape #3461 had for webhooks, closed the same way (ADR-0049 + enforce-or-remove, route: enforce). + + **`bootstrapDeclaredEmailTemplates`** now materializes declared templates into + `sys_email_template` at boot. Each item is validated through + `EmailTemplateDefinitionSchema.parse()` — the spec schema finally has a real + consumer, defaults and all — and projected with `mapTemplateToRow`, which is the + **same** mapping the built-in seeder uses, extracted and shared so the two doors + cannot drift apart. A malformed template warns and is skipped rather than + crashing boot. + + **Runtime writes take effect immediately.** Unlike `webhook`, `email_template` + is `allowRuntimeCreate: true`, so a boot-only bridge would have left a Studio + save inert until the next restart — the same bug, half-fixed. The plugin also + subscribes to `email_template` metadata changes and re-materializes the single + changed item; withdrawing a template deactivates its rows (across locales) + rather than deleting them. + + **Three breaks sat on this path, not one**, and closing any two of them would + still have shipped a template that never sent: + + - `@objectstack/objectql` never registered a manifest's `emailTemplates:` into + the metadata registry at all — the key was simply missing from the generic + ingestion list, so the bridge's own source was empty. + - The built-in seeder left `managed_by` at the column's `'admin'` default, which + made platform templates masquerade as admin-authored. Since the bridge refuses + to overwrite admin rows, a built-in would have permanently outranked the + template an app declared. Built-ins now stamp `managed_by: 'platform'`. + - Nothing materialized declared metadata into rows. + + **Seed-not-clobber** mirrors `sys_webhook` (#3489) and `sys_sharing_rule` + (#2909): `sys_email_template` gains `managed_by` / `customized`. Declared + templates re-seed every boot as `managed_by: 'package'`; a row an admin created + (`admin`) or edited (`customized`, stamped by a `beforeUpdate` hook) is never + overwritten, so reworded transactional mail survives redeploys. This is a + separate axis from `is_system`, which keeps its existing meaning for built-ins. + + The `email_template` liveness ledger flips from 13 dead properties to fully + live, with an ADR-0054 runtime proof bound on `subject` + (`email-template-materialization`): it boots a real stack, authors a template + that overrides a built-in auth template, and asserts the **authored** wording is + what reaches the transport. + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/formula@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/plugins/plugin-email/package.json b/packages/plugins/plugin-email/package.json index 79d6937a9c..fab080c43a 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.1", + "version": "17.0.0-rc.2", "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 9b8ae1f74c..4703a39c23 100644 --- a/packages/plugins/plugin-hono-server/CHANGELOG.md +++ b/packages/plugins/plugin-hono-server/CHANGELOG.md @@ -1,5 +1,73 @@ # @objectstack/plugin-hono-server +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [ff17642] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/observability@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Major Changes diff --git a/packages/plugins/plugin-hono-server/package.json b/packages/plugins/plugin-hono-server/package.json index 5ccf0350dd..0870be2215 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.1", + "version": "17.0.0-rc.2", "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 4753e35e22..584ad46cf3 100644 --- a/packages/plugins/plugin-pinyin-search/CHANGELOG.md +++ b/packages/plugins/plugin-pinyin-search/CHANGELOG.md @@ -1,5 +1,24 @@ # @objectstack/plugin-pinyin-search +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [257d97a] +- Updated dependencies [20b1a9e] +- Updated dependencies [63b33e6] +- Updated dependencies [a52e2ef] +- Updated dependencies [ce92674] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [1ee48bc] +- Updated dependencies [26bb053] +- Updated dependencies [d6bd5a1] +- Updated dependencies [ad5fe25] + - @objectstack/objectql@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/plugins/plugin-pinyin-search/package.json b/packages/plugins/plugin-pinyin-search/package.json index 742ba5c142..40a9d35114 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.1", + "version": "17.0.0-rc.2", "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 a2bd688320..1feca6b168 100644 --- a/packages/plugins/plugin-reports/CHANGELOG.md +++ b/packages/plugins/plugin-reports/CHANGELOG.md @@ -1,5 +1,71 @@ # @objectstack/plugin-reports +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/plugins/plugin-reports/package.json b/packages/plugins/plugin-reports/package.json index 7eb5555225..022cf47c8b 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.1", + "version": "17.0.0-rc.2", "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 dbfe14f919..da32ecec9d 100644 --- a/packages/plugins/plugin-security/CHANGELOG.md +++ b/packages/plugins/plugin-security/CHANGELOG.md @@ -1,5 +1,162 @@ # @objectstack/plugin-security +## 17.0.0-rc.2 + +### Minor Changes + +- 328ccc5: fix(security,analytics): scope /analytics/query to the caller's readable records, and refuse a measure over a missing field (#4467, #4437) + + Two defects on the analytics query path, both found by the v17 verification run + (#3909 / #4482), both reproduced against a live showcase server before the fix + and re-verified with the same requests after. + + ## #4467 — `/analytics/query` applied no record-level scoping + + `ISecurityService.getReadFilter` documents itself as "the same filter the engine + middleware AND-s into every find", and exists precisely for paths that bypass + that middleware — its own doc comment names the analytics raw-SQL path. But the + chain it mirrors is TWO sibling middlewares: plugin-security's RLS injection and + plugin-sharing's owner/share visibility filter (`buildSharingMiddleware` AND-s + `buildReadFilter` into `ast.where` for `find`/`findOne`/`count`/`aggregate`). + Only the RLS half was ever computed here, and analytics has no other source of + scope, so the OWD/share predicate simply never existed on that path. + + Live repro: `showcase_private_note` is `sharingModel: 'private'`; an admin owns + 5 notes, a member holds read shares on exactly 2 and no `viewAllRecords`. + `GET /data/showcase_private_note` correctly returned 2 for the member, while + `POST /analytics/query {measures:['count']}` returned 5 — and adding + `dimensions:['title']` returned all five titles, i.e. the VALUES of a column + that caller may not read, not merely a bad count. Any authenticated caller who + could reach `/analytics` could enumerate the field values of every row of any + object exposed as a cube, regardless of OWD, sharing rules, or RLS. + + `getReadFilter` now resolves plugin-sharing's `buildReadFilter` through the + late-bound `sharing` service and AND-composes it with the RLS filter — the same + composition the two middlewares reach by both writing into `ast.where`. It also + computes the ADR-0057 D1 `__readScope` depth that the security middleware + normally stashes on the context for plugin-sharing to widen its owner-match + with, using the same `getEffectiveScope` call the middleware makes: no + middleware runs on this path, and without it a caller granted `unit`/`org` read + depth would be silently narrowed to `own`. The sharing predicate is resolved for + every non-system caller AHEAD of the RLS stand-down branches, because those are + the RLS middleware's own early exits and none of them is a reason to drop a + sibling middleware's predicate; a sharing-resolution failure denies outright + rather than falling through to half a scope. + + **Why `minor` rather than `patch`.** This is an observable behaviour change on a + public read surface, in the narrowing direction: analytics results that a + principal could previously read they now cannot. Counts drop, `dimensions` + groupings lose rows, and any dashboard, report, or export built on + `/analytics/query` over an owner-private object will show smaller numbers for + non-superuser principals — correctly, but visibly. Deployments that had (however + unknowingly) come to depend on the unscoped totals will see them change on + upgrade, so this warrants more than a patch-level note even though it is a + security fix. No API signature changed: `ISecurityService.getReadFilter`'s + declaration is untouched — the implementation merely started honouring the + contract it already documented. + + ## #4437 — a measure naming a missing field 500'd with SQLITE_ERROR + + `inferMeasure('ghost_sum')` maps a suffix convention onto a field name and has + no way to know the field exists, so it built `SUM(ghost)`, the driver threw + `no such column`, and the caller got + `500 {"code":"SQLITE_ERROR","message":"Internal server error"}` — a driver error + class as the `error.code` for what is a plain typo, which ADR-0112 forbids. A + dotted spelling took the same path (`measures:['total.sum']` prefix-strips to + `sum` → `SUM(sum)` → 500). The DATA route has refused the identical mistake with + a `400 INVALID_FIELD` naming the field since #4315/#4254. + + `AnalyticsService.ensureCube` now validates each measure's resolved source field + against the backing object's field names before any SQL is built, and rejects + with the same envelope the data route produces (`400 INVALID_FIELD` carrying + `field`, `object`, `param`, `measure`) so one mistake has one shape across + `/data` and `/analytics`. The new `getObjectFieldNames` config hook reads the + same schema registry `isRegisteredObject` already consults and the data path's + own gate reads, so "which fields exist" has a single answer across both routes. + + The gate is tiered exactly like the #3867 cube-inference gate, deliberately + narrow: it applies only when the cube's `sql` is a bare object name (an authored + cube whose `sql` is a real SQL expression has no field list to check against), + only when the probe answers (no data engine, or an external datasource whose + columns are not mirrored locally, stands down), and only to measures whose + source is a bare column — `count(*)` has no source field, and a dotted + cross-object reference resolves through a join this layer cannot see, so both + pass through untouched. `id`/`created_at`/`updated_at` are admitted + unconditionally, matching the data path's `resolveQueryFields`: a gate stricter + than the engine it guards would reject queries that used to work. Validation + runs before the cube is registered, so a rejected query leaves no trace in the + registry — otherwise a retry would find a "registered" cube carrying the bogus + measure and sail straight into SQL. + + This half is `minor` for the same envelope reason: a request that used to return + 500 now returns 400 with a different `code`, which is a visible contract change + for any caller branching on the response. + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/formula@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/plugins/plugin-security/package.json b/packages/plugins/plugin-security/package.json index 2f58c5b8b6..cccd9d52b0 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.1", + "version": "17.0.0-rc.2", "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 fe0eb9be4c..a03765bfbf 100644 --- a/packages/plugins/plugin-sharing/CHANGELOG.md +++ b/packages/plugins/plugin-sharing/CHANGELOG.md @@ -1,5 +1,198 @@ # @objectstack/plugin-sharing +## 17.0.0-rc.2 + +### Major Changes + +- 4b6cac7: feat(spec)!: resolve the three cross-form dual-source names — ShareRecipientType, TransformType, suggestFieldType (#4539) + + Three `dual-source-exports.baseline.json` rows where the two declarations + sharing a name did not even share a FORM (type vs const, or two unrelated + functions), so a wrong import-path pick had no shape overlap to hide behind + and failed far from the cause. Each judged against a three-repo import-level + scan (framework, cloud, objectui — the latter two contained zero references + to all three names). All three rows are deleted from the baseline. + + **Renamed — `./contracts` `ShareRecipientType` → `RecordShareRecipientType`:** + + Two live concepts shared the name. The security zod enum + (`user | team | position | unit_and_subordinates | business_unit`) is the + authorable sharing-RULE recipient vocabulary and keeps the name. The contracts + type describes a different thing — the `recipient_type` a `sys_record_share` + ROW may carry — and its claim to "mirror spec/security" had been false since + `group`→`team`/`guest` were retired there. Its member set is now aligned to + the storage-side gate it actually mirrors, the `SysRecordShare` + `recipient_type` select: `role` (never persistable, zero producers) is + replaced by `position`. Only `user` is enforced (and written) today; + `ISharingService.grant` keeps refusing every other value (ADR-0078). + Fix: `import type { ShareRecipientType } from '@objectstack/spec/contracts'` + (or from `@objectstack/plugin-sharing`, whose re-export is renamed in + lockstep) → `RecordShareRecipientType`; code that named the `'role'` member + was describing a value no row could ever hold — use the rule vocabulary + (`SharingRuleRecipientType`) if a role recipient was meant. + + **Renamed — `./shared` `TransformTypeSchema` / `TransformType` → + `FieldMappingTransformSchema` / `FieldMappingTransform`:** + + `./data`'s `TransformType` (the authorable import-mapping enum + `none | constant | lookup | split | join | javascript | map`) is the live + declaration and keeps the name. `./shared` exported `TransformType` as the + inferred type of `TransformTypeSchema` — a differently-shaped discriminated + union of transform CONFIG objects — with zero importers for either name in + all three repos. The shared pair is renamed (not just the alias deleted): + the docs generator derives `import type { X }` examples by stripping + `Schema` from each schema const, so an alias-less `TransformTypeSchema` + would have kept generating a reference to an export that no longer exists. + Fix: `TransformTypeSchema` → `FieldMappingTransformSchema`, + `import type { TransformType } from '@objectstack/spec/shared'` → + `FieldMappingTransform` (same shape); importers who meant the import-mapping + enum import `TransformType` from `@objectstack/spec/data`. + + **Renamed — `./data` `suggestFieldType` → `suggestFieldTypeForSqlType`:** + + The only function-kind dual-source. The two implementations were never forks + of one function — different signatures, semantics and return types: + `shared/suggestions.zod.ts` (kept on `.` / `./shared` under the original + name) is the typo-suggester for an invalid authored FieldType + (`(input: string) => string[]`, alias table + Levenshtein, feeds the zod + error map), while `data/type-compat.ts` is the deterministic SQL-column → + FieldType mapper for external-datasource drafts + (`(rawType, dialect?) => FieldType | undefined`, ADR-0015 §4.6). Same input, + divergent outputs — `('varchar(255)')` → `[]` vs `'text'`; `('text_area')` → + `['textarea']` vs `undefined`; `('int')` → `['number']` vs `'number'` — and + the wrong pick compiled wherever the result was only truthiness-checked + (`[]` is truthy). Behavioral divergence is now pinned in + `data/type-compat.test.ts`. + Fix: `import { suggestFieldType } from '@objectstack/spec/data'` → + `suggestFieldTypeForSqlType` (same signature); imports from the root entry + or `./shared` are unaffected. + +### Minor Changes + +- ba5ff2f: fix(plugin-sharing): deactivating or deleting a sharing rule actually withdraws its grants (#4433, #4434) + + An over-granting sharing rule had no withdrawal path on the product's API + surface. Deactivating it left every grant it had materialised in place — not on + the next record touch, not after a full restart — and the DELETE route answered + 500 for both address forms it advertises, so the rule could not be removed + either. Together that made a too-broad rule unrecoverable short of hand-editing + `sys_record_share`, against a v17 release note that advertises the opposite + ("switching a rule off actually withdraws access"). + + `minor`, not `patch`: this changes an observable runtime behaviour that + deployments may have adapted to. A `source: 'rule'` grant whose rule is + inactive — or whose rule row is gone — now disappears, on the deactivating + write, on the next touch of the record, and on the next boot. Anything relying + on those rows surviving deactivation (including data repaired by hand around + the old behaviour) will see them revoked on upgrade. `DELETE +/api/v1/sharing/rules/:idOrName` also starts succeeding where it used to 500, + so callers that treated that 500 as "unsupported" will now really delete. + + #4433 — three independent gaps, one per path the report walked: + + - **The deactivating write.** The `sys_sharing_rule` reconcile trigger skipped + every `isSystem` write, on the theory that those were boot seeding. + `SharingRuleService.defineRule` — the only implementation behind + `POST /sharing/rules`, and the documented way to deactivate a rule — writes + with SYSTEM_CTX unconditionally, because it must reach a platform table the + sharing middleware otherwise gates. So the skip caught 100% of REST + authoring: the withdrawal path built by #3821 existed, had tests (against a + mocked session the real path never sends), and was unreachable in production. + Now gated on boot phase, which is the question the skip actually meant to + ask. + - **The record touch.** `evaluateAllForRecord` listed only active rules, so a + deactivated rule was absent from the loop entirely and its grants were never + examined. It now reconciles every rule; an inactive one desires nothing and + takes the existing revoke-the-remainder branch. + - **The boot pass.** `backfillRuleGrants` was handed an `activeOnly` list, + making it structurally incapable of revoking anything. It now walks every + rule, and a new `sweepOrphanedRuleGrants` retires grants whose rule row is + gone entirely — unreachable by rule iteration, so they need their own sweep. + + #4434 — `deleteRule` purged `sys_record_share` with a predicate-shaped + `engine.delete` carrying neither a scalar id nor `multi: true`, the one shape + the engine's dispatch refuses; it threw before ever reaching the rule row. + Fixed by routing through the same `SharingService.revoke` path every other + withdrawal already uses, rather than adding `multi: true` — a rule's grants now + retire exactly one way instead of two divergent ones. + + The unit fakes are part of the fix: `makeEngine().delete` accepted any `where`, + which is why #4434 shipped green — the pre-existing "deleteRule drops rule and + all its grants" test asserted success against a delete the running server + always rejected. The fakes now mirror the real engine's dispatch guard. + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [257d97a] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [63b33e6] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [1ee48bc] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [26bb053] +- Updated dependencies [04f1182] +- Updated dependencies [d6bd5a1] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/objectql@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/formula@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/plugins/plugin-sharing/package.json b/packages/plugins/plugin-sharing/package.json index fc02dd15c1..11771b4e1f 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.1", + "version": "17.0.0-rc.2", "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 84aaef89d1..1f536ccc75 100644 --- a/packages/plugins/plugin-webhooks/CHANGELOG.md +++ b/packages/plugins/plugin-webhooks/CHANGELOG.md @@ -1,5 +1,88 @@ # @objectstack/plugin-webhooks +## 17.0.0-rc.2 + +### Patch Changes + +- 257d97a: ADR-0078 Phase 4, decided rather than deferred: the silent skips stop being silent at runtime. The registry — the one choke point every metadata door goes through — now emits a functional-completeness diagnostic at registration, and the webhook enqueuer's zero-trigger skip warns instead of returning `null` wordlessly. + + **The Phase 4 ruling.** The phase had two halves, and they got opposite verdicts: + + - **Generative rule sweep: rejected — not deferred.** A generator can enumerate candidates ("which optional keys might be load-bearing?") but cannot verify runtime skip sites, and a rule without its skip-site citation is a false prescription — this campaign shipped four of those and every one was caught by the verification pass a generator would skip. The route is structurally wrong; no amount of waiting produces the evidence that would fix it. + - **Registration-time diagnostics: built now.** The evidence was already in hand, not pending: #3896 (Setup authoring inserted `sys_sharing_rule` rows directly, bypassing the schema that "required" `criteria`) and cloud's `rowColor.mapping` (an `as never` cast bypassed tsc) prove that doors which skip Zod and lint are real. The author-time gate only protects metadata that passes through `os build` / `validate` / `lint`; `SchemaRegistry.registerObject` is where _every_ door converges — declared stacks, plugin objects, `extend` contributions, `saveMetaItem`, raw `registerObject` calls. + + **Same predicate, same rule ids, different posture.** The registry calls the same `checkFieldCompleteness` that `validate-functional-completeness` uses, so the boot log carries the _same rule ids_ the lint reports (`field/summary-without-operations`, …) — an operator or an AI reading the log greps the id straight into the same docs and suppression story. But the registry **warns and never throws**: ADR-0078 §1's error severity means _the instance is dead_, not _the system is dead_ — an inert field must not kill a boot that thousands of healthy objects share. Errors block at author time; the registry's job is to make sure the silence never survives to runtime unobserved. + + One line per object with every finding aggregated (not per request — the hot path stays free; not per finding — a three-dead-field object is one greppable line). Follows `warnStrippedLegacyApiMethods` (#3543) exactly: module-level once-per-object dedup, injectable `warn`, pure observation that never mutates the schema. + + **The webhook skip now names itself.** `auto-enqueuer.ts`'s `if (triggers.size === 0) return null` sat under a comment blessing the empty case as "a manual-only webhook" — a mode #3196 removed (no manual fire path exists). The skip now warns with the author-time rule id (`webhook/without-triggers`), and the comment tells the truth. Only _active_ rows reach the parse (`where: { active: true }` — verified, not assumed), so a deliberately disabled webhook stays warning-free. + + **Scope honesty:** field rules and the webhook rule get the runtime twin. `view/layout-without-binding` stays author-time-only — views don't register through this choke point and the renderer half of the evidence lives in objectui. + + Tracked in #4544. This closes the ADR-0078 loop end to end: author-time error, runtime warning, one shared predicate deciding both. + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/service-messaging@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/plugins/plugin-webhooks/package.json b/packages/plugins/plugin-webhooks/package.json index 4ea65d524f..e875cea99b 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.1", + "version": "17.0.0-rc.2", "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 9bc30a5124..81a8212410 100644 --- a/packages/qa/dogfood/CHANGELOG.md +++ b/packages/qa/dogfood/CHANGELOG.md @@ -1,5 +1,97 @@ # @objectstack/dogfood +## 0.0.40-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [257d97a] +- Updated dependencies [e6b1b69] +- Updated dependencies [328ccc5] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [63b33e6] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [d449b0c] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [1ee48bc] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [ba5ff2f] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [26bb053] +- Updated dependencies [04f1182] +- Updated dependencies [c03108c] +- Updated dependencies [d6bd5a1] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [c2a1134] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/objectql@17.0.0-rc.2 + - @objectstack/plugin-webhooks@17.0.0-rc.2 + - @objectstack/plugin-security@17.0.0-rc.2 + - @objectstack/service-analytics@17.0.0-rc.2 + - @objectstack/example-crm@4.0.92-rc.2 + - @objectstack/plugin-sharing@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + - @objectstack/plugin-email@17.0.0-rc.2 + - @objectstack/example-showcase@0.3.14-rc.2 + - @objectstack/plugin-auth@17.0.0-rc.2 + - @objectstack/verify@17.0.0-rc.2 + - @objectstack/connector-mcp@17.0.0-rc.2 + - @objectstack/connector-openapi@17.0.0-rc.2 + - @objectstack/connector-rest@17.0.0-rc.2 + - @objectstack/mcp@17.0.0-rc.2 + - @objectstack/plugin-audit@17.0.0-rc.2 + - @objectstack/service-messaging@17.0.0-rc.2 + - @objectstack/service-storage@17.0.0-rc.2 + ## 0.0.40-rc.1 ### Patch Changes diff --git a/packages/qa/dogfood/package.json b/packages/qa/dogfood/package.json index 3a48f1dbe4..2e65744c41 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.1", + "version": "0.0.40-rc.2", "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 ed43c0de81..105311f976 100644 --- a/packages/qa/downstream-contract/CHANGELOG.md +++ b/packages/qa/downstream-contract/CHANGELOG.md @@ -1,5 +1,69 @@ # @objectstack/downstream-contract +## 0.0.38-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + ## 0.0.38-rc.1 ### Patch Changes diff --git a/packages/qa/downstream-contract/package.json b/packages/qa/downstream-contract/package.json index 439517ccf7..7cd94453c1 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.1", + "version": "0.0.38-rc.2", "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 6ca0462c7f..62a45d2b1d 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.2 + +### Patch Changes + +- @objectstack/core@17.0.0-rc.2 + ## 0.0.6-rc.1 ### Patch Changes diff --git a/packages/qa/http-conformance/package.json b/packages/qa/http-conformance/package.json index 32cad0f268..82e9cab70f 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.1", + "version": "0.0.6-rc.2", "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 5d4fa2f250..75b4eecc97 100644 --- a/packages/rest/CHANGELOG.md +++ b/packages/rest/CHANGELOG.md @@ -1,5 +1,176 @@ # @objectstack/rest +## 17.0.0-rc.2 + +### Minor Changes + +- 8aacf94: feat(rest,runtime,client): `POST /meta/_migrate-stored` — run the stored-metadata migration without a shell (#4327) + + `os migrate meta --stored` (#4327) gave ADR-0087's stored-metadata chain a finish + line, but only for someone who can reach the deployment's database from a + terminal. A hosted operator cannot, so on a managed deployment the chain had no + finish line at all — just the per-read conversion, running forever, with no way + to assert what protocol the rows are on. + + The same pass is now reachable over HTTP: + + ```ts + const preview = await client.meta.migrateStored(); // writes nothing + const result = await client.meta.migrateStored({ apply: true }); + const flows = await client.meta.migrateStored({ types: ["flow"] }); + ``` + + It returns the same `StoredMigrationReport` the CLI renders, and takes the same + posture: + + - **Preview by default.** `apply` must be literally `true`; an empty body, a + missing body, and `"apply": "yes"` all preview. Nothing is inferred. + - **Gated on `manage_metadata`.** Unlike the single-item `PUT /meta/:type/:name` + next door, this rewrites every eligible row in the deployment, so it demands + the ADR-0066 D1 authoring capability rather than just a session, and answers + `403` otherwise. The gate runs before the protocol is probed, so an + unauthorized caller cannot use `403`-vs-`501` to learn which kernels can be + migrated. `/meta`'s anonymous-deny umbrella still closes it to anonymous + callers first. + - **Attributed to the caller.** The `actor` recorded on the history and audit + rows names the user who fired it — that is the question those rows exist to + answer. + + **Flows need no extra setup on this path.** The CLI has to boot an inert + automation engine to hold the executor registry ADR-0078's conflict guard needs; + a server already has a live one, and the protocol resolves it from the services + registry itself (#4498), so this route covers flow rows by simply running in the + process that owns them. + + Registered on both the REST server and the runtime dispatcher's `/meta` domain, + ledgered in both route ledgers, and mounted before `/:type` so the + leading-underscore segment is never captured as a metadata type name. + +### Patch Changes + +- 2826d1e: fix(automation,approvals): an approval decision can no longer succeed while its flow stays parked (#4420) + + A flow paused at an `approval` node, a deploy, then an approver clicking + Approve: the request row flipped to `approved`, the UI toasted success — and + the flow never moved. No next-stage request, no error, the record's mirrored + status frozen mid-workflow. Approval flows pause for days by design, so a + restart mid-flight is the normal case: every release could quietly zombify + every in-flight approval, with the approvers none the wiser. + + Durable suspended runs (#1518) had shipped and were not the missing piece. Two + other things were. + + **The wiring could enable a store over a table nobody had created.** Object + registration and store activation resolve different services in different + phases — `manifest` at `init()`, `objectql` at `start()` — and the plugin + declared no ordering. Composed ahead of ObjectQL, `init()` found no `manifest`, + warned, and continued; `start()` then attached the DB-backed store anyway. Every + suspend failed with `no such table: sys_automation_run` into a log line nobody + read, pauses silently stayed in memory, and the next restart lost them all. + Now: `AutomationServicePlugin` declares `optionalDependencies: +['com.objectstack.engine.objectql']` (order-if-present, per ADR-0116 — an + engine-less kernel must still boot); a registration missed at `init()` is + retried at `start()`, which still lands before ObjectQL's schema sync; the + store is never attached when registration did not happen, and says so at + **error** level instead of warning; the table is probed once at boot so a + broken setup surfaces there rather than one failed write at a time; and a + failed durable write of a paused run is logged at error — it is data loss in + waiting, not a warning. + + **A reported resume failure read as success.** `AutomationEngine.resume()` + answers a lost run by _returning_ `{ success: false }`, never by throwing. + `ApprovalService` discarded that return value, and `decide()` counted only a + thrown error as failure — so a decision against a dead run came back + `resumed: true`, HTTP 200. Resume failures are now classified + (`RUN_NOT_FOUND`, `STORE_UNAVAILABLE`, `RESUME_IN_PROGRESS`, joining + `PERMISSION_DENIED` / `INVALID_SIGNAL`), so a run that is gone for good is + distinguishable from a store that is merely unreachable, and the raw resume + route maps them to 404 / 503 / 409. + + Approvals acts on them. A new `AutomationEngine.hasSuspendedRun(runId)` — which + reads the suspension store, unlike `getRun()`, and throws rather than answering + `false` when the store is unreadable — pre-flights every flow-advancing + operation (`decide`, `sendBack`, `resubmit`) **before its first write**, so the + zombie half-state is never created rather than merely reported: the decision + fails with `RESUME_TARGET_LOST` (HTTP 409) and the request stays actionable. A + resume that fails after the decision is durable can no longer be undone, but it + now throws `RESUME_FAILED` (HTTP 500) naming the stranded run instead of + reporting success. A concurrent duplicate resume stays benign — the engine's + idempotency guard is doing its job — and reports through the new optional + `resumeError` field. Recall and revise-window cancellation stay non-fatal by + design (they abandon the request), but log at error with the reason instead of + swallowing it. Compositions with no automation engine attached are unaffected. + + Existing zombie requests from affected deployments (already `approved`, run + stranded) are not repaired by this change — `releaseDeadRunRequests` only + sweeps requests that are still `pending`. + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [ff17642] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/observability@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/service-package@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Major Changes @@ -145,8 +316,8 @@ being a hand-listed union and a bare `string` respectively and reference the catalog, so the three cannot drift apart. - Lowercase is deliberate, not an oversight against ADR-0112's SCREAMING_SNAKE: a - top-level code names the condition the _request_ hit, while a field-level code + Lowercase is deliberate, not an oversight against ADR-0112's SCREAMING*SNAKE: a + top-level code names the condition the \_request* hit, while a field-level code names the _constraint_ the value violated — and constraints are declared in the metadata's own snake_case, so `max_length` the code and `max_length: 50` the property are the same word on purpose. diff --git a/packages/rest/package.json b/packages/rest/package.json index 711fd9d9d4..29268674bd 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/rest", - "version": "17.0.0-rc.1", + "version": "17.0.0-rc.2", "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 c443e6cf3e..33ae1204ba 100644 --- a/packages/runtime/CHANGELOG.md +++ b/packages/runtime/CHANGELOG.md @@ -1,5 +1,499 @@ # @objectstack/runtime +## 17.0.0-rc.2 + +### Minor Changes + +- 63b33e6: A `datasourceMapping` rule is routing, not a hint — an object mapped to an + unreachable datasource no longer silently reads and writes the DEFAULT store + (#4462). + + **Observable behavior change; read this before upgrading.** Measured on `main` + during the v17 verification: map an object to a Postgres datasource with a bad + URL and the boot succeeds, `/ready` answers `200`, the datasource name appears in + **zero** log lines, `POST /api/v1/data/` returns `201` — and the + row is physically in the default store. The operator finds out by opening the + database they declared and finding it empty. ADR-0062 D2's phase-1 note called a + mapping-only datasource "decorative" to keep an example byte-for-byte unchanged; + what that bought was a silent data-placement bug. + + The fix is a pair, and each half is what makes the other correct: + + 1. **Routing stops falling through** (`@objectstack/objectql`). `getDriver` step + 2: a mapping rule that MATCHES and names a datasource with no live driver now + throws — `DatasourceUnavailableError` when the connect layer recorded a + verdict, otherwise an error naming the object, the datasource and the two + remedies. `default` still resolves onward: the default driver keeps its + natural name (#3826), so step 5 is how routing to it works. + 2. **ADR-0062 D2 grows gate (d)** (`@objectstack/service-datasource`, + `@objectstack/runtime`). A datasource a mapping rule routes at least one + object to is auto-connected at boot, and a boot-time connect failure is + **fatal** with an operator-readable reason — the same call gate (b) already + makes for an explicit `object.datasource` binding, now correct for (d) + because half 1 removed the fallback. `OS_ALLOW_DRIVER_CONNECT_FAILURE` still + degrades the boot instead, as for every other fatal connect. + + The mapped-object list is resolved by the boot path from the engine's own + matcher (`ObjectQLEngine.resolveMappedDatasource`, newly public) and passed to + `connectDeclared({ mappedObjects })`; the connection service never re-derives + rule matching. Two matchers drifting by one clause would connect a datasource + routing never uses, or route to one nothing connects — the defect again. + + **What to do if this breaks your boot.** It means a `datasourceMapping` rule in + your stack points at a datasource that cannot be connected. Either fix the + datasource configuration, or delete the rule — the second is what + `examples/app-crm` did in this change, and it is what keeps that example's + runtime behavior identical: its rules routed everything to an unconnected + `:memory:` datasource, i.e. to the default store by fall-through. + +- ac471a0: **BREAKING**: `IAutomationService.getSuspendedScreen(runId)` is now **async** — it returns `Promise` instead of `ScreenSpec | null` (#4515). + + FROM → TO for anyone calling or implementing it: + + ```ts + // caller + - const screen = automationService.getSuspendedScreen(runId); + + const screen = await automationService.getSuspendedScreen(runId); + + // implementer + - getSuspendedScreen(runId: string): ScreenSpec | null + + async getSuspendedScreen(runId: string): Promise + ``` + + One-line fix: `await` the call (the enclosing function is almost certainly already `async`), and make any test double resolve rather than return (`mockResolvedValue`, not `mockReturnValue`). + + Why it had to change: the method could only ever read the engine's in-memory hot cache, because a synchronous signature cannot consult the durable suspended-run store. `SuspendedRun.screen` _is_ persisted (`sys_automation_run.screen_json`) and `resume()` cold-reads it back, so after a process restart a still-suspended screen run could be resumed (`POST …/runs/:runId/resume` → 200) while `GET …/runs/:runId/screen` returned 404 “No pending screen for run” — the refresh-safe re-fetch failing in exactly the situation it exists for (page refresh, another device), and the rendering half of ADR-0019's durable-suspend promise missing while the resuming half shipped. + + `AutomationEngine.getSuspendedScreen` now takes the hot cache as its fast path and falls through to the store via the same loader `resume()` rehydrates from. A run that does not exist, is no longer suspended, or paused at a non-screen node still resolves to `null`, so `GET …/runs/:runId/screen` keeps returning 404 for genuinely absent runs. No sync variant of the method remains on the contract. + +- eb4204b: feat(automation): a `script` node's purity contract is declared, and a function that writes can say so (#4396) + + The `script` executor's contract — _the named function returns a value; data I/O + stays on the flow graph_ — existed only as a comment inside the executor, while + #4354's run summary depended on it. That summary reports no record metrics for a + `script` step precisely because a pure function's writes are downstream + `create_record` / `update_record` nodes counting themselves. A function that + wrote anyway made its run report `selected: 30, acted: 0` — indistinguishable + from the broken sweep the counters exist to detect, recorded permanently on + `sys_automation_run`. + + **The rule is now visible.** `ActionDescriptor` carries + `handlerContract: 'none' | 'pure'`, and the `script` descriptor publishes + `'pure'`, so the action catalog, the designer palette and the reference docs + state the rule an author has to follow instead of an executor holding it + privately. + + **And a legitimate writer can opt out honestly.** A `defineStack({ functions })` + entry may declare what it does, in either shape: + + ```ts + defineStack({ + functions: { + scoreLead: (ctx) => ({ score: 42 }), // pure — the default + syncBilling: { handler: syncBilling, effect: "writes" }, // declared writer + }, + }); + ``` + + A step calling a declared writer reports `unmeasuredEffect`, so the run's + `unmeasured` tally keeps the broken-sweep query + (`selected > 0 AND acted = 0 AND unmeasured = 0`) off that flow — and only that + flow. Marking _every_ `script` step unmeasured was rejected: it would blind the + detector on every flow that calls any function in order to cover the few that + break the rule. + + Nothing here is retired or renamed: a bare `functions: { fn }` entry is + unchanged and means `effect: 'pure'`. The declaration is carried end to end — + `ObjectQL.registerFunction` accepts `{ packageId, effect }` alongside the + existing `packageId` string and exposes `resolveFunctionEntry(name)`, + `objectstack build` lowers a declared entry without dropping it, and the + artifact loader re-attaches the module's callable to the declaration the JSON + carried. + + **Also fixed:** `bindHooksToEngine` returned before registering a bundle's + functions when the stack declared no hooks, so a flow-only app's + `defineStack({ functions })` reached the engine as nothing and every `script` + node calling one failed with "no function named 'x' is registered". + +- 8aacf94: feat(rest,runtime,client): `POST /meta/_migrate-stored` — run the stored-metadata migration without a shell (#4327) + + `os migrate meta --stored` (#4327) gave ADR-0087's stored-metadata chain a finish + line, but only for someone who can reach the deployment's database from a + terminal. A hosted operator cannot, so on a managed deployment the chain had no + finish line at all — just the per-read conversion, running forever, with no way + to assert what protocol the rows are on. + + The same pass is now reachable over HTTP: + + ```ts + const preview = await client.meta.migrateStored(); // writes nothing + const result = await client.meta.migrateStored({ apply: true }); + const flows = await client.meta.migrateStored({ types: ["flow"] }); + ``` + + It returns the same `StoredMigrationReport` the CLI renders, and takes the same + posture: + + - **Preview by default.** `apply` must be literally `true`; an empty body, a + missing body, and `"apply": "yes"` all preview. Nothing is inferred. + - **Gated on `manage_metadata`.** Unlike the single-item `PUT /meta/:type/:name` + next door, this rewrites every eligible row in the deployment, so it demands + the ADR-0066 D1 authoring capability rather than just a session, and answers + `403` otherwise. The gate runs before the protocol is probed, so an + unauthorized caller cannot use `403`-vs-`501` to learn which kernels can be + migrated. `/meta`'s anonymous-deny umbrella still closes it to anonymous + callers first. + - **Attributed to the caller.** The `actor` recorded on the history and audit + rows names the user who fired it — that is the question those rows exist to + answer. + + **Flows need no extra setup on this path.** The CLI has to boot an inert + automation engine to hold the executor registry ADR-0078's conflict guard needs; + a server already has a live one, and the protocol resolves it from the services + registry itself (#4498), so this route covers flow rows by simply running in the + process that owns them. + + Registered on both the REST server and the runtime dispatcher's `/meta` domain, + ledgered in both route ledgers, and mounted before `/:type` so the + leading-underscore segment is never captured as a metadata type name. + +- ea90179: fix(data,runtime,drivers): four ADR-0112 envelope defects found in the v17 verification sweep (#4431, #4435, #4436, #4483) + + Four independent surfaces where the answer a caller received contradicted the + contract the surface declares. All four were found driving a real showcase boot + against `17.0.0-rc.1` and are catalogued in the #4482 rollup. + + - **#4431 — a sandbox capability denial answered 400.** A denial is the sandbox + refusing to run untrusted code that asked for a capability it does not hold, + which is the crash contract's case (#3951), not a deliberate rejection of a + malformed request. It now answers 500, and the `SandboxError:` debug prefix + no longer reaches the client. + + - **#4435 — PATCH/DELETE of a nonexistent record answered 200 success.** The + write path returned `record: null` / `success: true` for an id that resolves + to nothing, while GET on the same id correctly 404s; `deleteMany` reported + every typo'd id as deleted. Both now answer `RECORD_NOT_FOUND`, so a caller + can no longer read a successful envelope as proof the write landed. + + - **#4436 — the unsupported-filter-operator refusal shipped without + `error.code`.** A refusal with no code is unmatchable by a client, and the + message leaked the internal `[sql-driver]` prefix. It now speaks + `INVALID_FILTER` without the driver prefix. + + - **#4483 — the `$search` auto field set admitted its lead field + unconditionally.** `nameField`/`name`/`title` were prepended without passing + `SEARCH_AUTO_EXCLUDED_FIELDS`, so a search could be aimed at the primary key. + The lead field now only ORDERS the set it is already a member of; it can no + longer admit one. + + These change responses that were observably wrong, so callers coded against the + buggy shapes — a 200 on a missing record, a 400 on a capability denial — will + see different status codes. Graded `minor` on that basis rather than `patch`. + +- dadb43f: refactor(spec,client,metadata-protocol,runtime)!: retire the workflow service slot — declared end to end, implemented nowhere (#4451) + + The `workflow` slot was ADR-0078's silently-inert declaration at every layer at + once: a `CoreServiceName` nothing ever registered or resolved (ADR-0115 + Evidence 5 — "no code in this repository resolves either slot", verified across + both repositories), an `IWorkflowService` contract with zero implementations, a + `WorkflowProtocol` whose three methods no code ever provided, a discovery + `routes.workflow` field no builder could truthfully populate, and a + `/api/v1/workflow` advertisement for a path no host ever mounted (the + pre-#3586 `DEFAULT_DISPATCHER_ROUTES` already listed it among routes that + never existed). The capability it promised is live elsewhere and has been for + majors: record state machines are enforced by the `state_machine` validation + rule, approvals are first-class flow nodes on the approvals runtime + (ADR-0019), and record-triggered automation is lifecycle hooks + + `record_change` flows (`service-automation`). + + FROM → TO: + + - `CoreServiceName 'workflow'` / `ServiceRequirementDef.workflow` / + `CORE_SERVICE_PROVIDER['workflow']` → removed; there is no slot to fill. + - `IWorkflowService` (`@objectstack/spec/contracts`) → removed; no + implementation ever existed. Register nothing — use the mechanisms above. + - `WorkflowProtocol` + `GetWorkflowConfigRequest/Response`, + `WorkflowState`, `GetWorkflowStateRequest/Response`, + `WorkflowTransitionRequest/Response` (`@objectstack/spec/api`) → removed, + along with the seven published JSON schemas. Delete the import; nothing + ever answered these shapes. + - Discovery `routes.workflow` / `services.workflow` / `features.workflow` + (metadata-protocol + runtime builders) → absent. A reader keying on them + only ever saw `unavailable` / `false`; delete the read. + - `RouterConfig.mounts.workflow` → removed; there was never a surface to + mount at it. + - `RestApiRouteCategory 'workflow'` → removed; categorize automation-adjacent + routes as `'automation'`. + - `@objectstack/client` re-exports of the four workflow types → removed with + their source. (The `client.workflow.*` methods were already removed earlier + in the v17 cycle — this retires the types they returned.) + - Also removed: the stray `graphql` entry in `CORE_SERVICE_PROVIDER` and the + `graphql: { route: '/graphql' }` discovery entry — `graphql` was never a + `CoreServiceName`, and the dispatcher had already dropped `/graphql` as out + of the product plan (#2462 follow-on). + + The retirement kit: the `workflow-service-slot-retired` semantic migration + (major 17) carries this prescription into `spec-changes.json`, the generated + upgrade guide and the `spec_changes` MCP tool. These are TS/API surfaces and a + discovery response field — never stored in stack metadata — so there is no + load-path conversion and nothing for `os migrate meta` to rewrite; the + 21 `authorable-surface.json` baseline lines and 7 `json-schema.manifest.json` + entries for the deleted schemas are dropped deliberately in the same change + (the plugin-runtime precedent: a prescription nobody can receive is noise — + nothing parses these shapes any more). + +### Patch Changes + +- 2826d1e: fix(automation,approvals): an approval decision can no longer succeed while its flow stays parked (#4420) + + A flow paused at an `approval` node, a deploy, then an approver clicking + Approve: the request row flipped to `approved`, the UI toasted success — and + the flow never moved. No next-stage request, no error, the record's mirrored + status frozen mid-workflow. Approval flows pause for days by design, so a + restart mid-flight is the normal case: every release could quietly zombify + every in-flight approval, with the approvers none the wiser. + + Durable suspended runs (#1518) had shipped and were not the missing piece. Two + other things were. + + **The wiring could enable a store over a table nobody had created.** Object + registration and store activation resolve different services in different + phases — `manifest` at `init()`, `objectql` at `start()` — and the plugin + declared no ordering. Composed ahead of ObjectQL, `init()` found no `manifest`, + warned, and continued; `start()` then attached the DB-backed store anyway. Every + suspend failed with `no such table: sys_automation_run` into a log line nobody + read, pauses silently stayed in memory, and the next restart lost them all. + Now: `AutomationServicePlugin` declares `optionalDependencies: +['com.objectstack.engine.objectql']` (order-if-present, per ADR-0116 — an + engine-less kernel must still boot); a registration missed at `init()` is + retried at `start()`, which still lands before ObjectQL's schema sync; the + store is never attached when registration did not happen, and says so at + **error** level instead of warning; the table is probed once at boot so a + broken setup surfaces there rather than one failed write at a time; and a + failed durable write of a paused run is logged at error — it is data loss in + waiting, not a warning. + + **A reported resume failure read as success.** `AutomationEngine.resume()` + answers a lost run by _returning_ `{ success: false }`, never by throwing. + `ApprovalService` discarded that return value, and `decide()` counted only a + thrown error as failure — so a decision against a dead run came back + `resumed: true`, HTTP 200. Resume failures are now classified + (`RUN_NOT_FOUND`, `STORE_UNAVAILABLE`, `RESUME_IN_PROGRESS`, joining + `PERMISSION_DENIED` / `INVALID_SIGNAL`), so a run that is gone for good is + distinguishable from a store that is merely unreachable, and the raw resume + route maps them to 404 / 503 / 409. + + Approvals acts on them. A new `AutomationEngine.hasSuspendedRun(runId)` — which + reads the suspension store, unlike `getRun()`, and throws rather than answering + `false` when the store is unreadable — pre-flights every flow-advancing + operation (`decide`, `sendBack`, `resubmit`) **before its first write**, so the + zombie half-state is never created rather than merely reported: the decision + fails with `RESUME_TARGET_LOST` (HTTP 409) and the request stays actionable. A + resume that fails after the decision is durable can no longer be undone, but it + now throws `RESUME_FAILED` (HTTP 500) naming the stranded run instead of + reporting success. A concurrent duplicate resume stays benign — the engine's + idempotency guard is doing its job — and reports through the new optional + `resumeError` field. Recall and revise-window cancellation stay non-fatal by + design (they abandon the request), but log at error with the reason instead of + swallowing it. Compositions with no automation engine attached are unaffected. + + Existing zombie requests from affected deployments (already `approved`, run + stranded) are not repaired by this change — `releaseDeadRunRequests` only + sweeps requests that are still `pending`. + +- ff17642: fix(runtime): declarative `defineJob` cron jobs are actually scheduled (#4567) + + Every background job authored as `defineJob({ schedule: { type: 'cron', … } })` + was **silently never scheduled**. `JobSchema.parse` rewrites the cron + `expression` into the canonical expression envelope + (`{ dialect: 'cron', source: '0 1 * * *' }` — the authoring/persistence tier), + but `AppPlugin` handed `job.schedule` verbatim to `IJobService.schedule`, whose + boundary contract documents `expression` as a **bare cron string** because + `CronJobAdapter` passes it straight to croner. croner rejected the object + (`CronPattern: Pattern has to be of type string.`), the throw was swallowed by a + per-job `try/catch` that only `warn`ed, and the author saw a green build and a + green boot with the job never running. `interval` / `once` schedules and + flow `schedule` triggers were unaffected. + + **Fix (contract-first).** The authoring→boundary downgrade now happens at the one + place the two tiers meet — `AppPlugin`'s declarative-job registration, alongside + the existing `retryPolicy` / `timeout` threading — via + `toBoundaryJobSchedule()`. The adapters stay strict: no `typeof === 'object'` + tolerance was added downstream, so the boundary keeps exactly one shape. + A schedule that cannot be reduced to it (unknown type, AST-only or non-`cron` + expression envelope, missing `intervalMs` / `at`) is rejected by name. + + **The failure path is no longer silent.** A job that cannot be scheduled now logs + at **error** level with its own message (`Background job FAILED TO SCHEDULE — it +will never run`), plus a boot summary line when any job failed, and increments + the new `job_schedule_failures_total` counter + (`SEMCONV.jobScheduleFailuresTotal`, labels `app` / `job`) on the observability + metrics registry. "Failed to schedule" no longer shares the quiet `warn` used by + "handler not found in bundle.functions" — the first is an outage of declared + work, the second is a job that was never going to run. + + No authoring change is required: existing `defineJob` cron declarations start + working on upgrade. + +- 20bc357: fix(spec,metadata-protocol,runtime): discovery stops advertising routes for the kernel-internal cache/queue/job slots (#4318) + + The metadata-protocol discovery builder declared `/api/v1/cache`, `/api/v1/queue` + and `/api/v1/jobs` — three paths that existed nowhere else in the repository: no + dispatcher domain, no adapter mount, no plugin registration, and the shipped + providers (`service-cache`/`-queue`/`-job`) are in-process contracts that will + never mount one. Every default boot therefore advertised a route inside the same + `ServiceInfo` whose `handlerReady: false` said the opposite — a single record + contradicting itself (ADR-0076 D12). + + These slots are route-less now, like `realtime` — but unlike `realtime` an + unmarked real implementation stays `available`: the slot's contract is + in-process, so "no HTTP surface" is not reduced capability for it. `handlerReady` + is reported `false` on both discovery builders — for a route-less slot it is not + a proxy for anything, it is the fact itself (the dispatcher used to claim + `handlerReady: true` here for an unmarked occupant, a handler that does not + exist). The explanatory message is written once, as + `inProcessServiceMessage(slot)` in `@objectstack/spec/system`, so the two + builders cannot drift apart. + +- 5a84d41: fix(automation): `resume` enforces the suspended screen's declared field contract (#4477) + + A `screen` node's `config.fields` is a complete input contract — the author + declares the keys, their `required`-ness, and (via `visibleWhen`) when a field + is even asked for. The RENDER half honoured all of it: the paused result and + `GET …/runs/:runId/screen` carry `required` and `visibleWhen` intact. There was + no VALIDATION half — `POST …/runs/:runId/resume` folded whatever bag it was + handed straight into the flow variables, so a caller that skipped the dialog and + posted here directly was unconstrained by every `required` the author wrote. + Missing required fields, and keys the screen never declared, all completed the + run with `success: true`. + + Screen flows are the one place where the declared field contract is the ONLY + contract — no object schema sits behind a screen node to catch a bad bag + downstream. The platform already enforces the analogous contract everywhere else + this seam appears: action params (ADR-0104 D2), record writes (ADR-0113), + approval `decisionOutputs` (#3447). This is that rule for screen resume, built in + the same shape. + + `resume` now refuses a non-conforming submission with the new + `AutomationResult.code` `'INVALID_SCREEN_INPUT'` (a transport maps it to **400**, + as the automation domain route now does) and an `Invalid screen input: …` message + that names each violation and lists the declared field names. The refusal happens + BEFORE the suspension is consumed, so the pause stays live and the legitimate + submission still lands. + + `visibleWhen` is evaluated against the SUBMITTED values first (layered over the + run's variable snapshot), so a hidden field's `required` never fires — enforcing + it would dead-end the run at a field the user was never shown, which is #3528 + reproduced server-side. A predicate that cannot be evaluated is logged and + treated as hidden rather than visible: the client decides what the user saw, and + a broken predicate is not evidence a field was on screen. + + Scope, deliberately narrow — three shapes keep the historical pass-through: + + - an **object-form** screen (`kind: 'object-form'`), whose `fields` is empty by + construction because the client renders the object's own form and the write + path enforces that object's `required` fields itself; + - a **message-only** screen (`waitForInput: true`, no fields), which declares no + keys and so constrains none — the same pass-through `enforceActionParams` + gives a param-less action; + - `signal.output`, the node-OUTPUT namespace, which belongs to the approval-style + resume envelope rather than to the screen's collected-values channel. + +- Updated dependencies [0800433] +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [257d97a] +- Updated dependencies [e6b1b69] +- Updated dependencies [328ccc5] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [63b33e6] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [ff17642] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [8aacf94] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [9b43ee2] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [63b33e6] +- Updated dependencies [8aacf94] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [83cf2d3] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [1ee48bc] +- Updated dependencies [705e5c8] +- Updated dependencies [f61edce] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [304423e] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [26bb053] +- Updated dependencies [04f1182] +- Updated dependencies [c03108c] +- Updated dependencies [d6bd5a1] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [24915d2] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/metadata-protocol@17.0.0-rc.2 + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/objectql@17.0.0-rc.2 + - @objectstack/plugin-security@17.0.0-rc.2 + - @objectstack/rest@17.0.0-rc.2 + - @objectstack/service-datasource@17.0.0-rc.2 + - @objectstack/driver-sql@17.0.0-rc.2 + - @objectstack/observability@17.0.0-rc.2 + - @objectstack/metadata@17.0.0-rc.2 + - @objectstack/driver-sqlite-wasm@17.0.0-rc.2 + - @objectstack/plugin-auth@17.0.0-rc.2 + - @objectstack/driver-memory@17.0.0-rc.2 + - @objectstack/metadata-core@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/formula@17.0.0-rc.2 + - @objectstack/service-cluster@17.0.0-rc.2 + - @objectstack/service-i18n@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Major Changes diff --git a/packages/runtime/package.json b/packages/runtime/package.json index ca97baf251..d28e7670f8 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/runtime", - "version": "17.0.0-rc.1", + "version": "17.0.0-rc.2", "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 bce8344d80..6426e80c00 100644 --- a/packages/sdui-parser/CHANGELOG.md +++ b/packages/sdui-parser/CHANGELOG.md @@ -1,5 +1,7 @@ # @objectstack/sdui-parser +## 17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/sdui-parser/package.json b/packages/sdui-parser/package.json index cd2fbee84f..1214cd1e85 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.1", + "version": "17.0.0-rc.2", "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 108edfecba..e594f92c60 100644 --- a/packages/services/service-analytics/CHANGELOG.md +++ b/packages/services/service-analytics/CHANGELOG.md @@ -1,5 +1,291 @@ # Changelog — @objectstack/service-analytics +## 17.0.0-rc.2 + +### Major Changes + +- 3c7bcc0: feat(spec)!: converge the 11 contracts-vs-domain dual-source type names (#4538) + + `packages/spec/src/contracts/` hand-wrote parameter/result interfaces whose + names collided with same-named zod-derived types in the domains — the #4411 + trap, tracked as 11 rows of `dual-source-exports.baseline.json`. Each name was + judged individually against a three-repo import-level scan (framework, cloud, + objectui): which declaration actually flows at runtime decides the direction. + All 11 rows are deleted from the baseline; no name below is exported twice + anymore. + + **Converged — `./contracts` now re-exports the domain zod type (same + declaration on both entries, imports keep compiling from either):** + + - `NotificationChannel` → `system/notification.zod`'s + `z.infer` (member sets were identical). + - `ValidationResult` → `kernel/plugin-validator.zod` (shapes were identical). + - `HealthStatus` → `kernel/startup-orchestrator.zod` (`details` narrows + `Record` → `Record`). + - `PluginStartupResult` → `kernel/startup-orchestrator.zod`. FROM `plugin: +Plugin` (live object) and `error?: Error` TO the serializable projection + (`plugin: { name, version? }`-passthrough, `error?: { name, message, +stack?, code? }`). Neither side had any consumer outside spec; the + zod-validatable shape wins. + - `StartupOptions` → `kernel/startup-orchestrator.zod` — the PARSED tier + (defaults applied). `IStartupOrchestrator.orchestrateStartup` now takes + `StartupOptionsInput` (the caller-authored all-optional tier, also + re-exported from `./contracts`). Fix for callers typed to the old + all-optional `StartupOptions`: rename to `StartupOptionsInput`. + - `JobExecution` → `system/job.zod`. The system schema's `duration` field is + RENAMED `durationMs` — that is what every job adapter produces and what the + `sys_job_run.duration_ms` column round-trips; the schema described records + nothing ever wrote. Fix: `duration` → `durationMs` when parsing + `JobExecutionSchema` payloads. + - `AnalyticsQuery` → `data/analytics.zod`. The domain schema aligned to the + contract's semantics first: `timezone` LOST its `.default('UTC')` — absence + is meaningful (the engine resolves org timezone, #1982/#2018; the + `/analytics` entry always refused to apply that default). The schema is now + transform-free, so `AnalyticsQuery` ≡ `AnalyticsQueryInput` (both kept + exported). Fix for code that relied on `.parse()` injecting `timezone: +'UTC'`: pass the timezone explicitly or resolve it via the engine chain + (`selection.timezone ?? context.timezone ?? 'UTC'`). + + **Renamed — two genuinely different concepts were sharing one name (both + flow at runtime):** + + - `./contracts` `DriverCapabilities` → **`AnalyticsDriverCapabilities`** + (`{ nativeSql, objectqlAggregate, inMemory }`, the analytics strategy-chain + execution-path probe). The `DriverCapabilities` name now belongs solely to + the data domain's driver feature-flag record (`DriverCapabilitiesSchema`, + what `IDataDriver.supports` declares). Fix: importers of the trio from + `@objectstack/spec/contracts` (or `@objectstack/service-analytics`, whose + re-export is renamed in lockstep) rename the import; importers who meant + the driver flags import `DriverCapabilities` from `@objectstack/spec/data`. + + **Removed — the domain-side declaration was dead (zero import-level consumers + in framework/cloud/objectui; the #4411 family's last survivors):** + + - `system` `MetadataExportOptionsSchema` / `MetadataExportOptions` and + `MetadataImportOptionsSchema` / `MetadataImportOptions` (the + `output`/`source`-directory bags). The names now have ONE declaration each: + the `IMetadataService.exportMetadata` / `importMetadata` parameter + interfaces on `./contracts` (`types`/`namespaces`/`format` and + `conflictResolution`/`validate`/`dryRun`), which `MetadataManager` + implements. No tombstone/D2 conversion, deliberately — these are runtime + option-bag types, not authorable metadata (same reasoning as #4458). + `@objectstack/metadata` re-exports the two names from `./contracts` now + (it previously re-exported the dead system-side shapes its own manager + did not accept). + - `system` `JobSchedule` (the `= Schedule` back-compat alias). The name's one + declaration is the `IJobService.schedule` boundary shape on `./contracts` + (plain-string cron `expression`); the authored metadata type keeps its real + name `Schedule`. Fix: `import type { JobSchedule } from +'@objectstack/spec/system'` → `Schedule` (authoring tier) or the + `./contracts` `JobSchedule` (service boundary), whichever you meant. + +### Minor Changes + +- 328ccc5: fix(security,analytics): scope /analytics/query to the caller's readable records, and refuse a measure over a missing field (#4467, #4437) + + Two defects on the analytics query path, both found by the v17 verification run + (#3909 / #4482), both reproduced against a live showcase server before the fix + and re-verified with the same requests after. + + ## #4467 — `/analytics/query` applied no record-level scoping + + `ISecurityService.getReadFilter` documents itself as "the same filter the engine + middleware AND-s into every find", and exists precisely for paths that bypass + that middleware — its own doc comment names the analytics raw-SQL path. But the + chain it mirrors is TWO sibling middlewares: plugin-security's RLS injection and + plugin-sharing's owner/share visibility filter (`buildSharingMiddleware` AND-s + `buildReadFilter` into `ast.where` for `find`/`findOne`/`count`/`aggregate`). + Only the RLS half was ever computed here, and analytics has no other source of + scope, so the OWD/share predicate simply never existed on that path. + + Live repro: `showcase_private_note` is `sharingModel: 'private'`; an admin owns + 5 notes, a member holds read shares on exactly 2 and no `viewAllRecords`. + `GET /data/showcase_private_note` correctly returned 2 for the member, while + `POST /analytics/query {measures:['count']}` returned 5 — and adding + `dimensions:['title']` returned all five titles, i.e. the VALUES of a column + that caller may not read, not merely a bad count. Any authenticated caller who + could reach `/analytics` could enumerate the field values of every row of any + object exposed as a cube, regardless of OWD, sharing rules, or RLS. + + `getReadFilter` now resolves plugin-sharing's `buildReadFilter` through the + late-bound `sharing` service and AND-composes it with the RLS filter — the same + composition the two middlewares reach by both writing into `ast.where`. It also + computes the ADR-0057 D1 `__readScope` depth that the security middleware + normally stashes on the context for plugin-sharing to widen its owner-match + with, using the same `getEffectiveScope` call the middleware makes: no + middleware runs on this path, and without it a caller granted `unit`/`org` read + depth would be silently narrowed to `own`. The sharing predicate is resolved for + every non-system caller AHEAD of the RLS stand-down branches, because those are + the RLS middleware's own early exits and none of them is a reason to drop a + sibling middleware's predicate; a sharing-resolution failure denies outright + rather than falling through to half a scope. + + **Why `minor` rather than `patch`.** This is an observable behaviour change on a + public read surface, in the narrowing direction: analytics results that a + principal could previously read they now cannot. Counts drop, `dimensions` + groupings lose rows, and any dashboard, report, or export built on + `/analytics/query` over an owner-private object will show smaller numbers for + non-superuser principals — correctly, but visibly. Deployments that had (however + unknowingly) come to depend on the unscoped totals will see them change on + upgrade, so this warrants more than a patch-level note even though it is a + security fix. No API signature changed: `ISecurityService.getReadFilter`'s + declaration is untouched — the implementation merely started honouring the + contract it already documented. + + ## #4437 — a measure naming a missing field 500'd with SQLITE_ERROR + + `inferMeasure('ghost_sum')` maps a suffix convention onto a field name and has + no way to know the field exists, so it built `SUM(ghost)`, the driver threw + `no such column`, and the caller got + `500 {"code":"SQLITE_ERROR","message":"Internal server error"}` — a driver error + class as the `error.code` for what is a plain typo, which ADR-0112 forbids. A + dotted spelling took the same path (`measures:['total.sum']` prefix-strips to + `sum` → `SUM(sum)` → 500). The DATA route has refused the identical mistake with + a `400 INVALID_FIELD` naming the field since #4315/#4254. + + `AnalyticsService.ensureCube` now validates each measure's resolved source field + against the backing object's field names before any SQL is built, and rejects + with the same envelope the data route produces (`400 INVALID_FIELD` carrying + `field`, `object`, `param`, `measure`) so one mistake has one shape across + `/data` and `/analytics`. The new `getObjectFieldNames` config hook reads the + same schema registry `isRegisteredObject` already consults and the data path's + own gate reads, so "which fields exist" has a single answer across both routes. + + The gate is tiered exactly like the #3867 cube-inference gate, deliberately + narrow: it applies only when the cube's `sql` is a bare object name (an authored + cube whose `sql` is a real SQL expression has no field list to check against), + only when the probe answers (no data engine, or an external datasource whose + columns are not mirrored locally, stands down), and only to measures whose + source is a bare column — `count(*)` has no source field, and a dotted + cross-object reference resolves through a join this layer cannot see, so both + pass through untouched. `id`/`created_at`/`updated_at` are admitted + unconditionally, matching the data path's `resolveQueryFields`: a gate stricter + than the engine it guards would reject queries that used to work. Validation + runs before the cube is registered, so a rejected query leaves no trace in the + registry — otherwise a retry would find a "registered" cube carrying the bogus + measure and sail straight into SQL. + + This half is `minor` for the same envelope reason: a request that used to return + 500 now returns 400 with a different `code`, which is a visible contract change + for any caller branching on the response. + +- 6117f7b: fix(spec,service-analytics): a percentage measure carries its SCALE, so a ratio of 1 is 100% (objectui#3136) + + A `%` format string says how to PRINT a number, not what scale that number is + on — and the two readings collide at exactly `1`, which is both "100%" (a 0–1 + ratio at full compliance) and "1%" (a single percentage point). With nothing on + the wire to tell them apart, renderers guessed from the value's magnitude and + resolved the collision the wrong way: an SLA / pass-rate dashboard reporting + `sla_rate = 1` displayed **"1.0%"** — "everything met the SLA" read as "1% met + the SLA" — on both the KPI card and the dataset table. + + The scale was never actually unknowable; it just never left the server. A + measure declaring `derived: { op: 'ratio' }` is a 0–1 fraction _by definition_, + and a measure aggregating a `percent` field has whatever scale that field + stores. Both facts sit in metadata the enrichment pass already reads for the + ADR-0053 currency chain — which walks back to the source field, checks + `type === 'currency'`, and rides the resolved code onto the result column. + Percentages got no such treatment. They do now, through the same seam. + + **`percentScaleOf(field)` (`@objectstack/spec/data`)** is the one place the + question is answered. A `percent` field stores a FRACTION unless it declares + `max > 1` (e.g. `min: 0, max: 100`), which marks whole-percent storage — the + same rule the percent edit widget already writes by, so a value round-trips. + Non-`percent` fields get no opinion: a plain `number` an author formatted with + a `%` keeps meaning exactly what their format string says. + + **`AnalyticsResult.fields[].percentScale`** carries the answer: `'fraction'` + (`1` ⇒ "100%") or `'whole'` (`1` ⇒ "1%"), absent when the column is not a + percentage. `queryDataset` sets it from the measure's `derived.op === 'ratio'` + first, then the source field's scale. `currency` — emitted since ADR-0053 but + only ever written through a cast — is now declared on the same interface. + + The config seam `measureCurrency` is renamed **`sourceFieldMeta`** and returns + `max` alongside `type`/`defaultCurrency`. The old name had already outgrown + itself: the date-bucketing path reads `type` through it to tell a `date` + dimension from a `datetime` one, and the percent chain is its third consumer. + + Renderers that receive `percentScale` must scale by it rather than inferring + from the value; one that does not receive it (an older server) keeps whatever + fallback it has, so this is additive on the wire. + + **Same widget family, second fix: an empty filtered group is a measured zero.** + A measure-scoped filter can exclude every row of a group the grid still lists, + and the database reports that by omitting the group from the supplementary + result — after the merge, indistinguishable from "not measured". For a COUNT or + a SUM it _is_ measured: the answer is 0. `emptyGroupValueFor(aggregate)` + (`spec/data/aggregation-policy`) states which aggregates have an identity over + the empty set, and `queryDataset` fills it in once all supplementary merges are + done (a later measure's merge can append rows no earlier query saw). So + "0 of 12 paid" now reports `0` instead of blank, and a ratio built on it + computes to `0` instead of going null — the difference between a dashboard + saying "0% met the SLA" and saying nothing at all. `avg`/`min`/`max` keep their + null: there is nothing to average over an empty group, and flattening that to + zero would invent a measurement. + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/services/service-analytics/package.json b/packages/services/service-analytics/package.json index 52dd1c27af..a96e05e43f 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.1", + "version": "17.0.0-rc.2", "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 f62beafad4..b5d1e14cf9 100644 --- a/packages/services/service-automation/CHANGELOG.md +++ b/packages/services/service-automation/CHANGELOG.md @@ -1,5 +1,624 @@ # @objectstack/service-automation +## 17.0.0-rc.2 + +### Minor Changes + +- 2826d1e: fix(automation,approvals): an approval decision can no longer succeed while its flow stays parked (#4420) + + A flow paused at an `approval` node, a deploy, then an approver clicking + Approve: the request row flipped to `approved`, the UI toasted success — and + the flow never moved. No next-stage request, no error, the record's mirrored + status frozen mid-workflow. Approval flows pause for days by design, so a + restart mid-flight is the normal case: every release could quietly zombify + every in-flight approval, with the approvers none the wiser. + + Durable suspended runs (#1518) had shipped and were not the missing piece. Two + other things were. + + **The wiring could enable a store over a table nobody had created.** Object + registration and store activation resolve different services in different + phases — `manifest` at `init()`, `objectql` at `start()` — and the plugin + declared no ordering. Composed ahead of ObjectQL, `init()` found no `manifest`, + warned, and continued; `start()` then attached the DB-backed store anyway. Every + suspend failed with `no such table: sys_automation_run` into a log line nobody + read, pauses silently stayed in memory, and the next restart lost them all. + Now: `AutomationServicePlugin` declares `optionalDependencies: +['com.objectstack.engine.objectql']` (order-if-present, per ADR-0116 — an + engine-less kernel must still boot); a registration missed at `init()` is + retried at `start()`, which still lands before ObjectQL's schema sync; the + store is never attached when registration did not happen, and says so at + **error** level instead of warning; the table is probed once at boot so a + broken setup surfaces there rather than one failed write at a time; and a + failed durable write of a paused run is logged at error — it is data loss in + waiting, not a warning. + + **A reported resume failure read as success.** `AutomationEngine.resume()` + answers a lost run by _returning_ `{ success: false }`, never by throwing. + `ApprovalService` discarded that return value, and `decide()` counted only a + thrown error as failure — so a decision against a dead run came back + `resumed: true`, HTTP 200. Resume failures are now classified + (`RUN_NOT_FOUND`, `STORE_UNAVAILABLE`, `RESUME_IN_PROGRESS`, joining + `PERMISSION_DENIED` / `INVALID_SIGNAL`), so a run that is gone for good is + distinguishable from a store that is merely unreachable, and the raw resume + route maps them to 404 / 503 / 409. + + Approvals acts on them. A new `AutomationEngine.hasSuspendedRun(runId)` — which + reads the suspension store, unlike `getRun()`, and throws rather than answering + `false` when the store is unreadable — pre-flights every flow-advancing + operation (`decide`, `sendBack`, `resubmit`) **before its first write**, so the + zombie half-state is never created rather than merely reported: the decision + fails with `RESUME_TARGET_LOST` (HTTP 409) and the request stays actionable. A + resume that fails after the decision is durable can no longer be undone, but it + now throws `RESUME_FAILED` (HTTP 500) naming the stranded run instead of + reporting success. A concurrent duplicate resume stays benign — the engine's + idempotency guard is doing its job — and reports through the new optional + `resumeError` field. Recall and revise-window cancellation stay non-fatal by + design (they abandon the request), but log at error with the reason instead of + swallowing it. Compositions with no automation engine attached are unaffected. + + Existing zombie requests from affected deployments (already `approved`, run + stranded) are not repaired by this change — `releaseDeadRunRequests` only + sweeps requests that are still `pending`. + +- 5293114: fix(automation): a decision's three declared ways to route a branch are now one working model (#4414) + + A `decision` node advertised three mechanisms for splitting a path and only one + of them did anything. The other two were the ADR-0049 `declared ≠ enforced` + shape, and the pair of them shipped a guard that does not guard in + `examples/app-crm`. + + | mechanism | before | now | + | :--------------------------------------------------- | :----------------------------------------------------------------------------------------------------- | :---------------------------------------------------- | + | `edge.condition` | ✅ the only one that worked | unchanged | + | `edge.isDefault` | **zero readers** anywhere but the schema declaration | BPMN default flow, enforced in `traverseNext` | + | `decision.config.conditions[].label` → `branchLabel` | matched **0** out-edge labels across every example app, then fell back to the full edge set in silence | routes; an unclaimable label is logged, not swallowed | + + ## What was broken, end to end + + `crm_convert_lead_wizard` means "already converted → abort screen; otherwise → + the wizard". It ran **both**: an already-converted lead got + "This lead has already been converted" and then walked straight into the + conversion wizard behind it. Four independent silences stacked up: + + 1. the decision's first condition was authored `{lead_record.status} == +'converted'` — braces in a slot declared bare CEL, so it was string-compared + and never true; + 2. the second (`'true'`) therefore won, yielding `branchLabel: 'No — proceed'`; + 3. no out-edge carried that label (they were `'Yes'` / `'No'`), so traversal + discarded the branch and considered every out-edge; + 4. `e3b` was unconditional, so it ran regardless — and the natural fix, marking + it `isDefault: true`, was a dead key. + + ## The model + + `branchLabel` narrows the edge set → `condition` gates each edge → `isDefault` + catches whatever is left. Concretely: + + - **`isDefault` is enforced.** A default edge is traversed only when no + conditional sibling of the same source node matched, and it is no longer part + of the unconditional parallel fan-out — that distinction is the whole point of + the marker. Passed over because a real branch won, its target records the same + `skipped` step a closed gate does (#4354). + - **An unclaimable branch label warns.** Traversal still falls back to the full + edge set (a run mid-flight must not die on a metadata error) but says so, + naming the computed branch and the out-edge labels that exist. + - **A decision that declares no `conditions` reports no branch.** It used to + report `'default'` unconditionally — a label no out-edge in the repo ever + carried — which is why every decision node fell back to the full edge set. + The `'default'` sentinel survives for the case it actually describes (declared + conditions, none matched) and is now claimed by the `isDefault` edge as well + as by an edge literally labelled `'default'`. + - **`conditions[].expression` is evaluated as the bare CEL it is declared to + be.** The raw string went to the legacy `{var}` template path, where + `lead.status == 'converted'` cannot resolve and the branch is decided by + string comparison. Unlike `edge.condition` this slot carries no + `ExpressionInput` envelope — the decision descriptor is deliberately + schemaless — so the executor supplies the dialect. A brace-in-CEL predicate + now fails loudly (ADR-0032 §1c) instead of deciding `false`. + + ## Caught at authoring time too + + Four new `os build` / `os validate` warnings, because a wrong route is silent at + run time by nature (Prime Directive #12): + + `flow-branch-label-unmatched` (the shipped shape), + `flow-decision-unconditional-branch` (a guarded decision with an unconditional + sibling — the actual hole), `flow-default-edge-with-condition` and + `flow-multiple-default-edges`. + + Both of the first two fire on the pre-fix `convert-lead.flow.ts` and are silent + after it. + + ## Effect on flows that already exist + + Enforcing `isDefault` changes how a **stored** flow behaves, and the flows it + changes are mostly Studio's own. `objectui`'s flow edge inspector has always + written `isDefault: true` when you bind an out-edge to a decision's default/else + branch — into a key with zero readers, so that edge ran unconditionally, in + parallel with whichever branch actually matched. Those flows now take exactly + one branch. That is the fix, but it is a behaviour change on existing data + rather than only on newly authored metadata, so it is worth knowing before + upgrading: a flow that quietly ran two paths will now run one. + + Nothing changes for an edge that never carried the marker — `isDefault` defaults + to `false`, and an ordinary unconditional out-edge still fans out in parallel + exactly as before. + + ## The example app + + `crm_convert_lead_wizard`'s guard is now a plain exclusive gateway: the + redundant `config.conditions` is gone and `e3b` carries `isDefault: true`. One + mechanism per decision, and exactly one branch runs. + + Verified: 11 new engine/executor tests (including the reported repro in both + directions), 12 new linter tests; `@objectstack/service-automation` 577 tests + and `@objectstack/cli` 652 tests green, all three example apps build with no new + findings. + +- ac471a0: **BREAKING**: `IAutomationService.getSuspendedScreen(runId)` is now **async** — it returns `Promise` instead of `ScreenSpec | null` (#4515). + + FROM → TO for anyone calling or implementing it: + + ```ts + // caller + - const screen = automationService.getSuspendedScreen(runId); + + const screen = await automationService.getSuspendedScreen(runId); + + // implementer + - getSuspendedScreen(runId: string): ScreenSpec | null + + async getSuspendedScreen(runId: string): Promise + ``` + + One-line fix: `await` the call (the enclosing function is almost certainly already `async`), and make any test double resolve rather than return (`mockResolvedValue`, not `mockReturnValue`). + + Why it had to change: the method could only ever read the engine's in-memory hot cache, because a synchronous signature cannot consult the durable suspended-run store. `SuspendedRun.screen` _is_ persisted (`sys_automation_run.screen_json`) and `resume()` cold-reads it back, so after a process restart a still-suspended screen run could be resumed (`POST …/runs/:runId/resume` → 200) while `GET …/runs/:runId/screen` returned 404 “No pending screen for run” — the refresh-safe re-fetch failing in exactly the situation it exists for (page refresh, another device), and the rendering half of ADR-0019's durable-suspend promise missing while the resuming half shipped. + + `AutomationEngine.getSuspendedScreen` now takes the hot cache as its fast path and falls through to the store via the same loader `resume()` rehydrates from. A run that does not exist, is no longer suspended, or paused at a non-screen node still resolves to `null`, so `GET …/runs/:runId/screen` keeps returning 404 for genuinely absent runs. No sync variant of the method remains on the contract. + +- 68c02c2: fix(automation): `evaluateCondition` decides the dialect from the source, not from the caller (#4336) + + `AutomationEngine.evaluateCondition` picked its engine by asking whether an + `{ dialect, source }` **envelope** was present. A condition handed to it as a + plain string therefore never reached the CEL engine: it fell through to the + legacy `{var}` template path, which substitutes brace holes and then compares + whatever text is left — **as text**. Nothing errored, and the run was recorded + as `success`, with the failure direction depending on the predicate: + + | Handed in | Actually evaluated | Result | + | :--------------------- | :------------------------------------- | :----------------------------------- | + | `existingTask == null` | `'existingTask' === 'null'` | always **false** — gate never opens | + | `record.rating >= 4` | `'record.rating' >= '4'` → `'r' > '4'` | always **true** — branch pinned open | + + #4414 fixed the one built-in that was reaching this — the `decision` executor + now wraps `conditions[].expression` in a CEL envelope before calling. This + fixes the **evaluator**, so the next caller does not have to remember: the + dialect is now read from the source, and a condition is CEL unless it actually + contains a `{var}` hole. `evaluateCondition` is public API, so a + plugin-registered node executor evaluating its own predicate was getting the + table above with nothing to warn it. + + **The legacy `{var}` dialect keeps working** where it always did — + `{amount} > 100`, `{status} == active`, `{a.b} == 7` — and gains the two things + it was missing: + + - **A quoted literal compares as its contents.** `{status} == 'active'` used to + compare `active` against `'active'` — quotes included — and was false for + every value of `status`. It is the spelling the flow docs showed, and quoting + a string literal is what every other predicate surface requires. + - **It no longer answers `false` when it could not resolve something.** A `{…}` + hole matching no flow variable (`{lead_record.status}` — `get_record` stores + the whole row under one name, so that key never exists) and a substituted + value that is neither a boolean, a number, nor part of a comparison are + refused with the source and the offending reference attached. Both used to be + a silent `false`, which ADR-0032 §1c forbids: a predicate that cannot be + evaluated is a fault, never a quiet branch decision. + + Braces inside an explicit `dialect: 'cel'` envelope remain the #1491 brace-trap + and still throw — stating the dialect is the author saying "this is CEL". The + sniff reads the source outside string literals, so `record.label == '{pending}'` + stays CEL and compares the field. + + **Tightening to know about:** a bare string that is not valid CEL now raises + where it previously string-compared to some answer. That includes the + host-language payloads the safety tests use (`process.exit(1)`, + `require("fs")…`) — nothing executed before and nothing executes now, since CEL + has no `process`, no `require` and no arrow functions, but the failure is a + reported fault instead of a silent `false`. + +- eb4204b: feat(automation): a `script` node's purity contract is declared, and a function that writes can say so (#4396) + + The `script` executor's contract — _the named function returns a value; data I/O + stays on the flow graph_ — existed only as a comment inside the executor, while + #4354's run summary depended on it. That summary reports no record metrics for a + `script` step precisely because a pure function's writes are downstream + `create_record` / `update_record` nodes counting themselves. A function that + wrote anyway made its run report `selected: 30, acted: 0` — indistinguishable + from the broken sweep the counters exist to detect, recorded permanently on + `sys_automation_run`. + + **The rule is now visible.** `ActionDescriptor` carries + `handlerContract: 'none' | 'pure'`, and the `script` descriptor publishes + `'pure'`, so the action catalog, the designer palette and the reference docs + state the rule an author has to follow instead of an executor holding it + privately. + + **And a legitimate writer can opt out honestly.** A `defineStack({ functions })` + entry may declare what it does, in either shape: + + ```ts + defineStack({ + functions: { + scoreLead: (ctx) => ({ score: 42 }), // pure — the default + syncBilling: { handler: syncBilling, effect: "writes" }, // declared writer + }, + }); + ``` + + A step calling a declared writer reports `unmeasuredEffect`, so the run's + `unmeasured` tally keeps the broken-sweep query + (`selected > 0 AND acted = 0 AND unmeasured = 0`) off that flow — and only that + flow. Marking _every_ `script` step unmeasured was rejected: it would blind the + detector on every flow that calls any function in order to cover the few that + break the rule. + + Nothing here is retired or renamed: a bare `functions: { fn }` entry is + unchanged and means `effect: 'pure'`. The declaration is carried end to end — + `ObjectQL.registerFunction` accepts `{ packageId, effect }` alongside the + existing `packageId` string and exposes `resolveFunctionEntry(name)`, + `objectstack build` lowers a declared entry without dropping it, and the + artifact loader re-attaches the module's callable to the declaration the JSON + carried. + + **Also fixed:** `bindHooksToEngine` returned before registering a bundle's + functions when the stack declared no hooks, so a flow-only app's + `defineStack({ functions })` reached the engine as nothing and every `script` + node calling one failed with "no function named 'x' is registered". + +- fd3013a: feat(spec,automation)!: converge `script` to a function call — retire the `actionType` branches — and parse `script` / `subflow` config at execute time (#4343) + + A `script` node had four ways to name what it ran and only one of them ran anything. + Protocol 17 keeps that one and retires the rest. + + - **`config.actionType: 'email' | 'slack'`** were **logger-backed stubs**. They wrote a + line, reported success, and delivered nothing — under any configuration, installed + messaging service or not. Every bundled example used one; none of them ever sent + anything. + - **`config.template` / `.recipients` / `.variables`** fed those stubs, so they addressed + a message no channel sent. (The examples did not even reach them: they passed the + payload in `inputs`, which the built-in branch never read.) + - **inline `config.script`** was recognized and **never executed** — the built-in runtime + has no server-side JS sandbox, so the node warned and completed as a no-op. + - **any other `actionType`** was shorthand for a registered-function name — a second + spelling of `config.function` — and `'invoke_function'` was a marker that named nothing + on its own. + + What remains is what worked: `config.function` (now **required**) names a registered + function, `config.inputs` feeds it, `config.outputVariable` binds its return value. + + **The replacements are three different mechanisms, not one rename.** + + | Retired | Use instead | + | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | + | `actionType: 'email'` (+ `template` / `recipients` / `variables`) | a `notify` node — it delivers through the messaging service: the in-app inbox by default, real email once `@objectstack/plugin-email` is installed | + | `actionType: 'slack'` | a `connector_action` node with the Slack connector, or an `http` node posting to an incoming webhook — `notify` has no Slack channel | + | `actionType: 'my_fn'` (shorthand) | `function: 'my_fn'` — the conversion moves it for you | + | `script: '…'` (inline JS) | move the logic into a registered function and call it via `config.function` | + + **Execute-time parse.** `script` and `subflow` now run their config through the contract + before executing, the seam #4277 gave the flat builtins — a violation refuses the node as + a **guard** (wrong metadata; no `fault` edge may route it, #3863). `script` could not join + that seam while its legal key set depended on `actionType`: a flat parse would either + reject valid shapes or wave everything through. Converging the node is what made the + contract fit. `subflow`'s hand-written `flowName` check became the same parse, so its + message is now `subflow 'n1': config does not satisfy the subflow contract — +config.flowName: …`. `decision` deliberately stays export-only: its one key is optional, + so a parse would check nothing. + + **Migration.** `os migrate meta --from 16` rewrites stored sources; authoring one of these + keys in TypeScript is a compile error carrying the same prescription. A shorthand + `actionType` **converts into `function`** — that is what it named — unless `function` is + already set, in which case it was dead metadata the executor never reached. The other four + keys are dropped outright: nothing read them, so there is no value to preserve, and + rebuilding the intent is an authoring decision (the table above) rather than something a + mechanical rewrite can guess. + + The keys leave the **load path** (`retiredFromLoadPath`) with the rest of the keys retired + for _misdescribing themselves_ rather than for being renamed: absorbing + `actionType: 'email'` silently would let an author keep believing the flow sends mail. The + one seam that still replays it is `registerFlow`, which rehydrates data at rest (#3903) — + a row in `sys_metadata` has no author for a tombstone to teach. So a stored email-stub node + arrives stripped of the keys nothing read and then **refuses for naming no callable**, + where it used to log a line and report success. That flip is the behavior change to expect. + + **A build gap this surfaced, fixed here.** `FlowFunctionEntrySchema` now also accepts a + **lowered handler ref** (a non-empty string), the form `objectstack build` produces: the + CLI lowers every inline callable to a serialisable ref _before_ the stack is parsed (it + must — `z.function()` wraps callables and would break the ref mapping), so a built + manifest holds `{ myFn: 'myFn' }`, which neither previous member accepted. The result was + that `defineStack({ functions })` — a documented, first-class mechanism — could not + survive a build at all. Nothing had noticed because no bundled example used it; #4343 + turns that from latent into blocking, since `config.function` becomes the only thing a + `script` node can run. `Hook.handler` already declared exactly this pair (`z.union([ +z.string(), ])`, "string, post-build / inline function, pre-build"), so this + brings `functions` onto the platform's established shape rather than inventing one. A + string carries no callable and `normalizeFlowFunctionEntry` still drops it by design — the + real functions ride in the sibling ESM module the build emits, merged by name — so + hand-authoring one registers nothing and fails loudly at execute ("no function named '…' + is registered"), never silently. + + Also in this change: the retired constants `SCRIPT_BUILTIN_ACTION_TYPES`, + `SCRIPT_INVOKE_FUNCTION_ACTION_TYPE` and the `ScriptBuiltinActionType` type are removed + (they described the dispatch set that no longer exists); `os validate` names a retired key + and its replacement instead of reporting a generic missing callable; and the `#3796` + alias fixture, which carried `actionType: 'invoke_function'` through both sides, no longer + describes an end state protocol 17 can reach — the rename itself is untouched. No liveness + ledger row moves: the gate walks `FlowSchema`, whose `nodes[].config` is + `z.record(z.unknown())`, so these keys were never governed by one. + +- 304423e: feat(automation,migrate): `os migrate meta --stored` now covers flow rows too (#4454) + + #4327 gave the stored-metadata conversion chain a finish line for every + metadata type except `flow` — the one type where the most stored dialect + actually lives, since the graduated conversions `flow-node-crud-filter-alias`, + `flow-node-crud-object-alias`, `flow-node-notify-config-aliases` and + `flow-node-script-config-aliases` are all flow-node entries. Flow-node + conversions carry ADR-0078's open-namespace conflict guard, which has to consult + the _live_ executor registry to tell a rename from a clobber, and the metadata + layer has no way to obtain one. Flows were reported `skipped` with that reason. + They are now converted. + + **One canonicalization policy, two shapes.** + `AutomationEngine.canonicalizeStoredFlow` is the single implementation and + `registerFlow` calls it, so the load seam and the migration can never disagree + about what "canonical" means. It returns `parsed` (for execution — the + `FlowSchema.parse` + #4347 region output, schema defaults materialized) and + `storable` (for persistence). + + **`storable` excludes schema defaults, and that is the load-bearing decision.** + Measured rather than assumed: driving a pre-17 flow through all three steps + _removes_ nothing — `FlowSchema` is strict since #4001, so an unrecognized key + throws instead of being silently dropped, which means the + `graftNormalizedOperators` precedent (it exists because the _view_ parse strips + Studio-only auxiliary keys) does not transfer — and _adds_ only defaults: + `version`, `runAs`, per-edge `type` / `isDefault`. Persisting a default the + author never wrote would pin every migrated row to today's value while untouched + rows follow tomorrow's: two populations with different behaviour, which is + exactly the drift this pass exists to remove. So the write-back is the + conversion result plus the `{dialect, source}` envelopes the schema derives for + edge conditions, and nothing else. + + One subtlety worth knowing if you extend this: that envelope is a schema + transform, not a conversion, so it emits **no** notice while still changing the + body. Reading notices alone — correct for every other metadata type — would call + such a row canonical and leave it re-deriving on every boot. Both passes are + copy-on-write, so identity is the exact test for flows. + + **New: `AutomationServicePluginOptions.armRuntime`** (default `true`, so every + server, dev stack and test host is unaffected). Set `false` and the plugin + brings up the engine and the complete node registry — built-ins plus whatever + `automation:ready` contributes, because a _partial_ registry would make the + conflict guard read a live custom node type as unowned and rewrite over it — and + then stops before anything is armed: + + | Skipped when `armRuntime: false` | Why it must be | + | -------------------------------------------------------- | --------------------------------------------------------------------------------------------- | + | flow pull + `kernel:ready` / `metadata:reloaded` re-sync | `registerFlow` calls `activateFlowTrigger` — record triggers and scheduled jobs would go live | + | declarative connector materialization | opens real connections; an MCP provider spawns a child process | + | suspended-run wait-timer re-arm | would resume someone's paused approval mid-migration | + + `os migrate meta --stored` boots the plugin in that mode. A migration process + must not become a second server. + + A refused rename — the guard firing because the old node-type token is a live + name something else owns in this environment — fails that row loudly, naming the + token and its owner. Never a silent skip, never a clobber. A flow that cannot + canonicalize at all (a strict-schema violation, a malformed control-flow region) + is reported as failed with the parse message rather than persisted as a guess; + such a row cannot register today either, so the report is telling you about a + flow that is already broken at runtime. + +### Patch Changes + +- 5b843fb: fix(automation,spec): the cold-boot flow bind must survive the read path's own annotations (cloud#971) + + `getMetaItems({ type: 'flow' })` decorates every served item with + `_diagnostics` (and `_draft` on a preview read). The cold-boot bind fed that + served document straight into `engine.registerFlow` → `FlowSchema.parse`, and + since #4001 closed the metadata schemas an unrecognized key **throws** instead + of being dropped — so every flow failed to register on every boot with + `unrecognized_keys: ["_diagnostics"]`. Not fatal only by luck: the + record-change plugin binds record flows a second way, so automations kept + firing behind one WARN per flow. A flow whose only binding path is this one + would have gone silently dead. + + Fixed at the read seam (`readFlowDefsFromProtocol`), not by loosening + `FlowSchema`: the payload is malformed because we decorated it, so the + producer's annotation is the producer's to remove. + + `@objectstack/spec` gains `METADATA_READ_DECORATIONS` / `stripReadDecorations` + (`kernel/metadata-read-decorations`) — the list moves out of + `metadata-protocol`, where it was module-private, so the producer and its + cross-layer consumers share one definition. `metadata-protocol` re-exports + `stripReadDecorations` unchanged; no public surface is removed. + +- f3141d8: fix(spec): a node that publishes no descriptor configSchema can now own an expression-ledger entry (#4439) + + `FLOW_NODE_EXPRESSION_PATHS` is the #4027 ledger that tells `registerFlow` and + `objectstack validate` which config keys hold expressions, and in which dialect. + Its ratchet (`config-expression-ledger.test.ts`) derives what it expects from + descriptor `configSchema` `xExpression` markers, and fails in **both** + directions — an undeclared marker, or a ledger entry nothing declares. + + `decision` / `script` / `subflow` publish **no** descriptor `configSchema` on + purpose: a published partial schema would drop the editors their hand-written + Studio forms need (the #4210 incident), so their contract lives in + `schemaless-node-config.zod.ts`. Those two rules compose into a hole — an + expression slot on a schemaless node is structurally unreachable by the ratchet, + and because the reverse direction rejects unclaimed entries, it cannot be + entered by hand either. + + `decision.conditions[].expression` sat in that hole. Its own schema says + _"Bare CEL predicate deciding this branch"_ and its own comment names `{…}` as + the #1491 trap, and no validator walked it — so `{lead_record.status} == +'converted'` passed `tsc`, passed `objectstack validate`, passed registration. + #4414 made that fail loudly at run time; this makes it fail at build time, + which is the delay #4027 exists to remove. + + ## The fix + + The ratchet now reads **both** declaration channels: + + - **descriptor `configSchema`** — unchanged, enumerated from the live registry; + - **`schemaless-node-config.zod.ts`** — the marker rides + `.meta({ xExpression })` through `z.toJSONSchema`, the same channel + `loop.collection` has used since objectui#2670. + + Spec hands the second channel over as JSON Schema + (`getSchemalessNodeConfigJsonSchemas()`, memoized, `input` mode — the shape a + descriptor's `configSchema` already is), so the ratchet walks both with the + _same_ function. No second notion of "a declared expression property", which is + the duplication a ledger exists to remove, and no `zod` dependency added to + `service-automation`. Each channel is separately asserted non-empty, so a broken + derivation on one side cannot hide behind the other's results. + + `SCHEMALESS_NODE_CONFIG_SCHEMAS` is also exported for anything else that needs + to reason about all node config contracts. Additive — objectui's + `flow-node-config` reconciliation imports each schema by name and is unaffected. + + ## The sweep + + The other schemaless slots were checked and deliberately carry no marker: + `script.template` is a template **id**, not a body; `script.inputs` / + `script.variables` / `subflow.input` are values that interpolate `{token}` — + text-with-holes, the shape essentially every node config string has, already + covered generically by `validate-flow-template-paths` and the CLI flow linter. + A `flow-template` ledger entry means something narrower: a _reference that must + resolve to a value_, like `loop.collection`. So `decision.conditions[] +.expression` is the only genuinely declared expression slot on the class — now + recorded in the ledger's header so it is not re-derived. + + ## Docs corrected + + The flows guide taught the **wrong dialect** for decision predicates in three + places (`'{order_amount} > 10000'`), plus a "braces missing in a decision + expression" warning that inverted after #4414 — and `FlowNodeSchema`'s own + `@example` did the same. All corrected to bare CEL, with the history stated so + an author with a braced predicate knows what changed and why their build now + fails. The dialect table drops from three dialects to two: predicates never take + braces, values always do. + + Verified: 13 new/updated tests across the ratchet, the engine's registration + pass and `@objectstack/lint` (including the exact app-crm predicate rejected at + both `registerFlow` and `objectstack validate`); `pnpm build`, `pnpm typecheck` + (122 tasks), `pnpm lint` and `check:docs` clean. + +- 5a84d41: fix(automation): `resume` enforces the suspended screen's declared field contract (#4477) + + A `screen` node's `config.fields` is a complete input contract — the author + declares the keys, their `required`-ness, and (via `visibleWhen`) when a field + is even asked for. The RENDER half honoured all of it: the paused result and + `GET …/runs/:runId/screen` carry `required` and `visibleWhen` intact. There was + no VALIDATION half — `POST …/runs/:runId/resume` folded whatever bag it was + handed straight into the flow variables, so a caller that skipped the dialog and + posted here directly was unconstrained by every `required` the author wrote. + Missing required fields, and keys the screen never declared, all completed the + run with `success: true`. + + Screen flows are the one place where the declared field contract is the ONLY + contract — no object schema sits behind a screen node to catch a bad bag + downstream. The platform already enforces the analogous contract everywhere else + this seam appears: action params (ADR-0104 D2), record writes (ADR-0113), + approval `decisionOutputs` (#3447). This is that rule for screen resume, built in + the same shape. + + `resume` now refuses a non-conforming submission with the new + `AutomationResult.code` `'INVALID_SCREEN_INPUT'` (a transport maps it to **400**, + as the automation domain route now does) and an `Invalid screen input: …` message + that names each violation and lists the declared field names. The refusal happens + BEFORE the suspension is consumed, so the pause stays live and the legitimate + submission still lands. + + `visibleWhen` is evaluated against the SUBMITTED values first (layered over the + run's variable snapshot), so a hidden field's `required` never fires — enforcing + it would dead-end the run at a field the user was never shown, which is #3528 + reproduced server-side. A predicate that cannot be evaluated is logged and + treated as hidden rather than visible: the client decides what the user saw, and + a broken predicate is not evidence a field was on screen. + + Scope, deliberately narrow — three shapes keep the historical pass-through: + + - an **object-form** screen (`kind: 'object-form'`), whose `fields` is empty by + construction because the client renders the object's own form and the write + path enforces that object's `required` fields itself; + - a **message-only** screen (`waitForInput: true`, no fields), which declares no + keys and so constrains none — the same pass-through `enforceActionParams` + gives a param-less action; + - `signal.output`, the node-OUTPUT namespace, which belongs to the approval-style + resume envelope rather than to the screen's collected-values channel. + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/formula@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Major Changes diff --git a/packages/services/service-automation/package.json b/packages/services/service-automation/package.json index c18ad82db3..eca35a5771 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.1", + "version": "17.0.0-rc.2", "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 483918781f..7295afce99 100644 --- a/packages/services/service-cache/CHANGELOG.md +++ b/packages/services/service-cache/CHANGELOG.md @@ -1,5 +1,72 @@ # @objectstack/service-cache +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [ff17642] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/observability@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/services/service-cache/package.json b/packages/services/service-cache/package.json index 3a64662484..711270ba39 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.1", + "version": "17.0.0-rc.2", "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 681f8f3306..b085aa3634 100644 --- a/packages/services/service-cluster-redis/CHANGELOG.md +++ b/packages/services/service-cluster-redis/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/service-cluster-redis +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/service-cluster@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/services/service-cluster-redis/package.json b/packages/services/service-cluster-redis/package.json index 5f62e240af..76b5fa8e90 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.1", + "version": "17.0.0-rc.2", "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 d5464e4b42..0aa821e0b4 100644 --- a/packages/services/service-cluster/CHANGELOG.md +++ b/packages/services/service-cluster/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/service-cluster +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/services/service-cluster/package.json b/packages/services/service-cluster/package.json index cc1c3499ba..70323157fe 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.1", + "version": "17.0.0-rc.2", "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 266de1b7ed..01e662084d 100644 --- a/packages/services/service-datasource/CHANGELOG.md +++ b/packages/services/service-datasource/CHANGELOG.md @@ -1,5 +1,188 @@ # @objectstack/service-external-datasource +## 17.0.0-rc.2 + +### Minor Changes + +- cdf4d9a: `datasource.config` is now validated against its driver's contract (#4410) + + `config` was the one authorable slot on a datasource with no gate at all. The + schema's own comment claimed "the driver's own `configSchema` is what validates + it" — nothing did: both bundled driver specs set `configSchema: {}`, no code read + the field, and the per-driver zod schemas were not even exported from the + package. So `config: { hostname: 'db.internal' }` (the key is `host`) was + accepted in silence and the datasource connected to `localhost` while the parse, + the save and the connection probe all reported success. + + `DatasourceSchema` now parses `config` against + the contract for the declared driver, and `DatasourceAdminService` + (create/update/test, the Setup wizard's path) applies the same check. Both read + one registry in `@objectstack/spec/data`, which also projects each contract to + JSON Schema for `DriverDefinitionSchema.configSchema` and the Studio connection + form, so the form offers exactly the fields the validator accepts. + + New exports from `@objectstack/spec/data`: `PostgresConfigSchema`, + `MysqlConfigSchema`, `SqliteConfigSchema`, `SqliteWasmConfigSchema`, + `MongoConfigSchema`, `MemoryConfigSchema`, plus `resolveDriverId`, + `getDriverConfigSchema`, `getDriverConfigJsonSchemaById` and + `validateDriverConfig`. A driver the platform ships no contract for (a plugin's + `com.vendor.snowflake`) keeps an unvalidated `config`. + + **Migration.** A config that was silently ignored now fails with the correction + in the message. The renames: + + | Wrote | Write instead | Driver | + | ---------------------------- | ------------- | ---------------------- | + | `user` | `username` | postgres, mysql, mongo | + | `connectionString` / `dsn` | `url` | postgres, mysql, mongo | + | `uri` | `url` | mongo | + | `file` / `path` / `database` | `filename` | sqlite, sqlite-wasm | + | `hostname` | `host` | postgres, mysql, mongo | + | `searchPath` | `schema` | postgres | + + And the relocations — keys that were never driver config: + + | Wrote in `config` | Write instead | + | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | `min` / `max` / `idleTimeoutMillis` / `connectionTimeoutMillis` | the datasource's own `pool` block | + | `schemaMode` | next to `driver`, on the datasource | + | `readOnly` | `external: { allowWrites: false }` — the enforced write gate. (This row said `capabilities: { readOnly: true }` until #4487's liveness audit found that key has no reader.) | + | `ssl: { ca, cert, key, rejectUnauthorized }` | the datasource's own `ssl` block — inside `config`, `ssl` is the on/off boolean shorthand | + + Two memory-driver keys are **removed**: `indexes` and `maxRecordsPerObject`. + `InMemoryDriverConfig` has no field for either — the driver keeps no indexes and + evicts nothing — so both were inert. Drop them; for real indexing use a driver + that indexes. + + A postgres, mysql or mongo datasource must now name a connection target + (`database`, or a `url` that carries it). An empty `config` used to mean "the + client's own localhost default", which is the same defect in its most complete + form. + + **Also fixed, because the contract can only be enforced where it is honoured.** + These keys were declared and read by nothing; they now reach the driver: + + - `datasource.pool` is honoured by every SQL driver (it was declared, carried + into the connection spec, then overwritten with a hardcoded `{ min: 0, max: 5 }`), + and maps onto the Mongo client's `minPoolSize` / `maxPoolSize`. + - `datasource.schemaMode` reaches the driver. It was dropped between the + datasource record and the connection spec, so a `schemaMode: 'external'` + database — one ObjectStack must never run DDL against — was constructed as + `managed`. + - `datasource.ssl` reaches the SQL clients, certificates and all. It stopped at + the record — nothing put it on the connection spec — so a TLS block configured + nothing, which is exactly what its own schema comment warns about ("a TLS + setting that never took effect looked identical to one that did"). + - postgres `schema` (knex `searchPath`), `applicationName` and `statementTimeout`. + - mongo `password`, `authSource` and `options`. A mongo datasource carrying a + `config.password` previously composed its URL with an **empty** password. + +- 63b33e6: A `datasourceMapping` rule is routing, not a hint — an object mapped to an + unreachable datasource no longer silently reads and writes the DEFAULT store + (#4462). + + **Observable behavior change; read this before upgrading.** Measured on `main` + during the v17 verification: map an object to a Postgres datasource with a bad + URL and the boot succeeds, `/ready` answers `200`, the datasource name appears in + **zero** log lines, `POST /api/v1/data/` returns `201` — and the + row is physically in the default store. The operator finds out by opening the + database they declared and finding it empty. ADR-0062 D2's phase-1 note called a + mapping-only datasource "decorative" to keep an example byte-for-byte unchanged; + what that bought was a silent data-placement bug. + + The fix is a pair, and each half is what makes the other correct: + + 1. **Routing stops falling through** (`@objectstack/objectql`). `getDriver` step + 2: a mapping rule that MATCHES and names a datasource with no live driver now + throws — `DatasourceUnavailableError` when the connect layer recorded a + verdict, otherwise an error naming the object, the datasource and the two + remedies. `default` still resolves onward: the default driver keeps its + natural name (#3826), so step 5 is how routing to it works. + 2. **ADR-0062 D2 grows gate (d)** (`@objectstack/service-datasource`, + `@objectstack/runtime`). A datasource a mapping rule routes at least one + object to is auto-connected at boot, and a boot-time connect failure is + **fatal** with an operator-readable reason — the same call gate (b) already + makes for an explicit `object.datasource` binding, now correct for (d) + because half 1 removed the fallback. `OS_ALLOW_DRIVER_CONNECT_FAILURE` still + degrades the boot instead, as for every other fatal connect. + + The mapped-object list is resolved by the boot path from the engine's own + matcher (`ObjectQLEngine.resolveMappedDatasource`, newly public) and passed to + `connectDeclared({ mappedObjects })`; the connection service never re-derives + rule matching. Two matchers drifting by one clause would connect a datasource + routing never uses, or route to one nothing connects — the defect again. + + **What to do if this breaks your boot.** It means a `datasourceMapping` rule in + your stack points at a datasource that cannot be connected. Either fix the + datasource configuration, or delete the rule — the second is what + `examples/app-crm` did in this change, and it is what keeps that example's + runtime behavior identical: its rules routed everything to an unconnected + `:memory:` datasource, i.e. to the default store by fall-through. + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/services/service-datasource/package.json b/packages/services/service-datasource/package.json index 79681d1564..ffc7e01829 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.1", + "version": "17.0.0-rc.2", "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 d3b9ece799..082638bb7d 100644 --- a/packages/services/service-i18n/CHANGELOG.md +++ b/packages/services/service-i18n/CHANGELOG.md @@ -1,5 +1,71 @@ # @objectstack/service-i18n +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/services/service-i18n/package.json b/packages/services/service-i18n/package.json index eb8254bbb3..79a65f34e5 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.1", + "version": "17.0.0-rc.2", "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 766c301bb9..18845d151e 100644 --- a/packages/services/service-job/CHANGELOG.md +++ b/packages/services/service-job/CHANGELOG.md @@ -1,5 +1,71 @@ # @objectstack/service-job +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/services/service-job/package.json b/packages/services/service-job/package.json index f446ecd264..ab598861cb 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.1", + "version": "17.0.0-rc.2", "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 6324869835..dbafb0a9f6 100644 --- a/packages/services/service-knowledge/CHANGELOG.md +++ b/packages/services/service-knowledge/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/service-knowledge +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/services/service-knowledge/package.json b/packages/services/service-knowledge/package.json index 16ec778013..abb6aa67ee 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.1", + "version": "17.0.0-rc.2", "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 3c32899532..4a07a2ae90 100644 --- a/packages/services/service-messaging/CHANGELOG.md +++ b/packages/services/service-messaging/CHANGELOG.md @@ -1,5 +1,71 @@ # @objectstack/service-messaging +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/services/service-messaging/package.json b/packages/services/service-messaging/package.json index 3156bd1398..a4b729c6e6 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.1", + "version": "17.0.0-rc.2", "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 f0281f4bf1..0dbbc0b4e1 100644 --- a/packages/services/service-package/CHANGELOG.md +++ b/packages/services/service-package/CHANGELOG.md @@ -1,5 +1,71 @@ # @objectstack/service-package +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/metadata-core@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/services/service-package/package.json b/packages/services/service-package/package.json index d30d0c643b..68609f975b 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.1", + "version": "17.0.0-rc.2", "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 eb49360d20..e475dabf4d 100644 --- a/packages/services/service-queue/CHANGELOG.md +++ b/packages/services/service-queue/CHANGELOG.md @@ -1,5 +1,71 @@ # @objectstack/service-queue +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/services/service-queue/package.json b/packages/services/service-queue/package.json index dbce45a4ff..5634b63357 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.1", + "version": "17.0.0-rc.2", "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 32e5c8350a..c408f0c474 100644 --- a/packages/services/service-realtime/CHANGELOG.md +++ b/packages/services/service-realtime/CHANGELOG.md @@ -1,5 +1,71 @@ # @objectstack/service-realtime +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/services/service-realtime/package.json b/packages/services/service-realtime/package.json index ab98207952..7291808eb5 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.1", + "version": "17.0.0-rc.2", "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 3ba2e76012..c113dbbb5e 100644 --- a/packages/services/service-settings/CHANGELOG.md +++ b/packages/services/service-settings/CHANGELOG.md @@ -1,5 +1,72 @@ # @objectstack/service-settings +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/services/service-settings/package.json b/packages/services/service-settings/package.json index 7f6efb6db5..64e8965be7 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.1", + "version": "17.0.0-rc.2", "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 51526f10ae..65257db351 100644 --- a/packages/services/service-sms/CHANGELOG.md +++ b/packages/services/service-sms/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/service-sms +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/services/service-sms/package.json b/packages/services/service-sms/package.json index 456a87cdae..d4cc8acff4 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.1", + "version": "17.0.0-rc.2", "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 68c6efce34..20a897c8d9 100644 --- a/packages/services/service-storage/CHANGELOG.md +++ b/packages/services/service-storage/CHANGELOG.md @@ -1,5 +1,74 @@ # @objectstack/service-storage +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [ff17642] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/observability@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/types@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/services/service-storage/package.json b/packages/services/service-storage/package.json index ad1815991d..c41f7689e9 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.1", + "version": "17.0.0-rc.2", "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 444d2afbcb..c0232d371e 100644 --- a/packages/spec/CHANGELOG.md +++ b/packages/spec/CHANGELOG.md @@ -1,5 +1,2288 @@ # @objectstack/spec +## 17.0.0-rc.2 + +### Major Changes + +- b4487aa: The per-provider connector "template" cluster is removed (#4480, ADR-0049) + + `@objectstack/spec/integration` no longer exports the six per-provider + connector schemas and their sub-schema/type/example clusters (~110 exports, + 2,672 lines): + + - `DatabaseConnectorSchema` (+ `DatabaseProviderSchema`, `DatabasePoolConfigSchema`, + `SslConfigSchema`, `CdcConfigSchema`, `DatabaseTableSchema`, the three + `*ConnectorExample` constants) + - `FileStorageConnectorSchema` (+ bucket/versioning/multipart/filter configs, examples) + - `GitHubConnectorSchema` (+ repository/commit/PR/actions/release/issue configs, examples) + - `MessageQueueConnectorSchema` (+ its queue/topic/consumer configs, examples) + - `SaasConnectorSchema` (+ examples) + - `VercelConnectorSchema` (+ its deployment/domain/env configs, examples) + + The six generated reference pages under `docs/references/integration/` go with + them. + + **Why removal, not completion.** These files were the losing side of an + architecture decision the same module's live half already records. ADR-0023 + rejected hand-modelling each external system's shape inside the spec — + "re-inventing OpenAPI inside this schema" — and ADR-0097's connector protocol + does the opposite: one `ConnectorSchema`, with provider shapes coming from the + provider itself (`connector-openapi` materializes instances from an OpenAPI + document, `connector-mcp` from an MCP server). The templates hardcoded + Postgres/S3/GitHub/RabbitMQ/Vercel shapes into spec files nothing ever read: + + - `engine.registerConnector()` validates against `ConnectorSchema` from + `connector.zod.ts` — never the templates + - the `connectors:` stack collection parses `DeclarativeConnectorEntrySchema` — + never the templates + - nothing else in the monorepo, objectui included, imported any of the six + + They were also semantically wrong where they overlapped the live platform: + `DatabaseConnectorSchema` modelled "tables to sync", CDC, and `readReplicaConfig` + — a second, independent declaration of read-replica routing (the first, + `datasource.readReplicas`, was removed in #4468), complete with a `weight` + field for a load balancer that does not exist. External-database access is + datasource federation (ADR-0015), which is live and is not a connector. + + **Migration.** There is nothing to migrate: these schemas validated no stored + metadata (the `connectors:` collection never used them) and no runtime read + their output. If you imported one as a TypeScript type for your own code, + model your provider config yourself, or — the supported path — declare a + provider-bound connector instance and let connector-openapi / connector-mcp + derive the shape: + + ```ts + // before (typed against a dead spec export) + import { DatabaseConnector } from "@objectstack/spec/integration"; + + // after (the live protocol) + import { + Connector, + DeclarativeConnectorEntry, + } from "@objectstack/spec/integration"; + ``` + + The base protocol — `ConnectorSchema`, `DeclarativeConnectorEntrySchema`, the + ADR-0097 provider contract, connector-descriptor, connector auth — is + unchanged. + +- 9ca2d85: `datasource.readReplicas` is removed (#4468, ADR-0049 enforce-or-remove) + + It described replica connections nothing ever opened. `ConnectableDatasource` + and `DatasourceConnectionSpec` carry no replicas field, the driver factory never + reads the key, and no query path distinguishes a read from a write — the + platform has no read/write splitting at all, so every statement always went to + the primary no matter what was declared here. + + **Migration.** + + | Wrote | Write instead | + | ------------------------------------------ | -------------- | + | `readReplicas: [{ host: 'replica-a', … }]` | delete the key | + | `replicas: [ … ]` (the alias) | delete the key | + + There is no target to move to, because there is no read-replica routing to move + to. If you need replica reads today, front them behind a single endpoint — + pgpool, ProxySQL, an RDS reader endpoint — and point `config` at that endpoint. + That is the one read-scaling path that works, and it worked before this key was + removed too. + + Run `os migrate meta --from 16` to strip it from your sources; the + `datasource-read-replicas-removed` conversion emits one notice per datasource. + Authoring it now fails the parse with the same prescription. + + **Why this one is worth reading about.** #4410 closed the `datasource.config` + gap and, in passing, extended the new per-driver validation over each + `readReplicas` entry — reasonably, since replicas carry the same shape. The + result was a slot that had every marker of a working feature: declared with a + doc comment, `.strict()`-guarded against typos at the top level, and + field-by-field validated against the driver's contract underneath. A replica + block with a misspelt `hostname` was rejected by index, naming the canonical + key. + + None of that is evidence of a consumer, and all of it reads like one. That is + the specific trap ADR-0049 exists for: rigor is cheap to add to a dead slot and + expensive to distinguish from life. Two independent surfaces had drawn the + wrong conclusion — this validation, and objectui's datasource preview, which + rendered a "2 read replicas" pill confirming the config to the author while + nothing routed a single read. The preview goes with the key (objectui side, + same change); `packages/spec/liveness/README.md` has the standing rule it + violated ("an authoring/preview renderer is NOT a runtime consumer"). + + Read-replica routing remains unbuilt. It is tracked as a feature request rather + than left as a schema key that looks like one. + +- 3c7bcc0: feat(spec)!: converge the 11 contracts-vs-domain dual-source type names (#4538) + + `packages/spec/src/contracts/` hand-wrote parameter/result interfaces whose + names collided with same-named zod-derived types in the domains — the #4411 + trap, tracked as 11 rows of `dual-source-exports.baseline.json`. Each name was + judged individually against a three-repo import-level scan (framework, cloud, + objectui): which declaration actually flows at runtime decides the direction. + All 11 rows are deleted from the baseline; no name below is exported twice + anymore. + + **Converged — `./contracts` now re-exports the domain zod type (same + declaration on both entries, imports keep compiling from either):** + + - `NotificationChannel` → `system/notification.zod`'s + `z.infer` (member sets were identical). + - `ValidationResult` → `kernel/plugin-validator.zod` (shapes were identical). + - `HealthStatus` → `kernel/startup-orchestrator.zod` (`details` narrows + `Record` → `Record`). + - `PluginStartupResult` → `kernel/startup-orchestrator.zod`. FROM `plugin: +Plugin` (live object) and `error?: Error` TO the serializable projection + (`plugin: { name, version? }`-passthrough, `error?: { name, message, +stack?, code? }`). Neither side had any consumer outside spec; the + zod-validatable shape wins. + - `StartupOptions` → `kernel/startup-orchestrator.zod` — the PARSED tier + (defaults applied). `IStartupOrchestrator.orchestrateStartup` now takes + `StartupOptionsInput` (the caller-authored all-optional tier, also + re-exported from `./contracts`). Fix for callers typed to the old + all-optional `StartupOptions`: rename to `StartupOptionsInput`. + - `JobExecution` → `system/job.zod`. The system schema's `duration` field is + RENAMED `durationMs` — that is what every job adapter produces and what the + `sys_job_run.duration_ms` column round-trips; the schema described records + nothing ever wrote. Fix: `duration` → `durationMs` when parsing + `JobExecutionSchema` payloads. + - `AnalyticsQuery` → `data/analytics.zod`. The domain schema aligned to the + contract's semantics first: `timezone` LOST its `.default('UTC')` — absence + is meaningful (the engine resolves org timezone, #1982/#2018; the + `/analytics` entry always refused to apply that default). The schema is now + transform-free, so `AnalyticsQuery` ≡ `AnalyticsQueryInput` (both kept + exported). Fix for code that relied on `.parse()` injecting `timezone: +'UTC'`: pass the timezone explicitly or resolve it via the engine chain + (`selection.timezone ?? context.timezone ?? 'UTC'`). + + **Renamed — two genuinely different concepts were sharing one name (both + flow at runtime):** + + - `./contracts` `DriverCapabilities` → **`AnalyticsDriverCapabilities`** + (`{ nativeSql, objectqlAggregate, inMemory }`, the analytics strategy-chain + execution-path probe). The `DriverCapabilities` name now belongs solely to + the data domain's driver feature-flag record (`DriverCapabilitiesSchema`, + what `IDataDriver.supports` declares). Fix: importers of the trio from + `@objectstack/spec/contracts` (or `@objectstack/service-analytics`, whose + re-export is renamed in lockstep) rename the import; importers who meant + the driver flags import `DriverCapabilities` from `@objectstack/spec/data`. + + **Removed — the domain-side declaration was dead (zero import-level consumers + in framework/cloud/objectui; the #4411 family's last survivors):** + + - `system` `MetadataExportOptionsSchema` / `MetadataExportOptions` and + `MetadataImportOptionsSchema` / `MetadataImportOptions` (the + `output`/`source`-directory bags). The names now have ONE declaration each: + the `IMetadataService.exportMetadata` / `importMetadata` parameter + interfaces on `./contracts` (`types`/`namespaces`/`format` and + `conflictResolution`/`validate`/`dryRun`), which `MetadataManager` + implements. No tombstone/D2 conversion, deliberately — these are runtime + option-bag types, not authorable metadata (same reasoning as #4458). + `@objectstack/metadata` re-exports the two names from `./contracts` now + (it previously re-exported the dead system-side shapes its own manager + did not accept). + - `system` `JobSchedule` (the `= Schedule` back-compat alias). The name's one + declaration is the `IJobService.schedule` boundary shape on `./contracts` + (plain-string cron `expression`); the authored metadata type keeps its real + name `Schedule`. Fix: `import type { JobSchedule } from +'@objectstack/spec/system'` → `Schedule` (authoring tier) or the + `./contracts` `JobSchedule` (service boundary), whichever you meant. + +- 4b6cac7: feat(spec)!: resolve the three cross-form dual-source names — ShareRecipientType, TransformType, suggestFieldType (#4539) + + Three `dual-source-exports.baseline.json` rows where the two declarations + sharing a name did not even share a FORM (type vs const, or two unrelated + functions), so a wrong import-path pick had no shape overlap to hide behind + and failed far from the cause. Each judged against a three-repo import-level + scan (framework, cloud, objectui — the latter two contained zero references + to all three names). All three rows are deleted from the baseline. + + **Renamed — `./contracts` `ShareRecipientType` → `RecordShareRecipientType`:** + + Two live concepts shared the name. The security zod enum + (`user | team | position | unit_and_subordinates | business_unit`) is the + authorable sharing-RULE recipient vocabulary and keeps the name. The contracts + type describes a different thing — the `recipient_type` a `sys_record_share` + ROW may carry — and its claim to "mirror spec/security" had been false since + `group`→`team`/`guest` were retired there. Its member set is now aligned to + the storage-side gate it actually mirrors, the `SysRecordShare` + `recipient_type` select: `role` (never persistable, zero producers) is + replaced by `position`. Only `user` is enforced (and written) today; + `ISharingService.grant` keeps refusing every other value (ADR-0078). + Fix: `import type { ShareRecipientType } from '@objectstack/spec/contracts'` + (or from `@objectstack/plugin-sharing`, whose re-export is renamed in + lockstep) → `RecordShareRecipientType`; code that named the `'role'` member + was describing a value no row could ever hold — use the rule vocabulary + (`SharingRuleRecipientType`) if a role recipient was meant. + + **Renamed — `./shared` `TransformTypeSchema` / `TransformType` → + `FieldMappingTransformSchema` / `FieldMappingTransform`:** + + `./data`'s `TransformType` (the authorable import-mapping enum + `none | constant | lookup | split | join | javascript | map`) is the live + declaration and keeps the name. `./shared` exported `TransformType` as the + inferred type of `TransformTypeSchema` — a differently-shaped discriminated + union of transform CONFIG objects — with zero importers for either name in + all three repos. The shared pair is renamed (not just the alias deleted): + the docs generator derives `import type { X }` examples by stripping + `Schema` from each schema const, so an alias-less `TransformTypeSchema` + would have kept generating a reference to an export that no longer exists. + Fix: `TransformTypeSchema` → `FieldMappingTransformSchema`, + `import type { TransformType } from '@objectstack/spec/shared'` → + `FieldMappingTransform` (same shape); importers who meant the import-mapping + enum import `TransformType` from `@objectstack/spec/data`. + + **Renamed — `./data` `suggestFieldType` → `suggestFieldTypeForSqlType`:** + + The only function-kind dual-source. The two implementations were never forks + of one function — different signatures, semantics and return types: + `shared/suggestions.zod.ts` (kept on `.` / `./shared` under the original + name) is the typo-suggester for an invalid authored FieldType + (`(input: string) => string[]`, alias table + Levenshtein, feeds the zod + error map), while `data/type-compat.ts` is the deterministic SQL-column → + FieldType mapper for external-datasource drafts + (`(rawType, dialect?) => FieldType | undefined`, ADR-0015 §4.6). Same input, + divergent outputs — `('varchar(255)')` → `[]` vs `'text'`; `('text_area')` → + `['textarea']` vs `undefined`; `('int')` → `['number']` vs `'number'` — and + the wrong pick compiled wherever the result was only truthiness-checked + (`[]` is truthy). Behavioral divergence is now pinned in + `data/type-compat.test.ts`. + Fix: `import { suggestFieldType } from '@objectstack/spec/data'` → + `suggestFieldTypeForSqlType` (same signature); imports from the root entry + or `./shared` are unaffected. + +- ce92674: feat(spec)!: `job` is a code artifact — runtime creation and org overrides are withdrawn (#4509) + + A `job` metadata item created at runtime could never be scheduled. `JobSchema.handler` + names a function in the **compiled bundle's function table** — the schema says so + ("must match a key in `defineStack({ functions })`") and the scheduler is built that + way: `AppPlugin` sources jobs from `bundle.jobs` alone and resolves each handler + through `collectBundleFunctions(bundle)`, skipping any job whose handler is not in + that table. Yet the type was registered `allowRuntimeCreate: true` (and + `allowOrgOverride: true`), so a job authored in Studio or through `PUT /meta` parsed, + saved, reported success — and never ran. + + Unlike the sibling disconnects closed in this batch, this one **cannot be bridged**. + The runtime writer does not have the bundle and cannot name a function inside it; the + missing piece is a handler-binding design, not an ingestion path. Under ADR-0049 + enforce-or-remove, the honest move is to close the door: + + - `allowRuntimeCreate: false` — no "create job" in Studio or via `PUT /meta`. + - `allowOrgOverride: false` — no per-org job fork, which was unreachable for the same + reason. + + **`job` remains a first-class authorable type.** `*.job.ts` / `*.job.yml` / + `*.job.json` files and `defineStack({ jobs })` are the supported doors, and they are + fully enforced — every schedule shape, `retryPolicy`, `timeout` and `enabled` reach + the scheduler. The kind stays in the metadata registry because its file loader is + genuinely consumed (ADR-0088 admission test). + + **If you were creating jobs at runtime:** move the definition into your stack + (`defineStack({ jobs, functions })`) so the handler resolves against a real function. + Rows already in `sys_metadata` are left untouched — they were never scheduled, so + nothing changes behaviorally; `migrateStoredMetadata` now reports them `skipped`, the + same way it does for `agent`. + + Re-opening the type means constraining `handler` to something a runtime writer can + name — an already-registered flow, or a named and separately governed function — and + building the bridge to `IJobService.schedule`. Flipping the flag without that work + just restores the silent no-op. + +- cf2c9b7: refactor(spec)!: remove the `kernel` metadata-loader envelope family — eleven names that each existed twice, with different shapes, on two subpath entries (#4411) + + `MetadataFormat`, `MetadataStats`, `MetadataLoadOptions`, `MetadataSaveOptions`, + `MetadataExportOptions`, `MetadataImportOptions`, `MetadataLoadResult`, + `MetadataSaveResult`, `MetadataWatchEvent`, `MetadataCollectionInfo` and + `MetadataLoaderContract` (plus each one's `…Schema`) are removed from + `@objectstack/spec/kernel` (`kernel/metadata-loader.zod`). Every one of those + names _also_ existed, with a **different shape**, in + `@objectstack/spec/system` (`system/metadata-persistence.zod`). + + Which type you got depended on nothing but your import path: + + ```ts + import type { MetadataWatchEvent } from "@objectstack/spec/kernel"; // one shape + import type { MetadataWatchEvent } from "@objectstack/spec/system"; // another + ``` + + - **The `kernel` copies had zero consumers.** Import-statement scans across this + repo, `cloud` and `objectui` found every consumer importing from + `./system` (or, for the export/import options, `./contracts`' own interface). + Nothing but `kernel/metadata-loader.test.ts` ever parsed the `kernel` copies. + - **The naming intuition pointed the wrong way**, which is what made this worse + than an ordinary duplicate. The `kernel` copies were the ones that _looked_ + canonical — normalized enums, required fields, a `.describe()` on every + property — and they were the dead ones. The live copy is the loose superset, + and `metadata-manager.ts` calls it "legacy" in its own comments. An + auto-import or a model completion picking by name, or by which one reads as + more rigorous, picked the dead one; because the shapes overlap heavily, that + choice compiled and only failed later, at an edge value (`add` vs `added`) or + on a field one copy made required. + - **No load path parsed them.** These are runtime envelope types, not authorable + metadata — no authored source can carry them. So there is deliberately **no** + `retiredKey()` tombstone and **no** ADR-0087 conversion: a prescription nobody + can receive is noise, and there is nothing for `os migrate meta` to rewrite + (the `plugin-runtime.zod.ts` / dev-plugin precedents, #3950, #4149). + + **FROM → TO — change the import path, keep the name:** + + ```diff + -import type { MetadataWatchEvent, MetadataStats } from '@objectstack/spec/kernel'; + +import type { MetadataWatchEvent, MetadataStats } from '@objectstack/spec/system'; + ``` + + The surviving `system` copy is the **looser** of the two, so a _reader_ of these + types may need narrowing it did not need before; a _producer_ needs nothing. The + differences that actually bite: + + | Type | `kernel` (removed) | `system` (keep) | + | ------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------- | + | `MetadataWatchEvent.type` | `'added' \| 'changed' \| 'deleted'` | also `'add' \| 'change' \| 'unlink'` — the raw watcher values the runtime really emits | + | `MetadataWatchEvent` | `metadataType` / `name` / `timestamp` required | all three optional; adds `stats` | + | `MetadataStats` | `size` / `modifiedAt` / `etag` / `format` required | all optional; adds `mtime`, `hash` | + | `MetadataFormat` | `json \| yaml \| typescript \| javascript` | also the `yml` / `ts` / `js` aliases | + | `MetadataSaveResult.path` | required | optional; adds `stats` | + | `MetadataImportOptions` | `conflictResolution` / `dryRun` / `continueOnError` / `transform` | `source` / `strategy` / `validate` | + | `MetadataCollectionInfo` | `formats: MetadataFormat[]` | `namespaces: string[]` | + + No runtime behaviour changes: nothing read the removed copies. The `system` + shapes are **not** tightened here — they describe what `MetadataManager` + actually emits, and narrowing them would be a separate behaviour change. + + `MetadataManagerConfig` and `MetadataFallbackStrategy` are **unaffected**. They + were never duplicated — `kernel` owns them and `system` re-exports them — and + that is the split that survives: manager _wiring_ is kernel's, the loader/watch + _envelope_ is system's, and nothing is declared twice. + + The retirement kit: baselines dropped deliberately + (`json-schema.manifest.json` minus the 11 `kernel/Metadata*` entries; + `authorable-surface.json` minus the 65 matching lines — nothing can author + these, so no `[RETIRED]` markers); `api-surface.json` regenerated (22 exports + leave `./kernel`); `references/kernel/metadata-persistence.mdx` removed by + `gen:docs`; v17 release notes' dead-clusters table and upgrade checklist + extended. No liveness-ledger entries existed (the ledger tracks authorable + metadata types; these were never one). + +- a2cd18a: feat(spec)!: `@objectstack/spec/kernel` no longer exports `MetadataEvent(Schema)` / `MetadataBulkRegisterRequest(Schema)` — the bare names belong to `./api` alone (#4587) + + The names `MetadataEvent` / `MetadataEventSchema` / + `MetadataBulkRegisterRequestSchema` resolved to **two different declarations** + depending on the import path (`./api` vs `./kernel`) — the #4411 dual-source + trap. Resolution (three-repo, import-statement-level consumer scan: framework, + cloud, objectui — the `./kernel` copies had zero importers outside their own + unit test): + + - **Removed** `MetadataEventSchema` / `MetadataEvent` from + `@objectstack/spec/kernel`. This was a lifecycle-event envelope + (`event: 'metadata.registered' | … | 'metadata.exported'`, plus + `actor`/`payload`/`namespace`) that **nothing ever emitted or consumed** — + the vocabulary appears in no producer in any of the three repos. The live + contract is `./api`'s `MetadataEvent(Schema)` + (`type: 'metadata.{type}.{created|updated|deleted}'`, with `id` / `definition` + / `userId`): `MetadataManager` publishes those events to the realtime + service and `@objectstack/client` / `@objectstack/client-react` subscribe to + them. + - FROM `import { MetadataEvent } from '@objectstack/spec/kernel'` → + TO `import type { MetadataEvent } from '@objectstack/spec/api'`. + **Shape change**: the api event has `id` (uuid), `type` + (`metadata.{type}.{created|updated|deleted}`), `definition`, `userId`; the + removed kernel shape's `event` / `actor` / `payload` / `namespace` fields + do not exist there. If you needed runtime _watch_ events, that contract is + `MetadataWatchEvent` in `@objectstack/spec/system`; repository change-log + events are `MetadataEvent` from `@objectstack/metadata-core` (ADR-0008) — + a third, unrelated declaration that is not part of `@objectstack/spec`. + - **Removed** `MetadataBulkRegisterRequestSchema` / + `MetadataBulkRegisterRequest` from `@objectstack/spec/kernel`. It was a dead + near-duplicate of the REST contract that also diverged from the enforced + write path: its per-item `namespace` field exists neither in + `IMetadataService.bulkRegister` (contracts) nor in + `MetadataManager.bulkRegister`, and `namespace` is deprecated platform-wide. + - FROM `import { MetadataBulkRegisterRequestSchema, MetadataBulkRegisterRequest } from '@objectstack/spec/kernel'` → + TO `import { MetadataBulkRegisterRequestSchema, type MetadataBulkRegisterRequest } from '@objectstack/spec/api'` + (the `POST /api/meta/bulk/register` contract; the type export is new on + `./api` in this release). **Shape change**: items are strictly + `{ type, name, data }` — a per-item `namespace` no longer parses into the + accepted shape. `MetadataBulkRegisterRequest` is the authoring-side type + (`z.input`): `continueOnError` / `validate` stay optional and carry + defaults, as before. + - `@objectstack/spec/api`'s `MetadataEvent(Schema)` and + `MetadataBulkRegisterRequestSchema` are **unchanged** and are now the sole + owners of the bare names. Imports from `./api` need no migration. + - `@objectstack/spec/kernel`'s `MetadataBulkResultSchema` / + `MetadataBulkResult` are **unchanged** — only the bulk _register request_ + pair moved. + + `dual-source-exports.baseline.json` shrinks by exactly these 3 rows (31 → 28, + #4535 C2). + +- 4638aaa: `MetadataWatchEvent.type` now carries only the values the runtime emits: the enum narrows FROM `'add' | 'change' | 'unlink' | 'added' | 'changed' | 'deleted'` TO `'added' | 'changed' | 'deleted'` (#4536, follow-up to #4411). + + The three raw chokidar values had zero producers: both emit sites normalize before constructing the event — `packages/metadata/src/node-metadata-manager.ts` translates chokidar's `add`/`change`/`unlink` in the watcher callbacks (`handleFileEvent` accepts only the canonical three), and `packages/metadata/src/metadata-manager.ts` normalizes repository ops (`create`/`update`/`delete` → `added`/`changed`/`deleted`). Consumers parsing events therefore never received the raw values, and no runtime behavior changes. + + - FROM: an external implementor could construct events typed `'add'`/`'change'`/`'unlink'` and readers had to (needlessly) branch on six values. + - TO: an implementor constructing events with the raw values must emit `added`/`changed`/`deleted` instead; readers may delete any branches on `add`/`change`/`unlink` — they were unreachable. + + No tombstone / ADR-0087 conversion: this is a runtime event envelope type, not authorable metadata — nothing parses it on a load path (the #4411 route). + +- 0222d3c: feat(spec)!: converge the dual-source `MetadataFormat` and `CacheStrategy` enum declarations (#4537) + + Two enum vocabularies were declared twice, on `./shared` and `./system`, and had + diverged on their **values** — which type (and which accepted value set) you got + depended on nothing but the import path (the #4411 trap; #4535/#4506 baseline). + Both converge on one declaration each; the three + `dual-source-exports.baseline.json` rows are deleted. + + **`MetadataFormat` / `MetadataFormatSchema` — the shared declaration is the + single source.** `system/metadata-persistence.zod` no longer declares its own + 7-member copy; it re-exports `shared/metadata-types.zod` (the + `MetadataManagerConfig` pattern — `kernel/metadata-loader.zod` already imported + the shared one since #4411). Breaking on the `./system` entry only: the + extension-style aliases `'yml'`/`'ts'`/`'js'` are no longer accepted. They had + zero producers in this repo, objectui and cloud — every loader normalizes at the + boundary (`FilesystemLoader.detectFormat` maps `.yml` → `'yaml'`, `.ts` → + `'typescript'`, `.js` → `'javascript'`; the database/remote/memory loaders + always emit `'json'`). Migration: write the canonical name — + `'yml'` → `'yaml'`, `'ts'` → `'typescript'`, `'js'` → `'javascript'`. + + **`CacheStrategy` — `system/cache.zod` (`CacheStrategySchema`) is the single + declaration.** The `./shared` copy `CacheStrategyEnum` (and its `CacheStrategy` + type export) is removed: it had zero importers in all three repos, while the + system schema is the one `CacheTier.strategy` gates on — same disposition as + `AggregationFunctionEnum` (objectui#2945): removed rather than reconciled. + Migration: `import { CacheStrategySchema, type CacheStrategy } from +'@objectstack/spec/system'`. The value `'adaptive'`, declared only on the + system side with zero producers, is dropped — the enum carries the four values + both declarations agreed on (`'lru' | 'lfu' | 'fifo' | 'ttl'`); pick one of + those. + + No ADR-0087 conversion / tombstone: loader envelope + config vocabulary with no + authorable-metadata producers (the #4411 / #4536 route), verified by three-repo + scan on the issue. + +- fd3013a: feat(spec,automation)!: converge `script` to a function call — retire the `actionType` branches — and parse `script` / `subflow` config at execute time (#4343) + + A `script` node had four ways to name what it ran and only one of them ran anything. + Protocol 17 keeps that one and retires the rest. + + - **`config.actionType: 'email' | 'slack'`** were **logger-backed stubs**. They wrote a + line, reported success, and delivered nothing — under any configuration, installed + messaging service or not. Every bundled example used one; none of them ever sent + anything. + - **`config.template` / `.recipients` / `.variables`** fed those stubs, so they addressed + a message no channel sent. (The examples did not even reach them: they passed the + payload in `inputs`, which the built-in branch never read.) + - **inline `config.script`** was recognized and **never executed** — the built-in runtime + has no server-side JS sandbox, so the node warned and completed as a no-op. + - **any other `actionType`** was shorthand for a registered-function name — a second + spelling of `config.function` — and `'invoke_function'` was a marker that named nothing + on its own. + + What remains is what worked: `config.function` (now **required**) names a registered + function, `config.inputs` feeds it, `config.outputVariable` binds its return value. + + **The replacements are three different mechanisms, not one rename.** + + | Retired | Use instead | + | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | + | `actionType: 'email'` (+ `template` / `recipients` / `variables`) | a `notify` node — it delivers through the messaging service: the in-app inbox by default, real email once `@objectstack/plugin-email` is installed | + | `actionType: 'slack'` | a `connector_action` node with the Slack connector, or an `http` node posting to an incoming webhook — `notify` has no Slack channel | + | `actionType: 'my_fn'` (shorthand) | `function: 'my_fn'` — the conversion moves it for you | + | `script: '…'` (inline JS) | move the logic into a registered function and call it via `config.function` | + + **Execute-time parse.** `script` and `subflow` now run their config through the contract + before executing, the seam #4277 gave the flat builtins — a violation refuses the node as + a **guard** (wrong metadata; no `fault` edge may route it, #3863). `script` could not join + that seam while its legal key set depended on `actionType`: a flat parse would either + reject valid shapes or wave everything through. Converging the node is what made the + contract fit. `subflow`'s hand-written `flowName` check became the same parse, so its + message is now `subflow 'n1': config does not satisfy the subflow contract — +config.flowName: …`. `decision` deliberately stays export-only: its one key is optional, + so a parse would check nothing. + + **Migration.** `os migrate meta --from 16` rewrites stored sources; authoring one of these + keys in TypeScript is a compile error carrying the same prescription. A shorthand + `actionType` **converts into `function`** — that is what it named — unless `function` is + already set, in which case it was dead metadata the executor never reached. The other four + keys are dropped outright: nothing read them, so there is no value to preserve, and + rebuilding the intent is an authoring decision (the table above) rather than something a + mechanical rewrite can guess. + + The keys leave the **load path** (`retiredFromLoadPath`) with the rest of the keys retired + for _misdescribing themselves_ rather than for being renamed: absorbing + `actionType: 'email'` silently would let an author keep believing the flow sends mail. The + one seam that still replays it is `registerFlow`, which rehydrates data at rest (#3903) — + a row in `sys_metadata` has no author for a tombstone to teach. So a stored email-stub node + arrives stripped of the keys nothing read and then **refuses for naming no callable**, + where it used to log a line and report success. That flip is the behavior change to expect. + + **A build gap this surfaced, fixed here.** `FlowFunctionEntrySchema` now also accepts a + **lowered handler ref** (a non-empty string), the form `objectstack build` produces: the + CLI lowers every inline callable to a serialisable ref _before_ the stack is parsed (it + must — `z.function()` wraps callables and would break the ref mapping), so a built + manifest holds `{ myFn: 'myFn' }`, which neither previous member accepted. The result was + that `defineStack({ functions })` — a documented, first-class mechanism — could not + survive a build at all. Nothing had noticed because no bundled example used it; #4343 + turns that from latent into blocking, since `config.function` becomes the only thing a + `script` node can run. `Hook.handler` already declared exactly this pair (`z.union([ +z.string(), ])`, "string, post-build / inline function, pre-build"), so this + brings `functions` onto the platform's established shape rather than inventing one. A + string carries no callable and `normalizeFlowFunctionEntry` still drops it by design — the + real functions ride in the sibling ESM module the build emits, merged by name — so + hand-authoring one registers nothing and fails loudly at execute ("no function named '…' + is registered"), never silently. + + Also in this change: the retired constants `SCRIPT_BUILTIN_ACTION_TYPES`, + `SCRIPT_INVOKE_FUNCTION_ACTION_TYPE` and the `ScriptBuiltinActionType` type are removed + (they described the dispatch set that no longer exists); `os validate` names a retired key + and its replacement instead of reporting a generic missing callable; and the `#3796` + alias fixture, which carried `actionType: 'invoke_function'` through both sides, no longer + describes an end state protocol 17 can reach — the rename itself is untouched. No liveness + ledger row moves: the gate walks `FlowSchema`, whose `nodes[].config` is + `z.record(z.unknown())`, so these keys were never governed by one. + +- c57f3cf: The `trigger-registry.zod.ts` Connector cluster is removed (#4499) + + `@objectstack/spec/automation` no longer exports the third declaration of the + connector vocabulary: `ConnectorSchema`, `ConnectorInstanceSchema`, + `ConnectorOperationSchema`, `ConnectorTriggerSchema`, `ConnectorCategorySchema`, + `AuthenticationSchema` / `AuthenticationTypeSchema` / `AuthFieldSchema` / + `OAuth2ConfigSchema`, `OperationTypeSchema` / `OperationParameterSchema`, their + inferred types, and the `Connector.apiKey()` / `Connector.oauth2()` factory + helpers — 630 lines, all of `automation/trigger-registry.zod.ts`. + + Despite the filename, the file contained no trigger registry. Every export was + connector vocabulary, self-contained and read by nothing: + + - the automation engine registers and validates connectors against + `ConnectorSchema` from `integration/connector.zod.ts` (ADR-0097) — it never + imported this one; + - the stack `connectors:` collection parses `DeclarativeConnectorEntrySchema`; + - outside the spec package, the only references anywhere in the monorepo were + the two documentation generators that published it. + + This closes the connector triple-declaration: `integration/connector.zod.ts` + is the one live contract (ADR-0097), the six per-provider "templates" fell in + #4480, and this cluster is the last copy (Prime Directive #12 — one + capability, one contract). + + **Migration.** If you imported any of these names from + `@objectstack/spec/automation`, there is nothing to migrate _to_ on that + module: nothing ever consumed what you built against them. Declare real + connector instances with `defineConnector` / the stack `connectors:` collection + (`DeclarativeConnectorEntrySchema`), or materialize them from a provider + document via connector-openapi / connector-mcp. Note the name collision when + migrating types: the live `integration/connector.zod.ts` also exports a + `ConnectorTriggerSchema` and a `Connector` type with _different shapes_ — a + find-and-replace of the import path is not a migration. + + The removal also deletes the "When to use Integration Connector vs. Trigger + Registry?" comparison from `integration/connector.zod.ts`'s header, which + steered "lightweight" cases to the dead file with the platform's authority — + the same defect class as the `capabilities.readOnly` prescription #4487 + corrected. No D2 conversion: none of this was storable stack metadata, so + there is no source for `os migrate meta` to rewrite. + +- ce92674: feat(spec)!: retire the standalone `validation` metadata kind (#4509, ADR-0088) + + A validation rule authored as its own artifact bound to nothing and gated no + write. `ValidationRuleSchema` carries **no object-binding key** — no `object`, + no `objectName` — and all six variants are `strictObject`, so an author could + not supply one either. No merge step existed. The only code that expected such a + key was a reference-tracker row scanning a field the schema would have stripped. + Meanwhile the engine evaluates exactly one shape: the object's own + `validations[]` array, on insert and on every matched update row. + + So a rule created through the standalone door — a `*.validation.ts` file, or + Studio's Validations list — parsed, saved, reported success, and intercepted + nothing. Including a `state_machine` rule, which ADR-0020 routes through this + same vocabulary: an author could believe they had locked down record state + transitions and have changed nothing at all. + + Under ADR-0088 the kind fails the admission test on its first clause: a rule has + no independent lifecycle, because it only means something against an object. And + unlike the sibling disconnects closed in this batch, it could not be bridged into + one — the shape has nowhere to name its object. + + **The rule vocabulary is untouched.** `ValidationRuleSchema` and all six + variants are unchanged and fully live; the engine's evaluation path is not + modified by this change. It is the _kind_ that was inert, not the schema. The + liveness ledger keeps governing it through the gate's `SPEC_ONLY_SCHEMAS` + override (alongside `webhook` and `query`), because an ungoverned live schema is + exactly how the next drift would hide. + + **Migration.** Move the rule into the owning object's `validations:` array — the + rule body is identical, same schema, same six variants: + + ```ts + // before — a standalone *.validation.ts, which never ran + export default defineValidation({ name: 'amount_positive', type: 'script', … }) + + // after — on the object, where rules are evaluated + ObjectSchema.create({ + name: 'invoice', + validations: [{ name: 'amount_positive', type: 'script', … }], + }) + ``` + + Removed: the registry entry (and its `*.validation.ts` / `*.validation.yml` + patterns), the `MetadataTypeSchema` member, the metadata-core lockstep enum + member, the schema-map entry, the create seed, Studio's Validations nav item and + its hand-crafted form, and the dangling reference-tracker row. Standalone rows + already in `sys_metadata` are left alone — they were never evaluated, so nothing + changes behaviorally. + +- 355e951: feat(spec)!: `@objectstack/spec/api` no longer exports the bare names `WebhookConfig` / `WebhookEvent` — they belong to `./integration` alone (#4572) + + The names `WebhookConfig(Schema)` / `WebhookEvent(Schema)` resolved to **two + different declarations** depending on the import path (`./api` vs + `./integration`) — the #4411 dual-source trap, and a cross-form one: + `./api`'s `WebhookEventSchema` was a `z.object` (an OpenAPI 3.1 webhook + _definition_ descriptor: `name`/`description`/`method`/`payloadSchema`/ + `security`) while `./integration`'s is a `z.enum` of connector event types + (`'record.created'` … `'rate_limit.exceeded'`). Auto-importing the wrong side + compiled and validated the wrong contract. Resolution (three-repo, + import-statement-level consumer scan: framework, cloud, objectui): + + - **Removed** `WebhookConfigSchema` / `WebhookConfig` from + `@objectstack/spec/api`. This pair was dead: wired into nothing — not even + `RestServerConfigSchema` — with zero import-level consumers in all three + repos, and no runtime ever read a REST-server webhook config. + - FROM `import { WebhookConfig } from '@objectstack/spec/api'` → + TO: no replacement exists for a REST-server webhook config (it never had a + runtime). For a real outbound webhook use `Webhook` from + `@objectstack/spec/automation`; for a connector webhook use + `WebhookConfig` from `@objectstack/spec/integration` (a **different + shape**: it extends the canonical automation `WebhookSchema` with + `events` / `signatureAlgorithm`, and has no `deliveryConfig` / + `registrationEndpoint` / `enabled`). + - **Renamed** `WebhookEventSchema` / `WebhookEvent` in `@objectstack/spec/api` + → `OpenApiWebhookEventSchema` / `OpenApiWebhookEvent` (same shape, rename + only; joins the existing `OpenApi*` family). `OpenApi31ExtensionsSchema.webhooks` + now references the renamed schema — its parsed/authored shape is unchanged. + - FROM `import { WebhookEvent } from '@objectstack/spec/api'` → + TO `import { OpenApiWebhookEvent } from '@objectstack/spec/api'` (if you + meant the OpenAPI 3.1 webhook descriptor), or + `import { WebhookEvent } from '@objectstack/spec/integration'` (if you + meant the connector event enum — check which shape you actually consume: + object vs string enum). + - `@objectstack/spec/integration`'s `WebhookConfig(Schema)` / + `WebhookEvent(Schema)` are **unchanged** and are now the sole owners of the + bare names. Imports from `./integration` need no migration. + + `dual-source-exports.baseline.json` shrinks by exactly these 4 rows (35 → 31, + #4535 C1). + +- dadb43f: refactor(spec,client,metadata-protocol,runtime)!: retire the workflow service slot — declared end to end, implemented nowhere (#4451) + + The `workflow` slot was ADR-0078's silently-inert declaration at every layer at + once: a `CoreServiceName` nothing ever registered or resolved (ADR-0115 + Evidence 5 — "no code in this repository resolves either slot", verified across + both repositories), an `IWorkflowService` contract with zero implementations, a + `WorkflowProtocol` whose three methods no code ever provided, a discovery + `routes.workflow` field no builder could truthfully populate, and a + `/api/v1/workflow` advertisement for a path no host ever mounted (the + pre-#3586 `DEFAULT_DISPATCHER_ROUTES` already listed it among routes that + never existed). The capability it promised is live elsewhere and has been for + majors: record state machines are enforced by the `state_machine` validation + rule, approvals are first-class flow nodes on the approvals runtime + (ADR-0019), and record-triggered automation is lifecycle hooks + + `record_change` flows (`service-automation`). + + FROM → TO: + + - `CoreServiceName 'workflow'` / `ServiceRequirementDef.workflow` / + `CORE_SERVICE_PROVIDER['workflow']` → removed; there is no slot to fill. + - `IWorkflowService` (`@objectstack/spec/contracts`) → removed; no + implementation ever existed. Register nothing — use the mechanisms above. + - `WorkflowProtocol` + `GetWorkflowConfigRequest/Response`, + `WorkflowState`, `GetWorkflowStateRequest/Response`, + `WorkflowTransitionRequest/Response` (`@objectstack/spec/api`) → removed, + along with the seven published JSON schemas. Delete the import; nothing + ever answered these shapes. + - Discovery `routes.workflow` / `services.workflow` / `features.workflow` + (metadata-protocol + runtime builders) → absent. A reader keying on them + only ever saw `unavailable` / `false`; delete the read. + - `RouterConfig.mounts.workflow` → removed; there was never a surface to + mount at it. + - `RestApiRouteCategory 'workflow'` → removed; categorize automation-adjacent + routes as `'automation'`. + - `@objectstack/client` re-exports of the four workflow types → removed with + their source. (The `client.workflow.*` methods were already removed earlier + in the v17 cycle — this retires the types they returned.) + - Also removed: the stray `graphql` entry in `CORE_SERVICE_PROVIDER` and the + `graphql: { route: '/graphql' }` discovery entry — `graphql` was never a + `CoreServiceName`, and the dispatcher had already dropped `/graphql` as out + of the product plan (#2462 follow-on). + + The retirement kit: the `workflow-service-slot-retired` semantic migration + (major 17) carries this prescription into `spec-changes.json`, the generated + upgrade guide and the `spec_changes` MCP tool. These are TS/API surfaces and a + discovery response field — never stored in stack metadata — so there is no + load-path conversion and nothing for `os migrate meta` to rewrite; the + 21 `authorable-surface.json` baseline lines and 7 `json-schema.manifest.json` + entries for the deleted schemas are dropped deliberately in the same change + (the plugin-runtime precedent: a prescription nobody can receive is noise — + nothing parses these shapes any more). + +### Minor Changes + +- 80334c7: `action` rejects unknown keys, and the ADR-0010 protection-envelope debt list reaches zero. + + `ActionParamSchema` has been strict since #3746 — the template this whole campaign was generalized from, and the source of its sharpest lesson: `visibleWhen` → `visible` showed that the most valuable entry in an alias table is rarely a typo, it is a key that reads as a control and silently is not one. The action _around_ the param stayed open for three more releases. + + **The AI exposure block is the reason this one mattered.** `ActionAiSchema` is the governance gate — its own doc says the platform's value is that "a human can govern exactly which capabilities the agent fleet is allowed to invoke", and that "a half-finished or unreviewed action must never be silently armed". Yet `requireConfirmation` (one letter off `requiresConfirmation`) was dropped in silence, so an author who asked for a human-in-the-loop gate on an AI-invoked action did not get one and was not told. Both that block and the action root now reject, with prescriptions for the two keys authors reach for at the wrong level (`exposed` and `requiresConfirmation` belong under `ai`). + + **An action's capability gate is real, and the near-misses now rename onto it.** `requiredPermissions` (ADR-0066 D4) is enforced with a 403 on the platform action route, so `permissions` / `capabilities` / `acl` are aliased to it rather than being told the gate lives elsewhere. What _is_ tombstoned is the trap beside it: `visible` and `disabled` are UI predicates — **they hide or grey a button, they do not stop a request** — and an action with no UI surface is `locations: []`, still gated. + + That entry was wrong in the first draft of this change, in the direction that matters. It claimed an action carries no permission key and sent authors to the object's permission sets, which — had anyone followed it — invites deleting a working `requiredPermissions` gate. Caught by checking the docs the drift report flagged (`ui/actions.mdx` teaches exactly that key) against the schema. It is the ledger's finding 7 for the fourth time: **this campaign's own prescriptions are themselves a surface that can be confidently wrong**, and the only defence is verifying each one against the schema rather than against memory of it. + + `resultDialog` and its fields, the AI param hints, and the `bodyShape` wrapper close alongside. + + **The undeclared-envelope debt list is now empty.** The structural walk opened it with eight names (`action`, `book`, `field`, `job`, `mapping`, `page`, `translation`, `validation`) after replacing a probe that had been hiding seven of them; `action` was the last. The empty set is kept rather than deleted — with no exemptions, the `DECLARES the protection envelope` case now runs over every registered type, so a new type shipping without the spread fails immediately instead of being quietly added to a list. Adding a name back is filing a bug, not granting an exemption. + + Registered types closed at the top level: **24 of 25**. Only `view` remains. + + Two things the lint layer surfaced, recorded rather than papered over: + + - **The array-index test has run out of subject.** It was `pages[].regions[]`, then `objects[].actions[]`; with `action` closed there is no declared array-of-objects left anywhere in the registered surface that is still strip-mode. The walker's array handling is unchanged and still correct — what is gone is any metadata type that exercises it. The test now asserts the hand-off plus the per-node descent under a closed root (the #4522 fix), and says in place that an indexed assertion should be restored if a new strip surface ever appears. + - **`view` is the last open root**, so when it closes this layer has nothing left to warn about at a root. The test says to change the floor to 0 and assert the empty set _deliberately_ — not to delete the test, because an empty result nobody chose is indistinguishable from a derivation that broke. + + Authoring impact: a key `ActionSchema` does not declare is now rejected instead of silently discarded — it was already being ignored, so no working action changes. + +- a7163ea: The ADR-0078 completeness gate ships: a Zod-valid metadata instance that silently does nothing now fails at author time, on every authoring surface. + + This closes the hole _between_ the platform's existing gates. An instance can be Zod-valid (gate 1 green), use only _live_ properties (gate 2 green), and a correctly-authored sibling can be proven to run (gate 3 green) — and still be dead, because it omits a config its consumer needs and the consumer silently no-ops. The founding case (cloud#687): an AI authored `{ type: 'summary' }` with no `summaryOperations`; the engine's index builder skips it, the field reads 0 forever, the dependent "occupancy rate" is stuck at 0 — and the agent reported the work done, because every gate it could see was green. + + **Why this is worse than the unknown-key hole #4001 just closed.** There, the author wrote a key we don't know, and the parse now rejects it with a prescription. Here every key is one we know, the schema is satisfied, nothing warns, and the author gets a success. It manufactures false completion without the author mistyping anything — and the review step that catches a human's bare summary (seeing the field render `0`) is exactly the step AI authoring removes. + + **One shared predicate, every surface — the ADR's core decision.** Instance-completeness checks previously existed _only_ in cloud's AI-build graph-lint, so a stack authored with `os` + a coding assistant, an MCP agent, `os validate` in CI, or by hand got none of them (`formula_without_expression` existed nowhere in the framework). The judgement now lives in `@objectstack/spec/kernel`'s `checkFieldCompleteness` / `checkViewCompleteness` — sibling of `isIncoherentAggregate`, the ADR-0019 pattern — consumed by the new `@objectstack/lint` `validate-functional-completeness` and registered as an author-time rule (28 → 29), so `os build` / `os validate` / `os lint` / MCP / hand authoring are all covered. Cloud graph-lint can re-home its duplicate rules onto the same predicate rather than drifting from it. + + **Every rule cites the runtime line that makes it true**, because the completeness audit's scariest candidate — a "sharing rule fails open and shares every record" — collapsed on a three-file read, and #4001's last two batches shipped four confidently wrong prescriptions before learning the same thing: + + | rule | the silent skip | severity | + | ------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -------- | + | `field/summary-without-operations` | `engine.ts` — `if (!d.summaryOperations) continue` | error | + | `field/formula-without-expression` | `engine.ts` builds the formula plan only from fields that HAVE one | error | + | `field/relationship-without-reference` | `$expand` — `if (!referenceObject) continue` | error | + | `field/choice-without-options` (`select`, `radio`) | `record-validator.ts` — an empty option list disables server-side value validation | error | + | `field/choice-without-options` (`checkboxes`) | same branch, but shared with free-form | warning | + | `view/layout-without-binding` (`kanban`, `calendar`, `gantt`) | renderer falls back to literal default field names | warning | + + **The deliberate NON-rules are pinned as hard as the rules.** `multiselect` without options is _not_ flagged: `record-validator.ts` says verbatim `// free-form (tags without options)`. The runtime blesses it as a mode, which makes it ADR-0078 case (3) "genuinely optional" — flagging it would be another false prescription, and the test is where that attempt fails first. `timeline` / `tree` views are likewise out of v1: they have config schemas, but their renderer behaviour has not had its verification pass. + + **It found a real one on its first run against a real app.** `showcase_field_zoo.f_summary` was a bare `Field.summary({ label: 'Roll-up Summary' })` — one line below an `f_formula` that _is_ complete, in the object whose entire job is to show what each field type looks like. So the canonical example of a roll-up in this repo computed nothing. It could not be fixed by adding `summaryOperations`: a roll-up aggregates a child into its parent, and the zoo is a leaf (`f_master_detail` makes it a child of `showcase_project`, and nothing is a child of the zoo). Removed, with the working examples named — `showcase_invoice.total` for the plain sum, `showcase_expense_report.total_amount` / `approved_amount` for the `summaryOperations.filter` variant. The rule it broke was the file's own: "relationship types point at the other showcase objects so they have REAL targets." + + Tracked in #4544. This is Phase 1; Phase 2 (the cloud authoring-path config-drop fix) is in the `cloud` repo, and Phase 3 lands the Tier-B shapes one verification pass at a time. + +- e6e9379: ADR-0078 Phase 3: a webhook with no `triggers` now fails at author time — and the Tier-B candidate list is corrected to what verification actually supports. + + **The rule.** `webhook/without-triggers`, error severity, in the shared `@objectstack/spec/kernel` predicate alongside the Phase 1 rules, walked by `@objectstack/lint`'s `validate-functional-completeness` over `stack.webhooks` in both collection spellings. A webhook that declares no trigger materializes into `sys_webhook`, renders in Setup looking armed, and delivers nothing. + + **Why it needed two sources, and why the first one argued against it.** The runtime skip site reads: + + ``` + if (triggers.size === 0) { + // No dispatchable triggers (or a manual-only webhook with none) — + // skip auto-enqueue. + return null; + ``` + + That parenthetical _blesses_ the empty case as a deliberate mode — structurally identical to the `multiselect`-without-options NON-rule, where `record-validator.ts`'s `// free-form (tags without options)` is exactly why we do not flag it. On that evidence alone this candidate stays unenforced. + + The mode it names does not exist. `webhook.zod.ts`'s #3196 note records that the `api` (manual/programmatic fire) trigger was _removed_ because "no manual fire path exists — the only webhook HTTP surface re-queues already-failed deliveries". There is no way to fire a webhook the auto-enqueuer dropped. Inert on every path, so: `error`. + + > **The generalization, now written into the module and pinned by a test:** a runtime comment records what its author believed, and beliefs go stale when a sibling feature is deleted. A blessing has to be corroborated by something showing the blessed mode is still _reachable_ — otherwise it is a comment about a mode that no longer exists. The test asserts the finding carries both citations, so nobody demotes this rule on the strength of the comment alone. + + `triggers: []` is flagged identically to an omitted `triggers`. Unlike an action's `locations: []` — the documented headless spelling — an empty array here carries no "I meant it" signal, because turning a webhook off has its own key (`isActive`). The repo's one real webhook (`showcase_task_changed`) confirms it: shipped inactive via `isActive: false`, with a full trigger list. + + **The corrected Tier-B disposition.** Phase 3 was scoped from the 2026-06 audit's Tier-A/B catalog. Verifying each candidate before writing it — the discipline that caught four false prescriptions in #4001 — found most of the list already closed or misfiled: + + | candidate | disposition | + | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | + | A2 action without `locations` | **already shipped** — `validate-action-locations.ts`, which already exempts the documented `locations: []` | + | B approval empty/unresolvable approvers | **already shipped** — `validate-approval-approvers.ts` | + | B select/multiselect without options | shipped in Phase 1 | + | B write-side referential integrity | **not an authoring-lint item** — a runtime gap; no metadata omission to detect | + | B `unique:true` no-op on memory driver | **not an authoring-lint item** — a driver gap | + | B composite/repeater sub-field constraints | **not an authoring-lint item** — a runtime gap | + | B nav targets of type page/report/url/component/action | **genuine gap, different module** — the key is present but dangling, which is reference resolvability (ADR-0072), not completeness (ADR-0078) | + | B dataset with zero measures | **unverified — not shipped.** No runtime consumer in this repo; the dataset compiler lives elsewhere | + | B webhook without triggers | ✅ **this change** | + | B schedule trigger with invalid cron | **unverified — not shipped.** `normalizeSchedule` accepts any non-empty string, but the scheduler's behaviour on an invalid one was not traced | + + Two candidates are deliberately left unshipped rather than written on the audit's stated confidence, and one is left for the module that actually owns it. The audit's own lesson stands: it produces _candidates_, not confirmed bugs — the scariest one collapsed on a three-file read. + + Tracked in #4544. + +- e6b1b69: feat(spec): allow the aggregate bulk dispatch key `_selectedIds` through the action param gate (objectui#3139) + + A list view's `bulkActionDefs` entry can now opt into an aggregate single-call + dispatch (`execution: 'aggregate'`, objectui 17.1): the renderer invokes the + named object action ONCE for the whole selection, injecting every selected + record id as `params._selectedIds: string[]`, so a single call can produce one + aggregate artifact (zip of QR codes, merged PDF, batch print job). + + `ACTION_PARAM_BUILTIN_KEYS` gains `'_selectedIds'` so the ADR-0104 strict + param gate does not 400 an aggregate dispatch against an action that declares + params — like `recordId`/`objectName`, the key is dispatcher-injected and can + never be authored as a declared param. Pure widening: actions declaring no + params were never validated, and no authored bag legitimately carried this + key. The `bulkActionDefs` describe now documents the aggregate contract + (server reads `params._selectedIds`, results are all-or-nothing, `batchSize` + does not apply, set `maxRecords` for expensive aggregates, and toolbar + url/api actions can interpolate `${ctx.selection.ids}`). + + The showcase's Task → Bulk Actions view carries the specimen: + `showcase_recalc_selection` dispatches the recalc endpoint once for the whole + selection via the endpoint's new `_selectedIds` batch branch, next to the + per-record fan-out fixtures. + +- 2826d1e: fix(automation,approvals): an approval decision can no longer succeed while its flow stays parked (#4420) + + A flow paused at an `approval` node, a deploy, then an approver clicking + Approve: the request row flipped to `approved`, the UI toasted success — and + the flow never moved. No next-stage request, no error, the record's mirrored + status frozen mid-workflow. Approval flows pause for days by design, so a + restart mid-flight is the normal case: every release could quietly zombify + every in-flight approval, with the approvers none the wiser. + + Durable suspended runs (#1518) had shipped and were not the missing piece. Two + other things were. + + **The wiring could enable a store over a table nobody had created.** Object + registration and store activation resolve different services in different + phases — `manifest` at `init()`, `objectql` at `start()` — and the plugin + declared no ordering. Composed ahead of ObjectQL, `init()` found no `manifest`, + warned, and continued; `start()` then attached the DB-backed store anyway. Every + suspend failed with `no such table: sys_automation_run` into a log line nobody + read, pauses silently stayed in memory, and the next restart lost them all. + Now: `AutomationServicePlugin` declares `optionalDependencies: +['com.objectstack.engine.objectql']` (order-if-present, per ADR-0116 — an + engine-less kernel must still boot); a registration missed at `init()` is + retried at `start()`, which still lands before ObjectQL's schema sync; the + store is never attached when registration did not happen, and says so at + **error** level instead of warning; the table is probed once at boot so a + broken setup surfaces there rather than one failed write at a time; and a + failed durable write of a paused run is logged at error — it is data loss in + waiting, not a warning. + + **A reported resume failure read as success.** `AutomationEngine.resume()` + answers a lost run by _returning_ `{ success: false }`, never by throwing. + `ApprovalService` discarded that return value, and `decide()` counted only a + thrown error as failure — so a decision against a dead run came back + `resumed: true`, HTTP 200. Resume failures are now classified + (`RUN_NOT_FOUND`, `STORE_UNAVAILABLE`, `RESUME_IN_PROGRESS`, joining + `PERMISSION_DENIED` / `INVALID_SIGNAL`), so a run that is gone for good is + distinguishable from a store that is merely unreachable, and the raw resume + route maps them to 404 / 503 / 409. + + Approvals acts on them. A new `AutomationEngine.hasSuspendedRun(runId)` — which + reads the suspension store, unlike `getRun()`, and throws rather than answering + `false` when the store is unreadable — pre-flights every flow-advancing + operation (`decide`, `sendBack`, `resubmit`) **before its first write**, so the + zombie half-state is never created rather than merely reported: the decision + fails with `RESUME_TARGET_LOST` (HTTP 409) and the request stays actionable. A + resume that fails after the decision is durable can no longer be undone, but it + now throws `RESUME_FAILED` (HTTP 500) naming the stranded run instead of + reporting success. A concurrent duplicate resume stays benign — the engine's + idempotency guard is doing its job — and reports through the new optional + `resumeError` field. Recall and revise-window cancellation stay non-fatal by + design (they abandon the request), but log at error with the reason instead of + swallowing it. Compositions with no automation engine attached are unaffected. + + Existing zombie requests from affected deployments (already `approved`, run + stranded) are not repaired by this change — `releaseDeadRunRequests` only + sweeps requests that are still `pending`. + +- 20b1a9e: fix(data): the audit anchor is engine-owned, and a lookup must resolve (#4447, #4441) + + Two write-path contract holes from the v17 verification sweep. + + **#4447 — `created_at` was client-writable on an ordinary PATCH.** Its two + siblings only looked protected: the audit hook force-advances `updated_at` / + `updated_by` on every update, so a forged value is overwritten. `created_at` is + insert-only, so nothing overwrote it. The root cause is a _declared_ audit + field shadowing the platform's: `applySystemFields` skips its injection when + the object already carries the name, and the merge lets the declared one win — + correct for an authored business field, wrong for the audit family. A built app + artifact ships a materialized `created_at` carrying only FieldSchema defaults + (`readonly: false`), which shadowed the engine-owned definition, so the + readonly strip had nothing to key off. The audit family's **governance** + (`readonly` / `system` / `type` / `reference`) is now forced by the platform + while presentation (label, description, hidden, group …) stays the author's. + Back-dating is unaffected: `preserveAudit` (#3479/#3493) and `isSystem` writes + still reinstate the original timeline. The strip now also reports through + `droppedFields`, giving the #3794 contract its first live producer on this axis. + + **#4441 — a `lookup` accepted an id that exists in no row of its target.** + Including `sys_position_permission_set.permission_set_id`, where a dangling row + is a security-surface record that resolves to nothing and the audience-anchor + gate has to resolve that very set to evaluate the grant. Writes are now refused + with `400 VALIDATION_FAILED` and a `fields[]` entry + (`code: 'reference_not_found'`, naming the field, the target and the + unresolvable id) — the catalogued `FieldErrorCode` that had no emitter until + now, with its message in the four platform locales. + + Scope for #4441 is deliberately narrow: caller-supplied keys only (so server + stamps are never reported as the caller's bad reference), non-system writes only + (seed replay and package install keep their ordering freedom), empty means "no + link", and it fails OPEN when the target cannot be checked. The existence probe + is unscoped, because existence is a fact about the database — whether the caller + may create the binding stays the RBAC/RLS layer's decision. + +- 4820f55: A blueprint `formula` field can finally say what it computes: `BlueprintFieldSchema` and its OpenAI-strict mirror both gain `expression`. + + `BlueprintFieldSchema.type` is the **full** `FieldType` enum, so the AI-build design step could always NAME a `formula` field — but neither the lenient schema nor the strict mirror the model generates against had any key for the body. There was no way, anywhere on that surface, to state what the formula computed. It materialized bare, and cloud's graph-lint then correctly reported `formula_without_expression` with the fix _"Set field expression to a CEL formula"_ — a fix the agent could not write in the blueprint it was holding. Detected, but unfixable on the surface that produced it. + + This is the exact hole `summaryOperations` closed for roll-ups in cloud#970 (see this file's own test: _"z.object STRIPS unknown keys, so before this slot existed a blueprint that correctly declared `{ type:'summary', summaryOperations:{…} }` lost the config at the parse waist and materialized runtime-dead"_). `formula` was simply left behind — the same defect, one field type over. + + It bites hardest through `nameField`, whose own guidance tells the model to point at a formula for numbered entities (invoice/ticket) that compose `number · name`. Without an expression slot, following that advice produces a record title that is blank on every card, lookup chip and breadcrumb. + + **The pin matters more than the key.** A1's root cause is not a forgotten property — it is that two schemas describe the same shape and nothing forced them to agree. The mirror is what the model may EMIT; the lenient schema is what downstream READS. Drift in either direction silently drops authored config. A new test asserts the two field schemas carry **exactly** the same keys, so the next key added to one cannot go missing from the other. + + Cloud's `objectBody` carries the value through to materialization (companion change in the `cloud` repo); it reads the key via cast, as it already does for `defaultValue`, so it is inert against an older spec and live as soon as this ships. + +- 462d9c4: feat(spec): a solution blueprint can declare a roll-up, including a conditional one + + `BlueprintFieldSchema` gains `summaryOperations` (object / function / field / + relationshipField + a predicate), in both the lenient schema and the strict + structured-output mirror. Without a slot for it, a blueprint `summary` field + could only ever be proposed as a bare shell: `z.object` strips unknown keys, so + a blueprint that correctly declared `{ type:'summary', summaryOperations:{ +object:'task', function:'count', filter:{ status:'completed' } } }` lost that + config at the parse waist and materialized runtime-dead — and the design step, + the one place the aggregation is actually known, had nowhere to put it. + + It has to be declarable at design time: the engine recomputes a roll-up only + when a CHILD row is written, so operations added after a build's sample data + loaded leave every parent value empty until someone edits a child. + + Strict mode cannot represent the canonical `filter` map (open-ended + `additionalProperties`), so the predicate is a flat `conditions` array of + `{field, op, value}` — the shape a dashboard widget's `condition` already uses; + the lenient schema also accepts a real `filter` map for a hand-authored + blueprint. `BlueprintWidgetConditionSchema` is now an alias of the shared + `BlueprintConditionSchema`. + + Also makes the lenient schema's top-level `summary` optional. It is a purely + descriptive one-liner with no structural role, but it is what `apply_blueprint` + parses the model's re-emitted blueprint against — omitting it rejected the whole + build with `path: "summary"`, which an agent read as "the summary FIELDS are + invalid" and repaired by deleting the roll-up fields. The strict design contract + still requires it. + +- 5b843fb: fix(automation,spec): the cold-boot flow bind must survive the read path's own annotations (cloud#971) + + `getMetaItems({ type: 'flow' })` decorates every served item with + `_diagnostics` (and `_draft` on a preview read). The cold-boot bind fed that + served document straight into `engine.registerFlow` → `FlowSchema.parse`, and + since #4001 closed the metadata schemas an unrecognized key **throws** instead + of being dropped — so every flow failed to register on every boot with + `unrecognized_keys: ["_diagnostics"]`. Not fatal only by luck: the + record-change plugin binds record flows a second way, so automations kept + firing behind one WARN per flow. A flow whose only binding path is this one + would have gone silently dead. + + Fixed at the read seam (`readFlowDefsFromProtocol`), not by loosening + `FlowSchema`: the payload is malformed because we decorated it, so the + producer's annotation is the producer's to remove. + + `@objectstack/spec` gains `METADATA_READ_DECORATIONS` / `stripReadDecorations` + (`kernel/metadata-read-decorations`) — the list moves out of + `metadata-protocol`, where it was module-private, so the producer and its + cross-layer consumers share one definition. `metadata-protocol` re-exports + `stripReadDecorations` unchanged; no public surface is removed. + +- 67bf2e2: The dashboard's header, filter bar and root reject unknown keys — the posture its widget was rescued from three releases ago. + + `DashboardWidgetSchema` has been `.strict()` since the ADR-0021 cutover, and its error map states the reason in its own words: undeclared keys "were dropped silently before strict validation, shipping inert metadata". Everything _around_ the widget kept the posture the widget was rescued from — the header, the header actions, the global filters and their option sources, the date range, and the dashboard root itself. + + Closed with `strictObject`, so each now names its surface, echoes the offending key, and suggests the nearest declared one. The aliases are the vocabulary of a dashboard: `charts`/`components`/`cards`/`tiles` → `widgets`, `filters` → `globalFilters`, `refresh`/`autoRefresh`/`pollInterval` → `refreshInterval`, `dateFilter`/`timeRange` → `dateRange`. + + Three wrong-layer keys get a prescription rather than a rename, because a rename would be wrong: + + - **`layout` on the dashboard.** It reads like a template selector; there isn't one. Each _widget_ carries `layout: { x, y, w, h }`, and a widget with none is auto-flowed into the grid. + - **`subtitle` on the header.** The header renders the dashboard's own `label`/`description` — there is no separate header copy, only `showTitle`/`showDescription` to toggle them. + - **`filterBindings` on a global filter.** The binding runs the other way: a _widget_ maps this filter's `name` to one of its own fields, or `false` to opt out. + + Deliberately left open: `DashboardWidgetOptionsSchema` stays `passthrough`. It is the renderer-extras escape hatch by design — presentation settings the renderer understands are none of the spec's business — and the four keys in it that _do_ reach the analytics query are already declared explicitly (framework#3588). Closing it would break the escape hatch to fix a problem that was already fixed the right way. + + Also unchanged: the widget's bespoke `strictWidgetAnalyticsError`, which carries the pre-ADR-0021 inline-analytics and objectui-internal prescriptions. It works and is tested; converging it onto `strictObject` (which would add "did you mean" suggestions on top of those prescriptions) is a follow-up, not a prerequisite. + + Registered types closed at the top level: **23 of 25**. Still open: `action`, `view` — and they are the last two, so the unknown-key _warning_ layer is down to two covered roots. When both close it has nothing left to warn about at a root, which is the campaign finishing rather than the layer breaking; the test says so in place rather than being deleted. + + One thing surfaced while re-pointing a test at `view`: a single unknown key on a view reports **twice**, because `view` is a union (container | ViewItem | overlay) and the walk emits one finding per strip-mode variant the key lands in. Recorded in the test rather than dodged by picking a non-union collection; it becomes moot when `view` closes. + + Authoring impact: a key none of these shapes declares is now rejected instead of silently discarded — it was already being ignored, so no working dashboard changes. + +- 6117f7b: fix(spec,service-analytics): a percentage measure carries its SCALE, so a ratio of 1 is 100% (objectui#3136) + + A `%` format string says how to PRINT a number, not what scale that number is + on — and the two readings collide at exactly `1`, which is both "100%" (a 0–1 + ratio at full compliance) and "1%" (a single percentage point). With nothing on + the wire to tell them apart, renderers guessed from the value's magnitude and + resolved the collision the wrong way: an SLA / pass-rate dashboard reporting + `sla_rate = 1` displayed **"1.0%"** — "everything met the SLA" read as "1% met + the SLA" — on both the KPI card and the dataset table. + + The scale was never actually unknowable; it just never left the server. A + measure declaring `derived: { op: 'ratio' }` is a 0–1 fraction _by definition_, + and a measure aggregating a `percent` field has whatever scale that field + stores. Both facts sit in metadata the enrichment pass already reads for the + ADR-0053 currency chain — which walks back to the source field, checks + `type === 'currency'`, and rides the resolved code onto the result column. + Percentages got no such treatment. They do now, through the same seam. + + **`percentScaleOf(field)` (`@objectstack/spec/data`)** is the one place the + question is answered. A `percent` field stores a FRACTION unless it declares + `max > 1` (e.g. `min: 0, max: 100`), which marks whole-percent storage — the + same rule the percent edit widget already writes by, so a value round-trips. + Non-`percent` fields get no opinion: a plain `number` an author formatted with + a `%` keeps meaning exactly what their format string says. + + **`AnalyticsResult.fields[].percentScale`** carries the answer: `'fraction'` + (`1` ⇒ "100%") or `'whole'` (`1` ⇒ "1%"), absent when the column is not a + percentage. `queryDataset` sets it from the measure's `derived.op === 'ratio'` + first, then the source field's scale. `currency` — emitted since ADR-0053 but + only ever written through a cast — is now declared on the same interface. + + The config seam `measureCurrency` is renamed **`sourceFieldMeta`** and returns + `max` alongside `type`/`defaultCurrency`. The old name had already outgrown + itself: the date-bucketing path reads `type` through it to tell a `date` + dimension from a `datetime` one, and the percent chain is its third consumer. + + Renderers that receive `percentScale` must scale by it rather than inferring + from the value; one that does not receive it (an older server) keeps whatever + fallback it has, so this is additive on the wire. + + **Same widget family, second fix: an empty filtered group is a measured zero.** + A measure-scoped filter can exclude every row of a group the grid still lists, + and the database reports that by omitting the group from the supplementary + result — after the merge, indistinguishable from "not measured". For a COUNT or + a SUM it _is_ measured: the answer is 0. `emptyGroupValueFor(aggregate)` + (`spec/data/aggregation-policy`) states which aggregates have an identity over + the empty set, and `queryDataset` fills it in once all supplementary merges are + done (a later measure's merge can append rows no earlier query saw). So + "0 of 12 paid" now reports `0` instead of blank, and a ratio built on it + computes to `0` instead of going null — the difference between a dashboard + saying "0% met the SLA" and saying nothing at all. `avg`/`min`/`max` keep their + null: there is nothing to average over an empty group, and flattening that to + zero would invent a measurement. + +- cdf4d9a: `datasource.config` is now validated against its driver's contract (#4410) + + `config` was the one authorable slot on a datasource with no gate at all. The + schema's own comment claimed "the driver's own `configSchema` is what validates + it" — nothing did: both bundled driver specs set `configSchema: {}`, no code read + the field, and the per-driver zod schemas were not even exported from the + package. So `config: { hostname: 'db.internal' }` (the key is `host`) was + accepted in silence and the datasource connected to `localhost` while the parse, + the save and the connection probe all reported success. + + `DatasourceSchema` now parses `config` against + the contract for the declared driver, and `DatasourceAdminService` + (create/update/test, the Setup wizard's path) applies the same check. Both read + one registry in `@objectstack/spec/data`, which also projects each contract to + JSON Schema for `DriverDefinitionSchema.configSchema` and the Studio connection + form, so the form offers exactly the fields the validator accepts. + + New exports from `@objectstack/spec/data`: `PostgresConfigSchema`, + `MysqlConfigSchema`, `SqliteConfigSchema`, `SqliteWasmConfigSchema`, + `MongoConfigSchema`, `MemoryConfigSchema`, plus `resolveDriverId`, + `getDriverConfigSchema`, `getDriverConfigJsonSchemaById` and + `validateDriverConfig`. A driver the platform ships no contract for (a plugin's + `com.vendor.snowflake`) keeps an unvalidated `config`. + + **Migration.** A config that was silently ignored now fails with the correction + in the message. The renames: + + | Wrote | Write instead | Driver | + | ---------------------------- | ------------- | ---------------------- | + | `user` | `username` | postgres, mysql, mongo | + | `connectionString` / `dsn` | `url` | postgres, mysql, mongo | + | `uri` | `url` | mongo | + | `file` / `path` / `database` | `filename` | sqlite, sqlite-wasm | + | `hostname` | `host` | postgres, mysql, mongo | + | `searchPath` | `schema` | postgres | + + And the relocations — keys that were never driver config: + + | Wrote in `config` | Write instead | + | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | `min` / `max` / `idleTimeoutMillis` / `connectionTimeoutMillis` | the datasource's own `pool` block | + | `schemaMode` | next to `driver`, on the datasource | + | `readOnly` | `external: { allowWrites: false }` — the enforced write gate. (This row said `capabilities: { readOnly: true }` until #4487's liveness audit found that key has no reader.) | + | `ssl: { ca, cert, key, rejectUnauthorized }` | the datasource's own `ssl` block — inside `config`, `ssl` is the on/off boolean shorthand | + + Two memory-driver keys are **removed**: `indexes` and `maxRecordsPerObject`. + `InMemoryDriverConfig` has no field for either — the driver keeps no indexes and + evicts nothing — so both were inert. Drop them; for real indexing use a driver + that indexes. + + A postgres, mysql or mongo datasource must now name a connection target + (`database`, or a `url` that carries it). An empty `config` used to mean "the + client's own localhost default", which is the same defect in its most complete + form. + + **Also fixed, because the contract can only be enforced where it is honoured.** + These keys were declared and read by nothing; they now reach the driver: + + - `datasource.pool` is honoured by every SQL driver (it was declared, carried + into the connection spec, then overwritten with a hardcoded `{ min: 0, max: 5 }`), + and maps onto the Mongo client's `minPoolSize` / `maxPoolSize`. + - `datasource.schemaMode` reaches the driver. It was dropped between the + datasource record and the connection spec, so a `schemaMode: 'external'` + database — one ObjectStack must never run DDL against — was constructed as + `managed`. + - `datasource.ssl` reaches the SQL clients, certificates and all. It stopped at + the record — nothing put it on the connection spec — so a TLS block configured + nothing, which is exactly what its own schema comment warns about ("a TLS + setting that never took effect looked identical to one that did"). + - postgres `schema` (knex `searchPath`), `applicationName` and `statementTimeout`. + - mongo `password`, `authSource` and `options`. A mongo datasource carrying a + `config.password` previously composed its URL with an **empty** password. + +- 5293114: fix(automation): a decision's three declared ways to route a branch are now one working model (#4414) + + A `decision` node advertised three mechanisms for splitting a path and only one + of them did anything. The other two were the ADR-0049 `declared ≠ enforced` + shape, and the pair of them shipped a guard that does not guard in + `examples/app-crm`. + + | mechanism | before | now | + | :--------------------------------------------------- | :----------------------------------------------------------------------------------------------------- | :---------------------------------------------------- | + | `edge.condition` | ✅ the only one that worked | unchanged | + | `edge.isDefault` | **zero readers** anywhere but the schema declaration | BPMN default flow, enforced in `traverseNext` | + | `decision.config.conditions[].label` → `branchLabel` | matched **0** out-edge labels across every example app, then fell back to the full edge set in silence | routes; an unclaimable label is logged, not swallowed | + + ## What was broken, end to end + + `crm_convert_lead_wizard` means "already converted → abort screen; otherwise → + the wizard". It ran **both**: an already-converted lead got + "This lead has already been converted" and then walked straight into the + conversion wizard behind it. Four independent silences stacked up: + + 1. the decision's first condition was authored `{lead_record.status} == +'converted'` — braces in a slot declared bare CEL, so it was string-compared + and never true; + 2. the second (`'true'`) therefore won, yielding `branchLabel: 'No — proceed'`; + 3. no out-edge carried that label (they were `'Yes'` / `'No'`), so traversal + discarded the branch and considered every out-edge; + 4. `e3b` was unconditional, so it ran regardless — and the natural fix, marking + it `isDefault: true`, was a dead key. + + ## The model + + `branchLabel` narrows the edge set → `condition` gates each edge → `isDefault` + catches whatever is left. Concretely: + + - **`isDefault` is enforced.** A default edge is traversed only when no + conditional sibling of the same source node matched, and it is no longer part + of the unconditional parallel fan-out — that distinction is the whole point of + the marker. Passed over because a real branch won, its target records the same + `skipped` step a closed gate does (#4354). + - **An unclaimable branch label warns.** Traversal still falls back to the full + edge set (a run mid-flight must not die on a metadata error) but says so, + naming the computed branch and the out-edge labels that exist. + - **A decision that declares no `conditions` reports no branch.** It used to + report `'default'` unconditionally — a label no out-edge in the repo ever + carried — which is why every decision node fell back to the full edge set. + The `'default'` sentinel survives for the case it actually describes (declared + conditions, none matched) and is now claimed by the `isDefault` edge as well + as by an edge literally labelled `'default'`. + - **`conditions[].expression` is evaluated as the bare CEL it is declared to + be.** The raw string went to the legacy `{var}` template path, where + `lead.status == 'converted'` cannot resolve and the branch is decided by + string comparison. Unlike `edge.condition` this slot carries no + `ExpressionInput` envelope — the decision descriptor is deliberately + schemaless — so the executor supplies the dialect. A brace-in-CEL predicate + now fails loudly (ADR-0032 §1c) instead of deciding `false`. + + ## Caught at authoring time too + + Four new `os build` / `os validate` warnings, because a wrong route is silent at + run time by nature (Prime Directive #12): + + `flow-branch-label-unmatched` (the shipped shape), + `flow-decision-unconditional-branch` (a guarded decision with an unconditional + sibling — the actual hole), `flow-default-edge-with-condition` and + `flow-multiple-default-edges`. + + Both of the first two fire on the pre-fix `convert-lead.flow.ts` and are silent + after it. + + ## Effect on flows that already exist + + Enforcing `isDefault` changes how a **stored** flow behaves, and the flows it + changes are mostly Studio's own. `objectui`'s flow edge inspector has always + written `isDefault: true` when you bind an out-edge to a decision's default/else + branch — into a key with zero readers, so that edge ran unconditionally, in + parallel with whichever branch actually matched. Those flows now take exactly + one branch. That is the fix, but it is a behaviour change on existing data + rather than only on newly authored metadata, so it is worth knowing before + upgrading: a flow that quietly ran two paths will now run one. + + Nothing changes for an edge that never carried the marker — `isDefault` defaults + to `false`, and an ordinary unconditional out-edge still fans out in parallel + exactly as before. + + ## The example app + + `crm_convert_lead_wizard`'s guard is now a plain exclusive gateway: the + redundant `config.conditions` is gone and `e3b` carries `isDefault: true`. One + mechanism per decision, and exactly one branch runs. + + Verified: 11 new engine/executor tests (including the reported repro in both + directions), 12 new linter tests; `@objectstack/service-automation` 577 tests + and `@objectstack/cli` 652 tests green, all three example apps build with no new + findings. + +- ac471a0: **BREAKING**: `IAutomationService.getSuspendedScreen(runId)` is now **async** — it returns `Promise` instead of `ScreenSpec | null` (#4515). + + FROM → TO for anyone calling or implementing it: + + ```ts + // caller + - const screen = automationService.getSuspendedScreen(runId); + + const screen = await automationService.getSuspendedScreen(runId); + + // implementer + - getSuspendedScreen(runId: string): ScreenSpec | null + + async getSuspendedScreen(runId: string): Promise + ``` + + One-line fix: `await` the call (the enclosing function is almost certainly already `async`), and make any test double resolve rather than return (`mockResolvedValue`, not `mockReturnValue`). + + Why it had to change: the method could only ever read the engine's in-memory hot cache, because a synchronous signature cannot consult the durable suspended-run store. `SuspendedRun.screen` _is_ persisted (`sys_automation_run.screen_json`) and `resume()` cold-reads it back, so after a process restart a still-suspended screen run could be resumed (`POST …/runs/:runId/resume` → 200) while `GET …/runs/:runId/screen` returned 404 “No pending screen for run” — the refresh-safe re-fetch failing in exactly the situation it exists for (page refresh, another device), and the rendering half of ADR-0019's durable-suspend promise missing while the resuming half shipped. + + `AutomationEngine.getSuspendedScreen` now takes the hot cache as its fast path and falls through to the store via the same loader `resume()` rehydrates from. A run that does not exist, is no longer suspended, or paused at a non-screen node still resolves to `null`, so `GET …/runs/:runId/screen` keeps returning 404 for genuinely absent runs. No sync variant of the method remains on the contract. + +- 07a4e26: `field` rejects unknown keys, reusing the curated table that already knew which advice would be wrong. + + `FieldSchema` carries more silently-stripped keys than any other shape in the spec, and it said so about itself for two releases. Two separate notes on the object — one on `accept`/`maxSize`, one on the five pruned governance keys — both state that a write "parsed clean and the key was silently stripped", and both name it the ADR-0104 failure class. Neither could do anything about it, because the object was not `.strict()`. This is the fix those comments wanted. + + **The guidance is derived, not hand-written, and the reason is a bug the first attempt shipped.** `FIELD_KEY_GUIDANCE` in `data/authoring-key-lint.ts` is twenty-odd curated entries for exactly this surface — every one found in the wild, already held honest by a test asserting each `to` names a key `FieldSchema` really declares. A hand-written table beside it would be a second copy of the truth, and it immediately proved why that matters: the lint's table suppresses the suggestion for `pii` **because `pii` is three edits from `min`**. A bare edit-distance suggester answers a personally-identifiable-information key with _"did you mean `min`?"_ — confident, wrong, about an unrelated concept. The hand-written pass did exactly that. `FieldSchema` now reads the table directly (`to` → alias, `why` → guidance). + + Note what moved: the table is unchanged and still tested. Its _consumer_ changed — the lint no longer reaches `field` now that the parse rejects first, so the same curation that powered a warning now powers a rejection. That is the intended end state for every entry in it. + + Among what it carries, the two that matter most are the ones that read as protection and were not: `encryptionConfig` and `maskingRule` were pruned in 2026-06 because they "implied at-rest protection that never happened". An author who declared either had their field stored in plaintext exactly as if they had not, and heard nothing. The rejection now points at `type: 'secret'` and at `requiredPermissions` (ADR-0066 D3, enforced by the FieldMasker). + + **A cycle the whole test suite passed through.** `shared/suggestions.zod` imports `FieldType` from `data/field.zod`, so adopting `strictObject` here closed a loop — field → strict-object → suggestions → field. Under `OS_EAGER_SCHEMAS=1` (how `build-schemas.ts` runs) every `lazySchema` body executes at module init, so the loader hit a half-initialized module and threw before a single schema was built. **284 test files and 7,239 cases went green over it**; tests import lazily, so the cycle never resolved in the order that breaks. Only the eager build caught it. + + `strictObject` now defers its error map to first use, which costs nothing and makes the helper cycle-proof for every schema after this one rather than making each conversion prove it is not in a loop. The property is pinned via an observable — an alias-table getter that fires exactly when the map is built — and verified to go red when the map is hoisted back to construction. + + `field` also gains its ADR-0010 protection envelope. It was the one type the original envelope probe actually checked (the other 24 took an early return), so it was the only gap anyone could see for as long as that probe was green — and it outlasted every gap the probe was hiding. **The undeclared-envelope debt list is down to one** (`action`), from eight. + + `SelectOptionSchema`, `CurrencyConfigSchema` and the nested shapes under `FieldSchema` (lookup columns, lookup filters, `dependsOn` entries, roll-up summaries) close alongside it. Left open deliberately: `AddressSchema`, `LocationCoordinatesSchema` and `CurrencyValueSchema` are runtime _value_ shapes with no consumers at all, two already marked for removal — not authoring surfaces, so strictness is not the question they raise. + + Registered types closed at the top level: **22 of 25**. Still open: `action`, `dashboard`, `view`. + + Authoring impact: a key `FieldSchema` does not declare is now rejected instead of silently discarded — it was already being ignored, so no working field changes. + +- eb4204b: feat(automation): a `script` node's purity contract is declared, and a function that writes can say so (#4396) + + The `script` executor's contract — _the named function returns a value; data I/O + stays on the flow graph_ — existed only as a comment inside the executor, while + #4354's run summary depended on it. That summary reports no record metrics for a + `script` step precisely because a pure function's writes are downstream + `create_record` / `update_record` nodes counting themselves. A function that + wrote anyway made its run report `selected: 30, acted: 0` — indistinguishable + from the broken sweep the counters exist to detect, recorded permanently on + `sys_automation_run`. + + **The rule is now visible.** `ActionDescriptor` carries + `handlerContract: 'none' | 'pure'`, and the `script` descriptor publishes + `'pure'`, so the action catalog, the designer palette and the reference docs + state the rule an author has to follow instead of an executor holding it + privately. + + **And a legitimate writer can opt out honestly.** A `defineStack({ functions })` + entry may declare what it does, in either shape: + + ```ts + defineStack({ + functions: { + scoreLead: (ctx) => ({ score: 42 }), // pure — the default + syncBilling: { handler: syncBilling, effect: "writes" }, // declared writer + }, + }); + ``` + + A step calling a declared writer reports `unmeasuredEffect`, so the run's + `unmeasured` tally keeps the broken-sweep query + (`selected > 0 AND acted = 0 AND unmeasured = 0`) off that flow — and only that + flow. Marking _every_ `script` step unmeasured was rejected: it would blind the + detector on every flow that calls any function in order to cover the few that + break the rule. + + Nothing here is retired or renamed: a bare `functions: { fn }` entry is + unchanged and means `effect: 'pure'`. The declaration is carried end to end — + `ObjectQL.registerFunction` accepts `{ packageId, effect }` alongside the + existing `packageId` string and exposes `resolveFunctionEntry(name)`, + `objectstack build` lowers a declared entry without dropping it, and the + artifact loader re-attaches the module's callable to the declaration the JSON + carried. + + **Also fixed:** `bindHooksToEngine` returned before registering a bundle's + functions when the stack declared no hooks, so a flow-only app's + `defineStack({ functions })` reached the engine as nothing and every `script` + node calling one failed with "no function named 'x' is registered". + +- 4f13be2: Liveness coverage is complete: the nine remaining registered metadata types are + governed (#4488) — `app`, `book`, `doc`, `email_template`, `job`, `mapping`, + `seed`, `translation`, `validation` — and `PENDING_GOVERNANCE` is empty. Every + type in the metadata-type registry now has a ledger with per-property verdicts, + evidence, and a `verifiedAt` stamp. + + Spec: + + - Nine new ledgers under `packages/spec/liveness/` (≈150 verdicts). Highlights: + the ENTIRE `email_template` authoring surface is dead (nothing materializes + metadata items into the `sys_email_template` rows `sendTemplate` reads — an + admin editing the password-reset mail in Studio changes nothing; #4509); + `app.areas[].visible` / `areas[].requiredPermissions` are fail-open dead + gates (item-level siblings ARE enforced); `translation.validationMessages` + is read by nothing while #3778's own migration table steers authors into it; + `job`/`validation` have runtime-authoring doors disconnected from their + execution points (#4509). `doc` and `seed` are fully live. + - `check-liveness.mts`: the walker now sees through `z.preprocess` pipes + (takes the OUT side when the IN side is a transform) — `translation`'s + registered schema was unwalkable before this. + - `liveness/README.md`: the per-type count table's method is now decided and + recorded (it mirrors `check-liveness.mts --json` `byStatus`, the number CI + enforces); all rows regenerated from one run, and the two-generations-stale + `webhook` row rewritten to the post-#3489/#3494 state. + + CLI: + + - `lint-liveness-properties` registers the six newly governed types that carry + `authorWarn` entries (`apps`, `books`, `jobs`, `emailTemplates`, `mappings`, + `translations`), so authors hear about the misleading keys at compile time. + +- 7cf42fe: `mapping`, `agent` and `page` reject unknown keys — and `strictObject` stops suggesting keys that were removed. + + **A bug this campaign introduced into its own helper, fixed first.** `skill` closed in the last batch while still carrying `retiredKey` tombstones, and `strictObject` built its "did you mean" candidates from the whole shape — tombstones included. So a `triggerPhrase` typo was answered with _"Did you mean `triggerPhrases`?"_, a key that had been **removed**. An author who complied landed on the tombstone and got a second rejection telling them to delete what they had just been told to write. + + Third occurrence of a shape the ledger already records twice — this campaign's fix pointing the way into the failure it exists to kill — and the first one in a _shared_ helper, where it would have reached every conversion after it. Fixed structurally: **never suggest a key the schema cannot accept.** Candidates that accept `never` are dropped, so the rule holds without knowing why a key is unwritable. The two helpers stay complementary; `retiredKey` is _stronger_ than a `guidance` entry, because typing the key as `never` also fails `tsc` when the config arrives through a variable, where excess-property checking would not fire. + + **`agent` had two security-shaped removals with no tombstone.** `visibility` and `tenantId` were deleted as unenforced security properties — correctly, since neither did anything — but deleted without a prescription, because the shape was `.strip` and there was no rejection to attach one to. An author who wrote `visibility: 'private'` believed the agent was hidden. It was listed to everyone, and always had been. This is the `skill.permissions` class: a key that reads as a security control, is not one, and says nothing when you write it. Closing the shape created the channel, so both now name what actually gates an agent (`access` / `permissions`, enforced at the chat route since #1884). + + **`route` on a page was a fiction the platform's own test suite carried.** `stack.test.ts` authored `route: '/landing'` for years. `PageSchema` has never declared it — a page is routed by its `name`, which in the map format under test IS the map key, which the test asserted six lines below the key contradicting it. Fifth test found codifying a strip-era fiction as intent, and the most likely to be reinvented, since `route` is the first key anyone reaches for on a page. Tombstoned with `path` and `url`. + + Also tombstoned from each file's own comments, now that there is somewhere to put them: `agent.memory.shortTerm` (declared a working-memory window nothing consumed — ADR-0013 D3), `page.recordReview` and `page.blankLayout` (page types with no renderer, removed in framework#2265), and wrong-layer pointers for the page keys that read like real controls — `interfaceConfig.visualization` (the display mode is chosen from `appearance.allowedVisualizations`, and is not a page type), `guardrails.allowedTopics` (there is no allow-list, only `blockedTopics`). + + `mapping` and `page` also gain their ADR-0010 protection envelope, which their loaders stamp and their schemas could not hold. **The undeclared-envelope debt list is down to two** (`action`, `field`), from eight. + + Registered types closed at the top level: **21 of 25**. Still open: `action`, `dashboard`, `field`, `view`. + + That count is now derived and pinned rather than tallied by hand — it had already drifted by one, in a campaign whose recurring lesson is that hand-maintained measurements of coverage go stale. `metadata-type-schemas.test.ts` walks each registered schema for its top-level catchall and carries the open list as a reverse pin, so closing a type fails the test until the list shrinks, exactly like the envelope debt list next to it. + + The unknown-key warning layer's covered roots drop from 6 to 3 — verified as a hand-off rather than a hole: `agent.zzz`, `page.zzz` and the nested `page.regions[0].zzz` are each now rejected by the parse. A broken walk and a successful graduation shrink that count identically, so the check is pinned in the test alongside the number. + + Authoring impact: a key none of these shapes declares is now rejected instead of silently discarded — it was already being ignored, so no working metadata changes. + +- 7bba90b: `ObjectSchema` rejects unknown top-level keys on the PARSE path, not only in `create()` — closing the founding example of #4001, which had been live for the whole time #1535 was considered fixed. + + **The gap.** #1535 built the unknown-key guard as a hand-rolled check inside the `ObjectSchema.create()` factory, on the reasoning that authored `*.object.ts` modules call `create()`. They do — but they are not the only producer, and not the path most instances travel. `defineStack({ objects })`, `/api/v1/meta/types/object` and the Studio form all reach the schema through `parse()` / `safeParse()`, which kept stripping unknown keys in silence: + + ``` + ObjectSchema.parse({ …, workflows: ['x'] }) → key silently discarded + ObjectSchema.create({ …, workflows: ['x'] }) → rejected since #1535 + ``` + + Object-level `workflows: [...]` — the example this campaign was filed on, an author believing they had wired up automation and shipping dead metadata — was still reproducible on the main path. + + The base shape is now `.strict()` with the `UNKNOWN_KEY_GUIDANCE` tombstones and the semantic renames the warning layer already knew (`capabilities` / `features` → `enable`), so graduating from warn to reject costs an author no prescription. `create()` is unaffected: its own check runs before parsing and throws a richer located error. Safe on the read path for the same reason the other closed registered types are — the ADR-0010 envelope is declared, and `stripReadDecorations` removes `_diagnostics` / `_draft` before any strict re-parse. Verified rather than assumed: every `ObjectSchema.create()` call across `platform-objects` and the three example apps uses only declared top-level keys. + + **A new tombstone.** `namespace` (retired in ADR-0006 D4) had none, so it was stripped in silence — an object written as `{ namespace: 'sys', name: 'user' }` shipped as plain `user`, under a name its author never intended. The rejection now carries the fix (`name: "sys_user"`). + + **And a coverage regression this change would otherwise have introduced.** The unknown-key warning layer gated each metadata collection on its ROOT schema's posture, so closing `object` at the root switched off the warnings for everything _beneath_ it too — its 71 nested strip-mode sites would have stopped reporting in the same change, with nothing to say so. Posture is a per-node property and the walk now treats it as one: a strict root stays silent at its own level (the parse owns that failure) while the descent continues. Nested `object.fields.*` warnings are unaffected by the graduation. + + Three tests that asserted the strip as correct behaviour are now rejection tests — `namespace`, the retired `compactLayout` alias, and the removed `detail` block. The `compactLayout` one had pinned the author-hostile outcome explicitly: "the retired key is STRIPPED, not aliased — an old-key author gets no highlightFields rather than silently working." + +- ebb209c: fix(spec,lint): withdraw the `record:*` blocks from the react tier — no renderer read the props it published (#4413) + + The react-tier contract published `objectName` / `recordId` on + ``, ``, `` and + ``, and no renderer read either prop. All ten `record:*` renderers + take their record from `useRecordContext()`, which only the record route + (`RecordDetailView`) and the metadata editor's preview (`PagePreview`) ever + mount; the `kind:'react'` page renderer wraps the page in a + `SchemaRendererProvider` alone. So the blocks rendered their "bind a record to + preview" placeholder — or, for `record:related_list` (the one that does read + `schema.objectName`), refused to fetch because the parent id never arrived. A + page authored exactly to contract came back EMPTY with nothing reported + anywhere, including by `os validate`, which resolved those props' field names + against the object they named: lint standing guard over a binding that never + ran. + + Withdrawn rather than implemented. The contract was not merely unimplemented, + it was the wrong SHAPE: per-block bindings describe four independent fetches of + one record, which is exactly the coupling the shared record context exists to + prevent (`record:details` drops the fields a mounted `record:highlights` + registered; one inline-edit save bar commits them all under a single + `ifMatch`). Honoring the props would have fossilized that (Prime Directive + #12). The naming of that primitive — a record SCOPE an author wraps around the + family, one fetch, shared context — is the open design question, filed as #4444. + + `@objectstack/spec` drops the four blocks from `REACT_BLOCKS` and gains the + ledger for why, plus the working replacement per type. The family is derived + from `ComponentPropsMap`, so a record component added later is gated the day it + lands — including the six that were never in the contract but are just as + reachable through the registry-built react scope. + + `@objectstack/lint` gains `react-block-needs-record-context` (error), which + rejects them on a react page by tag and through `` + alike, quoting the block that does work: `', '=', +parentId]}>` for a related list, `` for a + field panel. A locally-declared component of the same name shadows the injected + scope and is left alone. + +- 2a44c1d: Six more registered metadata types reject unknown keys — `report`, `dataset`, `email_template`, `skill`, `job`, `book` — and `skill`'s silently-stripped `permissions` key now says where the real gate lives. + + Mechanical work on the registered-type line, using `strictObject`. Each conversion is one call plus the aliases that fit that surface's vocabulary (`sections`/`chapters`/`toc` → `groups` on a book, `cron`/`interval` → `schedule` on a job, `title`/`content`/`html` → `subject`/`body` on an email template). + + **One of them was a silent permission gate, which is the class this campaign cares most about.** `skill` accepted a `permissions` key and dropped it — skill invocation was never permission-gated. An author who wrote it believed they had restricted who could invoke the skill, and had not. A test even pinned that strip as correct behaviour, with a comment explaining the right answer (gate at the AGENT via `access` / `permissions`, enforced since #1884) — but that comment was only visible to someone reading the test file, never to the author who got it wrong. The rejection now carries the prescription, and the test asserts the rejection. + + Same shape as `visibleWhen` → `visible` in #3746: the most valuable entry in an alias table is rarely a typo, it is a key that reads as a security control and silently is not one. + + Registered types closed at the top level: **16 of 25**, up from 9 when this line started. Still open: `action`, `agent`, `dashboard`, `field`, `mapping`, `page`, `translation`, `view`. + + The unknown-key warning layer's covered population drops from 12 roots to 6 as a result, which is the campaign succeeding rather than coverage rotting — the parse takes over where the lint used to warn. Nested strip sites under a closed root still report, unchanged. + +- f3141d8: fix(spec): a node that publishes no descriptor configSchema can now own an expression-ledger entry (#4439) + + `FLOW_NODE_EXPRESSION_PATHS` is the #4027 ledger that tells `registerFlow` and + `objectstack validate` which config keys hold expressions, and in which dialect. + Its ratchet (`config-expression-ledger.test.ts`) derives what it expects from + descriptor `configSchema` `xExpression` markers, and fails in **both** + directions — an undeclared marker, or a ledger entry nothing declares. + + `decision` / `script` / `subflow` publish **no** descriptor `configSchema` on + purpose: a published partial schema would drop the editors their hand-written + Studio forms need (the #4210 incident), so their contract lives in + `schemaless-node-config.zod.ts`. Those two rules compose into a hole — an + expression slot on a schemaless node is structurally unreachable by the ratchet, + and because the reverse direction rejects unclaimed entries, it cannot be + entered by hand either. + + `decision.conditions[].expression` sat in that hole. Its own schema says + _"Bare CEL predicate deciding this branch"_ and its own comment names `{…}` as + the #1491 trap, and no validator walked it — so `{lead_record.status} == +'converted'` passed `tsc`, passed `objectstack validate`, passed registration. + #4414 made that fail loudly at run time; this makes it fail at build time, + which is the delay #4027 exists to remove. + + ## The fix + + The ratchet now reads **both** declaration channels: + + - **descriptor `configSchema`** — unchanged, enumerated from the live registry; + - **`schemaless-node-config.zod.ts`** — the marker rides + `.meta({ xExpression })` through `z.toJSONSchema`, the same channel + `loop.collection` has used since objectui#2670. + + Spec hands the second channel over as JSON Schema + (`getSchemalessNodeConfigJsonSchemas()`, memoized, `input` mode — the shape a + descriptor's `configSchema` already is), so the ratchet walks both with the + _same_ function. No second notion of "a declared expression property", which is + the duplication a ledger exists to remove, and no `zod` dependency added to + `service-automation`. Each channel is separately asserted non-empty, so a broken + derivation on one side cannot hide behind the other's results. + + `SCHEMALESS_NODE_CONFIG_SCHEMAS` is also exported for anything else that needs + to reason about all node config contracts. Additive — objectui's + `flow-node-config` reconciliation imports each schema by name and is unaffected. + + ## The sweep + + The other schemaless slots were checked and deliberately carry no marker: + `script.template` is a template **id**, not a body; `script.inputs` / + `script.variables` / `subflow.input` are values that interpolate `{token}` — + text-with-holes, the shape essentially every node config string has, already + covered generically by `validate-flow-template-paths` and the CLI flow linter. + A `flow-template` ledger entry means something narrower: a _reference that must + resolve to a value_, like `loop.collection`. So `decision.conditions[] +.expression` is the only genuinely declared expression slot on the class — now + recorded in the ledger's header so it is not re-derived. + + ## Docs corrected + + The flows guide taught the **wrong dialect** for decision predicates in three + places (`'{order_amount} > 10000'`), plus a "braces missing in a decision + expression" warning that inverted after #4414 — and `FlowNodeSchema`'s own + `@example` did the same. All corrected to bare CEL, with the history stated so + an author with a braced predicate knows what changed and why their build now + fails. The dialect table drops from three dialects to two: predicates never take + braces, values always do. + + Verified: 13 new/updated tests across the ratchet, the engine's registration + pass and `@objectstack/lint` (including the exact app-crm predicate rejected at + both `registerFlow` and `objectstack validate`); `pnpm build`, `pnpm typecheck` + (122 tasks), `pnpm lint` and `check:docs` clean. + +- e5e7ee0: `strictObject` makes closing an authoring shape one call; `seed` and `doc` are the first two registered metadata types converted with it; and a new invariant test found two live protection-envelope bugs on `hook` and `datasource`. + + **The helper.** The #4001 wiring was four parts per schema plus a drift test: a hand-transcribed `const X_KEYS = [...]` array, a `strictUnknownKeyError({ knownKeys: X_KEYS, … })` call, the `{ error }` argument, `.strict()`, and an "accepts every declared key" probe to catch the array drifting from the shape it describes. That was 34 key arrays and 16 probe files with most of the authorable surface still ahead — and the array was never necessary: `knownKeys` feeds only the edit-distance suggestion, and the shape object is right there at the call site. `strictObject({ surface, history, aliases?, guidance? }, shape)` derives it, which also removes the per-schema drift probe: a key list read from the shape cannot disagree with it. `aliases` and `guidance` stay hand-written and stay **optional** — they carry judgement rather than transcription, and treating curation as a precondition is part of why the ratchet moved slowly. + + **A sharper targeting rule.** The five-directory triage answers "is this authorable?" but not "is this parsed?" — and after #4410 that second question decides whether a flip enforces anything. `BUILTIN_METADATA_TYPE_SCHEMAS` answers both: every entry is author-written and parsed on three paths (`defineStack()`, `/api/v1/meta/types/:type`, the Studio form). Ten had no strictness at all; `seed` and `doc` are the first two converted. Five of the ten live in `system/`, which the directory triage never covered — the two lenses miss different things. + + **Two live bugs, found by a check rather than by reading.** `MetadataPlugin`'s artifact loader stamps `_packageId` / `_provenance` on every registered type, so a strict schema that does not declare `MetadataProtectionFields` rejects its own loader's output — a hard 422 on the ADR-0094 overlay path. That defect had been found three times by hand (`permission`, `position`, then `seed`/`doc`). A new invariant test over the registered-type registry found it twice more on its first run: **`hook` and `datasource` had both gone strict in the #4001 data step without the envelope.** Both now declare it. The test asserts the hard case (rejects) unconditionally with no exemption list, and tracks the quieter case (silently strips, currently only `field`) separately. + + **Ledger.** `strictObject` replaces the old wiring recipe as the standard, and the gate's site-counting method now counts `strictObject(` alongside `z.object(` — counting only the latter would have made every conversion look like surface disappearing, so "solved" and "deleted" would read the same. The gate caught that itself on the first conversion. + + Authoring impact: on `seed` and `doc`, a key the schema never declared is now rejected instead of silently discarded — it was already being ignored, so no working behavior changes. The rejection names the surface, echoes the key and suggests the closest declared one (`rows` → `records`, `body` → `content`), with tombstones for `path` / `slug` on `doc`. The published JSON Schema is unchanged: `build-schemas.ts` converts with `io: 'output'`, which already emitted `additionalProperties: false` for these shapes. `validation` is the remaining registered type with a known envelope gap; it is a `z.lazy()` discriminated union whose variants `.extend()` a shared base, so it needs per-variant conversion rather than one call. + +- 800bdb0: The Studio authoring surface rejects unknown keys — plugin manifests, the flow builder, and the object designer. + + All 27 shapes across `studio/` close. These are the configs a Studio extension author writes by hand, and a dropped key here is quiet in the way this campaign cares about: the plugin loads, the canvas renders, the designer opens — each contributing less than its author declared. A viewer that never appears in the switcher looks like a registration bug, not a spelling one. + + **The plugin manifest gets the guidance that matters, because this file invites the mistake itself.** It says outright that the manifest is "the `package.json` equivalent" and that `contributes` is "analogous to VS Code's". That analogy is the point _and_ the hazard: an author who knows VS Code reaches for its vocabulary, and every near-miss is a word that is correct over there. `displayName` → `name`, `publisher` → `author`, `contributions` → `contributes`, `activation` → `activationEvents`; and for the keys with no counterpart at all — `main`, `engines`, `categories`, `keywords`, `repository`, `icon`, `dependencies` — a sentence saying what to use instead. + + `main` is the one worth calling out. An author declares an entry point, gets a plugin that loads and contributes nothing, and it looks exactly like a broken `activate()`. The rejection now says there is no entry-point key: contributions are declared in the manifest, runtime components are registered imperatively in `activate()`. + + **The triage verdicts were provisional, and verifying them found one wrong.** All three files carried `(p)` from the original pass, and `plugin.zod.ts` was `mixed` — with an empty note, which is how an unexamined label survives. Reading it settles the question: all eight shapes are contribution points on a hand-written manifest. There is no wire half. + + The method is worth keeping, because the original triage had none: **each file exports a `define*` factory that parses an author-written literal, and a `define*` factory is the authoring door.** That is the same lens the registered-type batches used, and it is cheaper than reasoning about who consumes the output. Recorded in the ledger for the next row that needs promoting out of `(p)`. + + What this checkout could not settle, stated in the ledger rather than glossed: whether `objectui` also _constructs_ these configs programmatically and parses them with extra internal keys. If it does, strictness turns that into a loud 422 at its build — detectable, with the rename suggested — rather than the silent narrowing it replaces. + +- 38f7e4f: Translation bundles and `translation` items reject unknown keys, at both doors — and a bespoke guard that only covered one of them retires into the rejection message. + + Translation data has the cruellest version of the silent-strip failure in the spec. A misspelled group is dropped, the bundle loads without complaint, and the string renders in the source language — **indistinguishable from a translation nobody has written yet.** There is no wrong output to notice, so the bug looks like a coverage gap forever. + + **#3778 already knew this, and fixed it for ten keys.** Retiring the object-first (`o.`) dialect meant old-shape items saved cleanly and resolved to nothing, so it added a `z.preprocess` that scanned for the ten retired keys and raised a 422 naming the right destination for each. + + That guard has the shape every workaround for `.strip` has: + + - **It could only catch mistakes someone had already thought of.** `object` for `objects`, `message` for `messages`, an invented group — still dropped in silence. + - **It ran on one of the two doors.** Only `TranslationItemSchema` (Studio / the metadata API). The same ten keys in a file-authored bundle — the path the examples and the platform apps actually use — were stripped with no complaint at all. The same asymmetry #4522 found in #1535's object guard, two authors solving the problem in front of them. + + With the shape closed the guard is redundant, so it is gone and its ten prescriptions ride the rejection as `guidance`. **What was worth keeping was never the detection — it was the prose.** Detection generalizes for free once the default flips; the sentence telling an author where their content goes does not. + + Closed across every authorable group: object/field/view/action/section translations, apps and navigation, dashboards and widgets, pages, settings, metadata forms, and the i18n config. Aliases carry the near-misses edit distance cannot: `views` → `_views`, `help` → `helpText` on an action param (`help` is correct one surface over), `label` → `title` on a widget (a dashboard's headline is `label`, its widget's is `title` — one level apart, opposite spellings). + + **The i18n config's four removed knobs get tombstones.** #3494 deleted `fileOrganization`, `messageFormat`, `lazyLoad` and `cache` because no runtime read them. Removing a key that was already a no-op leaves the author with the same silence and one more reason for it; the rejection now says which issue removed it and why. + + **Two gates were found doing half their job.** + + `translation` was on the ADR-0010 envelope debt list — the loader stamps `_packageId`/`_provenance` and the schema could not hold them, so `authored-translation-sync` strips them by hand on the read side. Declared; the list is down to four. + + And `metadata-create-seeds.test.ts` — the canonical guard against a designer's create shape drifting from the spec — asserts every seed parses. The `translation` seed ships `{ name, label, locale, objects }` and the type declared neither `name` nor `label`, so **two thirds of the authoritative create shape was being stripped while the gate that exists to catch that reported green.** A gate built on a `.strip` schema catches a missing required key and can never catch an extra undeclared one. `name`/`label` are now declared (`translation` was the only registered type of 25 without a `name`), classified in the liveness ledger as dead _body_ keys with the row column as the live one. + + Registered types closed at the top level: **17 of 25**. Still open: `action`, `agent`, `dashboard`, `field`, `mapping`, `page`, `view`. + + Authoring impact: a key none of these shapes declares is now rejected instead of silently discarded — it was already being ignored, so no working translation changes. Verified against the real bundles in `examples/app-crm`, `examples/app-todo` and `platform-objects`, all of which `.parse()` at module load, and against the live `GET /translations/:locale` body. + +- 97faca3: feat(spec,lint)!: give `bulkActionDefs` a shape, and lint the aggregate name it references (#4457) + + A selection-bar bulk action was declared as + `z.array(z.record(z.string(), z.any()))` — **no shape at all**. The real + contract lived in objectui's `BulkActionDef` interface and in the executor that + reads it, so every authoring mistake landed as a silent runtime downgrade: + `opeartion` parsed and the executor hit `Unknown operation: undefined` per row; + `excution: 'aggregate'` parsed and the def stayed per-record, so the endpoint + written for ONE `_selectedIds` call got N calls instead — the exact defect + objectui#3139 was filed to make expressible. That is ADR-0018's "second + vocabulary" smell (an action surface sharing none of `ActionSchema`'s checks) + crossed with ADR-0078's silently-inert metadata. + + `ui/bulk-action.zod.ts` types it, with the same treatment `ActionParamSchema` + got in #3746/#4001: a **strict** def whose unknown-key error names the offending + key and the canonical spelling. Beyond spelling, it refuses the combinations the + executor never reads — `patch` outside an `update`, `execution` outside a + `custom`, `params` on a `delete`, `batchSize` on an aggregate — and refuses a + hand-written `actionDef`, which is attached by the renderer when it resolves the + def's `name` and which authored by hand would smuggle an action definition past + the action registry. + + **One shape that parsed before is now rejected**: `operation: 'custom'` without + `execution: 'aggregate'`. `resolveBulkActions` attaches a dispatcher for exactly + one authored shape (the aggregate one); every other custom def falls to + `Promise.resolve()` per row — a button that reports success for every selected + record and does nothing. The error names both legal forms: `bulkActions: +['']` for per-record (promoted with the action's own label, params and + `visible`), `execution: 'aggregate'` for one call over the whole selection. + + Two things are deliberately left open: + + - **`params[]` is `.passthrough()`.** objectui's `BulkActionParam` declares a + `[key: string]: unknown` catch-all — widget config (min/max/step/format) + forwarded to the field renderer as-is. Locking it down would reject valid + config, so declared keys are typed and the rest rides through, the same call + `dashboard.zod.ts` makes for a widget's `config`. + - **The bulk-param / action-param spelling divergence** (`help`/`helpText`, + `default`/`defaultValue`, `object`/`reference`, plus `labelField`, which + `ActionParamSchema` has no counterpart for). objectui already owns a converter + for the promoted direction; converging the authored direction is a cross-repo + change with its own migration. Typing them as they are is what makes the + divergence visible rather than undocumented — the prerequisite for closing it. + + `label` and the param/option labels are `z.string()`, not `I18nLabelSchema`: + an authored def reaches the grid verbatim (nothing resolves an `{ en, zh }` map + on this path) and the bar renders `def.label` as a React child, so blessing the + map form would trade a parse error for a blank screen. Localize by declaring a + real action and naming it in `bulkActions` — that path runs through the i18n + resolver. + + **Lint**: `validate-action-name-refs` now covers `bulkActionDefs`. Only an + `execution: 'aggregate'` entry is a name reference (it is what + `resolveBulkActions` looks up); an `update`/`delete` def's `name` is a button id + and resolving it would be nonsense. The walk also reaches an **object's own + `listViews`** for the first time — an object has no top-level `list`, so that + tier had simply never been visited while the view-level ones were covered. And + the hint no longer tells a bulk-surface author to add a `locations` entry: the + selection bar is the one surface that does not filter on it, so naming the + action there is the whole placement. + + Verified zero new findings against `app-showcase` / `app-crm` / `app-todo`. + +- ad5fe25: fix(spec,objectql,metadata-protocol): a `user` field carries its target in the TYPE — bare `{type:'user'}` is not targetless + + `field.zod` defines `user` as "a lookup specialized to the `sys_user` system + object … target fixed to the `sys_user` system object", and `Field.user()` — + unlike `Field.lookup(reference, …)` — takes no target argument and writes + `reference: 'sys_user'` itself. The target is a constant of the type. + + Two callers read `field.reference` raw and so disagreed: the protocol's expand + gate refused `?expand=` with `400 INVALID_FIELD … declares no +target object`, and objectql's expand loop skipped it. Metadata authored without + the redundant `reference` — hand-written JSON, an AI author, a Studio form — was + read as under-specified when it was complete. Live capture (cloud#983): an + AI-built app's very first screen rendered an error page over that 400. + + New: `referenceTargetOf` in `@objectstack/spec/data` — the single arbiter of + "what does this reference field point at", next to `REFERENCE_VALUE_TYPES` (the + set those same two callers already share for "is this a reference at all"). Both + halves of the expand path read it, so the gate can no longer refuse a field the + engine would have expanded, nor bless one it skips. + +- ea90179: fix(data,runtime,drivers): four ADR-0112 envelope defects found in the v17 verification sweep (#4431, #4435, #4436, #4483) + + Four independent surfaces where the answer a caller received contradicted the + contract the surface declares. All four were found driving a real showcase boot + against `17.0.0-rc.1` and are catalogued in the #4482 rollup. + + - **#4431 — a sandbox capability denial answered 400.** A denial is the sandbox + refusing to run untrusted code that asked for a capability it does not hold, + which is the crash contract's case (#3951), not a deliberate rejection of a + malformed request. It now answers 500, and the `SandboxError:` debug prefix + no longer reaches the client. + + - **#4435 — PATCH/DELETE of a nonexistent record answered 200 success.** The + write path returned `record: null` / `success: true` for an id that resolves + to nothing, while GET on the same id correctly 404s; `deleteMany` reported + every typo'd id as deleted. Both now answer `RECORD_NOT_FOUND`, so a caller + can no longer read a successful envelope as proof the write landed. + + - **#4436 — the unsupported-filter-operator refusal shipped without + `error.code`.** A refusal with no code is unmatchable by a client, and the + message leaked the internal `[sql-driver]` prefix. It now speaks + `INVALID_FILTER` without the driver prefix. + + - **#4483 — the `$search` auto field set admitted its lead field + unconditionally.** `nameField`/`name`/`title` were prepended without passing + `SEARCH_AUTO_EXCLUDED_FIELDS`, so a search could be aimed at the primary key. + The lead field now only ORDERS the set it is already a member of; it can no + longer admit one. + + These change responses that were observably wrong, so callers coded against the + buggy shapes — a 200 on a missing record, a 400 on a capability denial — will + see different status codes. Graded `minor` on that basis rather than `patch`. + +- 5ef0b5b: The six `validation` rule variants reject unknown keys, each against its own key set, and the type can finally represent its ADR-0010 protection envelope. + + `validation` was the registered metadata type this campaign kept deferring: a `z.lazy()` discriminated union whose six variants each `.extend()` a shared base, so the one-call `strictObject` conversion the other types took does not apply. + + **Why not just close the base.** `.extend()` inherits strictness, so closing `BaseValidationSchema` alone would have rejected unknown keys correctly — but the error map closes over the key list it was _built_ with, which for the base is only the shared keys. A typo of a variant's own key (`transtions` for `transitions`, `formuIa` for `formula`) would have been rejected with no rename offered, which is the difference between a fixable error and a confusing one. The union discriminates on `type`, so an author is always on exactly one variant and that variant's full key set is the right candidate list. The base is now a named shape spread into six `strictObject` calls, each with the aliases that fit its own vocabulary. + + **The envelope.** `validation` is a registered metadata type, so `MetadataPlugin`'s loader stamps `_packageId` / `_provenance` on it and `getMetaItemLayered` → `saveMetaItem` round-trips a body carrying them — and the schema could not represent them, so they were dropped on every parse. Declared once in the shared shape, so all six variants inherit it, and the type comes off the debt list in `kernel/metadata-type-schemas.test.ts` (that list carries a reverse pin, so removing an entry is forced rather than optional). + + Authoring impact: a key none of the variants declares is now rejected instead of silently discarded — it was already being ignored, so no working behavior changes. The rejection names which variant it landed on ("this state-machine validation rule"), echoes the key, and suggests the closest declared one from that variant's full set. + +- 48fbacb: The view surface closes — the container, both view kinds, and the ~28 config shapes under them. This is the last batch of #4001. + + Views are the surface an author iterates on visually, which is exactly why a dropped key hides here: the view still renders, just not the way it was described. `FormFieldBaseSchema`, `FormSectionSchema` and `FormButtonConfigSchema` were closed years ago under ADR-0089 D3a; the other forty-odd shapes in the file kept the posture those three were rescued from. + + **`defineView`'s guard was another one-door workaround.** It rejects a container that defines no views, and its comment says why: "`ViewSchema` strips unknown top-level keys, so a _flat_ list view would parse to an empty container". That is the fifth bespoke guard this campaign has found built around silent stripping, and like all of them it covered exactly one door — `defineView`. Through the metadata door (Studio, the API, an agent) a flat view produced an empty container in silence. The rejection now lives in the parse, so it reaches both, and carries the wrap instruction rather than only the symptom. The guard stays for the case strict cannot see: `defineView({})`, which has no unknown keys and still registers nothing. + + **The container carries its own identity and object binding, and the first draft tombstoned all three.** `name`, `label` and `object` were in that guidance list, telling authors they "belong to a single VIEW, not to the container" — which rejected shapes the platform itself writes: `saveMetaItem` sends the name, artifact-shipped containers (`service-ai/ai_traces`) carry it, the validation sweep injects it, and a stack-level `views: [...]` entry needs `object` to say which object its views belong to (`getViewsByObject()` reads that binding). All three are now declared — `object` as live with its consumer cited, `name`/`label` as dead _body_ keys with the row column live, exactly as `translation` needed in batch 5. + + Caught by the full monorepo suite — `@objectstack/objectql`, then `@objectstack/cli` — never by `packages/spec`. That is the fourth false guidance claim in three batches (`action.permissions`, `action.location`, `view.name`/`label`, `view.object`), and the fix that finally worked was not more care but a different method: **scan every real container payload in the repo and keep only the guidance entries no real payload contradicts.** Six of the nine survived. That check costs one command and should have run before the guidance, not after three CI failures. + + The rule worth carrying: **a rejection's prose is behaviour, not documentation.** It tells an author what to do next, and a confidently wrong one is worse than none, because there is no reason to doubt it. + + **Three shapes are deliberately left open, each with its reason in the file rather than a silent skip:** + + - **`FormSectionSchema`** already closed under ADR-0089 D3a with `strictVisibilityError` and a `.transform()` that normalizes the `visibleWhen`/`visibility` pair. Converting means re-expressing that map as `guidance` and re-proving the transform — a refactor of working, tested behaviour, not a strictness change. + - **`UserFiltersSchema`** deliberately _strips_ `tabs`/`showAllRecords`, which are page-only keys (ADR-0047), with a test asserting the drop. The likely right end state is a rejection saying "tabs are page-only" — but that is a behaviour change with a real consumer question behind it (something may pass a page-shaped block through relying on the strip to narrow it). The campaign's own rule is verify-then-enforce, and this batch did not verify it. Named as the one open shape in the file. + - **The flattened Studio overlay** in `ViewMetadataSchema` must stay open: it carries auxiliary round-trip keys (`isPinned`, `sortOrder`, …) that `saveMetaItem` persists verbatim. + + **That last one is the trap the ledger warned about, arriving on schedule.** `.extend()` inherits strictness, so closing `ListViewSchema`/`FormViewSchema` for authoring silently made the overlay strict too — turning a shape _the platform itself writes_ into a 422. Both members now `.strip()` back, with a comment saying the `.strip()` is load-bearing rather than leftover. + + ## `view` is the end state, not the last item of debt + + The registered `view` schema stays `strip`, and it always will: it is a union of three runtime shapes and a union is only as closed as its most open member. That member is the Studio overlay above — a wire shape wearing the same type name. + + So the campaign's final number is **24 of 25 registered types closed, with the 25th a documented permanent exception**. That is recorded in `metadata-type-schemas.test.ts` beside the reverse pin, so nobody "finishes the job" by force. What closed is everything an author writes; what stayed open is the thing the ledger's classification rule exists to distinguish — arriving here as the campaign's answer rather than as an exception to it. + + ## Where the campaign ends up + + - **Registered types closed: 24 of 25** (from 9 when this line started), the last one exempt with a stated reason. + - **The ADR-0010 undeclared-envelope debt list is empty**, from the eight the structural walk opened it with. + - **The unknown-key warning layer has one covered root left** — `view`, and only its open member. When a layer built to warn about strip-mode metadata has almost nothing left to warn about, that is the ratchet finishing. + + Authoring impact: a key none of these shapes declares is now rejected instead of silently discarded — it was already being ignored, so no working view changes. + +### Patch Changes + +- 5a84d41: fix(approvals): record an admin override of a staffed approver slate AS an override (#4466) + + An admin who is not in a request's `pending_approvers` may still act on it — the + `#3424` privileged-override path exists so a request routed to an unstaffed + position, or to approvers who have all left, is not undecidable forever. The + override is defensible; what was not is what the audit trail recorded. + + `sys_approval_action` had no override column at all. So an admin overriding a + properly-staffed slate wrote a row **byte-for-byte identical** to the designated + approver approving normally: a reader of the timeline saw `approve` by the admin + and could not tell whether the admin _was_ an approver or _overrode_ the ones who + were, and the bypassed approver's later `409 INVALID_STATE` was the only trace — + existing only if they happened to try. The platform knows at decision time (it + took the `isOverrideActor` branch to admit the call at all), so this was dropped + information, not unavailable information. The whole point of an approval record + is to answer "who authorized this, and were they entitled to?". + + `sys_approval_action` now carries **`via_override`** (boolean, optional), set on + exactly the actions admitted by that branch — `decideNode`'s approve/reject and + `reassign`'s admin rescue. It is surfaced on `ApprovalActionRow.via_override` + (`@objectstack/spec/contracts`), returned by `listActions`, and added to the + object's `highlightFields` and two grid list views so a timeline can say + "overrode the approver slate" instead of rendering it as an ordinary approval. + + Three distinctions the column keeps apart deliberately: + + - **`true`** — the actor held no slot in the slate and was admitted only by the + override branch. + - **`false`** — checked, and it was not an override. An admin who _is_ a + designated approver is approving normally and records `false`: the marker is + about which branch admitted the call, not about whether the actor holds admin + rights. + - **absent** — a row written before this column existed. "Not recorded" is not + the same claim as "not an override", so `rowFromAction` maps `null` to + `undefined` rather than to `false`. + + Additive and nullable, so this needs no data migration: existing rows keep + working and simply read as unrecorded. Levelled `patch` rather than `minor` + because nothing an author writes changes — but note it _is_ an observable + behaviour change on a read surface: `listActions` responses and the + `sys_approval_action` grid views now carry a field consumers did not see before, + and `sys_approval_action` gains a column on next schema sync. + +- a52e2ef: fix(driver-sql,spec,objectql): a `defaultValue` runtime token never becomes a column DEFAULT (#4560) + + `Field.user({ defaultValue: 'current_user' })` is resolved by the **engine**, at + insert time, from the request's `ExecutionContext` — and with no authenticated + user (system / anonymous writes: seed replay, package install, boot + provisioning) `applyFieldDefaults` deliberately leaves the field **unset** + rather than stamp a bogus owner. + + The SQL DDL had never heard of the token. `createColumn` passed any non-object + `defaultValue` straight through to `col.defaultTo(dv)`, so the column was + created as `DEFAULT 'current_user'` and the **database** overrode the engine's + decision: every insert that omitted the field stored the literal string + `current_user` in a `lookup('sys_user')` column — a value that is not any user's + id. `?expand` resolves it to nothing, and on an owner / approver field it is a + silent mis-attribution. Found by #4551's dangling-reference audit on its first + run against a real boot; #4441's referential check could never have caught it, + because it inspects the values a **caller** supplied and here nobody supplied + one. + + **The token vocabulary is now declared once, in `@objectstack/spec/data`** + (`DEFAULT_VALUE_TOKENS`, `isRuntimeDefaultToken`, `isNowDefaultToken`, + `isCurrentUserDefaultToken`, `isAppResolvedDefaultToken`). The engine's + insert-time resolution and the driver's DDL read the same set, which is the + actual defect: `'NOW()'` was special-cased in the branch immediately above for + precisely this reason, and `current_user` — the same convention family — simply + had no entry anywhere the DDL could see. A token added to the set tomorrow is + excluded from literal column DEFAULTs automatically, rather than leaking its own + spelling into the database the way this one did. + + **DDL, in one place** (`applyDeclaredColumnDefault`, shared by column creation + and the SQLite table rebuild): + + - `'NOW()'` → the driver-native canonical default, exactly as before; + - any other runtime token → **no column default at all** (the engine owns it); + - Expression envelopes (`{ dialect, source }`) → unchanged, no default; + - a real literal → emitted verbatim, unchanged. + + **Existing databases carry the wrong DEFAULT**, so it is corrected through the + managed schema-drift path (#2186) rather than a bespoke migration: a new + `default_mismatch` finding with a `drop_column_default` op, categorised `safe` + (the statement cannot fail and touches no rows). Dev boots with + `autoMigrate: 'safe'` reconcile it automatically; everywhere else it is reported + with an actionable hint and applied by `os migrate apply`. Postgres/MySQL use + `ALTER COLUMN … DROP DEFAULT`; SQLite, which cannot alter a default in place, + goes through the existing table rebuild — which now re-materialises every + column's default from **metadata**, so a sibling `defaultValue: 'NOW()'` column + keeps the default it always had instead of losing it to the rebuild. + + **Rows already holding the bogus value are NOT rewritten.** That is #4551's + standing rule — report, never rewrite — so they stay visible to the + dangling-reference audit for operators to resolve deliberately. + +- 20bc357: fix(spec,metadata-protocol,runtime): discovery stops advertising routes for the kernel-internal cache/queue/job slots (#4318) + + The metadata-protocol discovery builder declared `/api/v1/cache`, `/api/v1/queue` + and `/api/v1/jobs` — three paths that existed nowhere else in the repository: no + dispatcher domain, no adapter mount, no plugin registration, and the shipped + providers (`service-cache`/`-queue`/`-job`) are in-process contracts that will + never mount one. Every default boot therefore advertised a route inside the same + `ServiceInfo` whose `handlerReady: false` said the opposite — a single record + contradicting itself (ADR-0076 D12). + + These slots are route-less now, like `realtime` — but unlike `realtime` an + unmarked real implementation stays `available`: the slot's contract is + in-process, so "no HTTP surface" is not reduced capability for it. `handlerReady` + is reported `false` on both discovery builders — for a route-less slot it is not + a proxy for anything, it is the fact itself (the dispatcher used to claim + `handlerReady: true` here for an unmarked occupant, a handler that does not + exist). The explanatory message is written once, as + `inProcessServiceMessage(slot)` in `@objectstack/spec/system`, so the two + builders cannot drift apart. + +- 2382580: fix(docs): the schema-extension FAQ rotted in reverse — `.extend()` works on `FieldSchema` again since protocol 17 + + The #3890 fix taught that `FieldSchema` / `ObjectSchema` / `ActionSchema` are all + `ZodPipe`s and that `FieldSchema.extend` throws. True when written; protocol 17 + (#3855) then retired the deprecated aliases whose lowering was the whole reason + for the field/object transforms, the pipes collapsed to plain `ZodObject`s, and + both prose claims inverted within days: `.extend` works, and the recommended + `FieldSchema.in` is now `undefined` — following the FAQ was once again the only + way to hit an error. Only `ActionSchema` (whose `requiresFeature` → `visible` + lowering is still live) remains a pipe. + + The example gate never noticed because the checked block used only `.parse()` — + deliberately shape-agnostic after CI rejected the first #3890 attempt. That made + the code durable and left the PROSE as the only load-bearing surface, which is + where the rot settled. + + So the rewrite moves the claim into the checked block: it now calls + `FieldSchema.extend({ … })` directly, so if the schema ever grows a transform + again the gate goes red instead of the prose going quietly wrong. Composition + stays as the shape-agnostic default, `ActionSchema` is documented as the pipe + case with the `.in.extend` caveat, and the FAQ teaches the one-line probe + (`typeof SomeSchema.extend === 'function'`) instead of a table of shapes that + history says will not stay true. + +- 7631964: feat(spec): ratchet cross-entry dual-source exports — same name, different declaration, caught by symbol identity (#4446) + + `api-surface.json` records every export per entry point, so a name appearing on + two entries was VISIBLE — but nothing distinguished a re-export (one + declaration, two import paths — fine) from a **dual-source** (each entry + resolving the shared name to its OWN declaration, so which type a consumer gets + depends on nothing but the import path). The dual-source case is the #4411 + trap: spec carried two differently-shaped `MetadataWatchEvent`s plus ten more + pairs, and the copy that _looked_ canonical was the dead one — an auto-import + or model completion picking by name compiled fine and failed later, at an edge + value. + + New pure check `check:dual-source-exports` (lint.yml, after the build step): + + - **Judged by symbol identity, not name.** Every export of all 16 public + entries is resolved through its alias chain to the original symbol; a name + whose entries resolve to ≥2 distinct symbols is dual-source. Name-based + counting would drown the signal — the real surface carries 148 legitimate + re-exported names. + - **Shrink-only baseline** (`dual-source-exports.baseline.json`): the 63 + existing dual-source names are recorded (including the `MetadataFormat` + `./shared`≠`./system` enum divergence, the `./contracts` third-shape + interfaces, and two type-vs-const cases `ShareRecipientType` / + `TransformType`). A NEW dual-source fails the gate with the fix at the + declaration (converge + re-export, or rename); a resolved one fails until its + line is deleted. The baseline is hand-edited under review, deliberately not + generated — a `gen:` would admit new dual-sources via "run the fix command". + - **Self-tests first** (like `check:exported-any`): a fixture proves the + detector still flags a true dual-source (incl. type-vs-const) and still + passes re-exports, so a resolution failure can never read as "clean". + + No runtime code changes; no export changes. The 63 baseline entries are + pre-existing debt, now visible and non-growing. + +- 60ae58e: Reference docs: import examples are now spelled from the package's real export surface + + `build-docs.ts` derived each page's "TypeScript Usage" block from the JSON Schema file + name — the value import verbatim, the `import type` line with a `Schema` suffix stripped — + and nothing verified either name existed. `check:docs` could not catch it: it diffs the + generator's output against the committed docs, so a name the generator invents stays "in + sync" with itself forever. 150 of the committed `import type` names did not compile, and + the `.parse()` example called a type rather than the schema const. + + Both lines are now resolved against `api-surface.json`, the committed record of every + `name (kind)` per entry point: only names the entry really exports are emitted, and the + example parses with the actual schema const. A name that resolves to nothing is dropped + from the page and recorded in the new `docs-import-surface.baseline.json` — a shrink-only + ratchet, so removing a type alias while its schema keeps a reference page now turns + `check:docs` red instead of silently publishing a dead import. + +- ce92674: feat(email): declared email templates reach the mail service (#4509) + + Authoring an `email_template` was a silent no-op. `EmailService.sendTemplate` + resolves `(name, locale)` against **`sys_email_template` rows**, and the only + writers of those rows were the built-in auth templates plus a code-constructed + `EmailServicePluginOptions.templates` that no bootstrapper ever passed. Every + door an author can actually use — a stack's `emailTemplates:`, an + `*.email-template.ts` file, Studio's metadata-admin list, `PUT /meta` — parked + items in a metadata store nothing read back. So an admin could "fix" the + password-reset email in Studio, get a success toast, and watch users keep + receiving the built-in copy: ADR-0078 false compliance on **authentication + mail**. This is the shape #3461 had for webhooks, closed the same way (ADR-0049 + enforce-or-remove, route: enforce). + + **`bootstrapDeclaredEmailTemplates`** now materializes declared templates into + `sys_email_template` at boot. Each item is validated through + `EmailTemplateDefinitionSchema.parse()` — the spec schema finally has a real + consumer, defaults and all — and projected with `mapTemplateToRow`, which is the + **same** mapping the built-in seeder uses, extracted and shared so the two doors + cannot drift apart. A malformed template warns and is skipped rather than + crashing boot. + + **Runtime writes take effect immediately.** Unlike `webhook`, `email_template` + is `allowRuntimeCreate: true`, so a boot-only bridge would have left a Studio + save inert until the next restart — the same bug, half-fixed. The plugin also + subscribes to `email_template` metadata changes and re-materializes the single + changed item; withdrawing a template deactivates its rows (across locales) + rather than deleting them. + + **Three breaks sat on this path, not one**, and closing any two of them would + still have shipped a template that never sent: + + - `@objectstack/objectql` never registered a manifest's `emailTemplates:` into + the metadata registry at all — the key was simply missing from the generic + ingestion list, so the bridge's own source was empty. + - The built-in seeder left `managed_by` at the column's `'admin'` default, which + made platform templates masquerade as admin-authored. Since the bridge refuses + to overwrite admin rows, a built-in would have permanently outranked the + template an app declared. Built-ins now stamp `managed_by: 'platform'`. + - Nothing materialized declared metadata into rows. + + **Seed-not-clobber** mirrors `sys_webhook` (#3489) and `sys_sharing_rule` + (#2909): `sys_email_template` gains `managed_by` / `customized`. Declared + templates re-seed every boot as `managed_by: 'package'`; a row an admin created + (`admin`) or edited (`customized`, stamped by a `beforeUpdate` hook) is never + overwritten, so reworded transactional mail survives redeploys. This is a + separate axis from `is_system`, which keeps its existing meaning for built-ins. + + The `email_template` liveness ledger flips from 13 dead properties to fully + live, with an ADR-0054 runtime proof bound on `subject` + (`email-template-materialization`): it boots a real stack, authors a template + that overrides a built-in auth template, and asserts the **authored** wording is + what reaches the transport. + +- ec975f1: fix(objectql,driver-mongodb)!: `findOne` must say which record it wants, and executes every option it declares (#4419) + + `findOne` reads a single row, which makes its predicate the only thing between + the caller and _an arbitrary record_. When the predicate is missing the result is + not `null` — it is the object's **first row**: a real, plausible-looking record + with nothing to do with the request, which the `if (!row)` check every call site + already has cannot catch, and which then propagates into whatever is computed + next. Reported downstream: line items defaulting their price from the first + product in the catalog rather than the selected one, and "is this deal already + closed?" answered against an unrelated record while the write that followed + correctly targeted the intended id. A throw would have been caught in + development; a `null` would have been caught by the null-check. A valid-looking + wrong record defeats both. + + **Breaking — `findOne` now refuses a query that selects nothing in particular.** + + FROM → TO: + + | Was | Now write | Meaning | + | ----------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------ | + | `findOne(o)`, `findOne(o, {})`, `findOne(o, { where: {} })` | `findOne(o, { where: … })` | the record matching this predicate | + | | `findOne(o, { search: 'Acme' })` | the record this search finds | + | | `findOne(o, { orderBy: [{ field: 'created_at', order: 'desc' }] })` | the FIRST record in this order — the newest | + | | `find(o, { limit: 1 })` | any row will genuinely do, said at the call site | + + One-line fix: add the `where` you meant, or `orderBy` if you meant "the newest + one", or switch to `find(o, { limit: 1 })` if any row will do. The error names + all four. `find` and `count` are unchanged — returning or counting every row is + an honest answer; only `findOne`'s implicit "just one of them" turns a missing + predicate into a confidently wrong record. The guard reads the CALLER's + predicate, before RLS/sharing middleware injects its own: a tenant filter + narrows which rows are visible, it does not make "whichever comes first" + something the caller asked for. + + **Two silent drops that produced the same wrong record are fixed with it.** + + - **`findOne({ search })` applies the search.** The ADR-0061 `search` → + cross-field `$contains` expansion lived inline in `find` and nowhere else, + while `find` and `findOne` are checked against the SAME legal-key set — so + `search` passed the gate, rode onto the AST, and reached a driver. No driver + reads `ast.search`. The read therefore ran with no predicate at all and + `limit: 1` did the rest. The expansion is now one method both call. + - **`MongoDBDriver.findOne` applies `orderBy`, `fields` and `offset`.** It + translated `query.where` and dropped the rest, so `findOne({ orderBy })` did + not return the newest record — it returned whichever document the scan reached + first. `find` and `_findStream` in the same driver had always handled all + three. This one matters beyond Mongo: the guard above tells an unpredicated + caller to reach for `orderBy`, and an escape hatch one backend ignores is not + an escape hatch. No ordering is IMPOSED when the caller supplies none — both + drivers keep that carve-out (#4363), and `SqlDriver`'s comment about Mongo + "never sorting" is corrected, since it cited the dropped parameter as + agreement. + + **And a gate so the class does not come back.** A drift pin walks + `ENGINE_OPTION_KEY_SETS.findOne` and requires each declared key to have an + observable effect — on the AST the driver receives, on the driver options, or in + an explicit "not executed, and here is why" entry (only `limit`, which the + contract's `limit: 1` overrides). `search` sat declared-but-unexecuted through + two rounds of hardening because nothing asked that question. + + Together with #4346 (`filter` → `where` folds on every entry point) and #4400 + (unknown option keys throw), a read parameter the engine does not execute now + fails at the call site instead of quietly changing the answer. + +- 0f9faa2: The liveness gate now governs every registered metadata type (#4487) + + `GOVERNED` in `check-liveness.mts` was a hand-maintained list, and nothing ever + compared it against the registry it claims to cover. It governed **15 of 25** + registered metadata types while reporting itself complete. A type in the other + ten was authorable — served by `/api/v1/meta/types/:type`, editable in Studio — + and was never asked who reads its properties, so an inert key on it was + invisible to CI and its silence read as success. + + `datasource` was in that state for its entire life. #4410, #4465 and #4481 found + six inert keys on it **by hand**, two of them security-shaped: `schemaMode` was + dropped between the record and the connection spec, so a database ObjectStack + must never run DDL against was constructed as `managed`; `ssl` stopped at the + record, so a TLS block with a CA certificate in it configured nothing while + looking identical to one that worked. + + **The gate is now answerable to the registry.** Every registered type must be in + `GOVERNED` or in `PENDING_GOVERNANCE` with a reason and an issue. Registering a + type and forgetting the ledger fails CI with the entry to write. The reverse rots + too, so it also fails: a `PENDING_GOVERNANCE` row for a type that has since been + governed claims a debt that no longer exists. + + **`datasource` is now governed** — `liveness/datasource.json`, all 43 properties + classified with evidence. The result is the highest dead ratio of any governed + type: **20 of 43 have no runtime consumer.** + + | Dead cluster | Why | + | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | `capabilities.*` (11) | The engine gates pushdown on the runtime driver's own `supports.*` object — `autonumber`, `batchSchemaSync`, `queryDateGranularity` — a different mechanism whose vocabulary does not overlap this block at all. `having-filter.ts` says it outright: "SQL pushdown can come later behind a driver capability flag." | + | `healthCheck.*` (3) | Nothing schedules a datasource probe. Liveness is checked on demand through the driver handle's `ping()`. | + | `retryPolicy.*` (4) | No connect or query path retries. | + | `external.label`, `external.requirePermission` | No reader. | + + **One correction ships with this**, and it is the reason the audit was worth + doing rather than a bookkeeping exercise. `capabilities.readOnly` reads as a + safety switch and gates nothing — and **two shipped prescriptions pointed + authors at it**: the `externalSettingsUnknownKeyError` guidance in + `datasource.zod.ts` ("or `capabilities.readOnly` to describe the driver") and + the #4465 changeset's relocation table. Both now name `external.allowWrites: +false`, which is the write gate the ObjectQL engine actually checks. An author + who followed the old advice believed they had marked a datasource non-writable + and had not. The v17 release notes carried a matching false claim — that an + unregistered `capabilities` key made the engine stop pushing work down to the + driver — corrected in the same change. + + Two traps worth naming, because both nearly produced a wrong verdict here: + + - **`healthCheck` and `retryPolicy` are name collisions.** A bare grep for + either returns plenty of live readers — the plugin health monitor, `hook`, + `job` — none of which is this type. `hook.retryPolicy` even spells its delay + `backoffMs` where this declares `baseDelayMs`; the shape mismatch is the tell + that nothing reads both. + - **objectui's `DatasourcePreview` renders `pool`, `ssl`, `retryPolicy` and + `healthCheck` as panels**, and is cited as evidence for none of them. That is + the standing rule in `liveness/README.md`, and #4481 is the fresh precedent: + the only "consumer" of `readReplicas` in either repo was a preview pill. + + The CLI advisory lint picks the ledger up automatically, so `os compile` now + warns an author who sets any of the 20. That needed one line beyond the ledger — + `datasource` had to be added to `TYPE_COLLECTIONS`. Coverage grows by marking + entries `authorWarn` only _within_ a type the lint already walks; a newly + governed type needs its collection registered or its ledger warns nobody. + + Nine types remain ungoverned and are now enumerated rather than implied: + `app`, `book`, `doc`, `email_template`, `job`, `mapping`, `seed`, `translation`, + `validation` (#4488). + +- 061406d: The protection-envelope invariant test was hollow — it silently skipped 24 of 25 registered types. Fixed, and it immediately found 8 undeclared envelopes instead of 1. + + The check shipped in the previous change asserted two things about every registered metadata type: that it does not _reject_ the ADR-0010 envelope its loader stamps (the hard-422 case), and that it does not _strip_ it (the silent-loss case). The reject half worked — it found `hook` and `datasource` on its first run. + + The strip half did not. It probed each schema with one generic body and asked whether `_packageId` survived; a type whose required fields that body did not satisfy failed for unrelated reasons and the assertion returned early. **24 of the 25 types took that early return.** Only `field` was ever actually checked, and the suite reported green. + + That is the campaign's own subject matter — a success signal covering an omission — reproduced inside the instrument built to detect it, one change after the ledger recorded the same lesson about the strictness gate's non-recursive directory walk. A check that skips is indistinguishable from a check that passes. + + **The declaration side is now structural.** It walks the schema — unwrapping `lazy` / `pipe` / `optional` / `default` and expanding unions — and asks whether any resolved object shape declares the key. That answer does not require constructing a valid instance, so it cannot skip. Two guards keep it honest: a type whose shape the walker cannot resolve is a hard failure (the walker going quiet is exactly when this test would otherwise stop covering something), and the debt list carries a reverse pin that fails when an entry is fixed, so the list cannot outlive the debt it tracks. + + **What it found:** 8 registered types do not declare the envelope, not 1 — `action`, `book`, `field`, `job`, `mapping`, `page`, `translation`, `validation`. `job` and `book` are closed here, leaving 6 on the list. Each is protection metadata lost on every round-trip today, and a hard 422 the day its schema is closed. + +- 9c93465: `check:react-conformance` → `check:react-declaration-parity` — the gate compares two declarations, and said it compared a declaration to an implementation. + + Its header opened by claiming it "confirms the objectui components **ACTUALLY implement** + the props the spec protocol declares". It never could. Both sides of its diff are + declarations: the spec zod schema's props on the left, and on the right the `inputs` the + objectui _registry config_ declares — copied verbatim into `sdui.manifest.json` by + `manifestFromConfigs`. No renderer appears anywhere in the chain. So a prop **both sides + declare and nothing reads** is, to this gate, perfect agreement. + + That is not hypothetical. #4413's four blocks (`record:details` / `record:highlights` / + `record:related_list` / `record:path`) published `objectName`/`recordId` that no renderer + read, rendered a "bind a record to preview" placeholder on a `kind:'react'` page, and sat + behind `{ "frontendOnly": [], "missing": false }` in the committed baseline for the whole + life of the defect. A human reading the objectui renderers found it. A gate reporting + green on a promise it cannot keep is worse than no gate — without one, someone checks by + hand. + + Prime Directive #10 (declared ≠ enforced), landing on the thing whose job is to catch it. + Same shape as #1475's "spec declares 9 validation rules, the executor honors 3". + + - **Renamed to what it does**, name and header together, because the name was load-bearing + in the misreading: `check-react-blocks-declaration-parity.ts`, + `react-declaration-parity.baseline.json`, and `frontendOnly` → `registryOnly` in the + baseline ("the registry _declared_ it", not "the frontend _implements_ it"). + - **The scope caveat is emitted on every run, clean ones included.** Whoever forms a + belief about this gate is reading a CI log, not a source header. + - **It actually gates now.** `gen-sdui-manifest.sh` ran it without `--strict` and swallowed + the exit code behind a `⚠`, so even the divergence it _could_ see was recorded and never + stopped (#4472 secondary finding 1). The ratchet fires only on divergence new since the + accepted baseline, so a failure is always a deliberate registry change. + - **The claim is pinned by a test.** `check-react-blocks-declaration-parity.test.ts` + asserts both directions of what the gate can see, that the caveat rides along, and that + the implementation claim does not come back. + + What it sees is unchanged and still worth having — `spec-only` (palette gap, soft), + `registry-only` (undocumented extension, ratcheted), `missing` (not registered / not + public). Exactly one class is invisible: both sides declare it, nothing reads it. + + Evidence about the render path has to come from the render path, which is objectui's side. + `public-block-binding-reach.test.tsx` there mounts every public block declaring an + `objectName` under a recording `dataSource` and asserts the binding reaches it; its first + run separated five bound blocks from three unbound and surfaced two real defects of the + same shape (objectui#3144) — the confirmation this evidence was never obtainable here. + ADR-0082 carries the addendum; the 2026-06 audit carries a correction banner over the + assumption that carried the mistake ("the component reads its full config from the spec + schema at render" — an expectation, never measured). + +- 63b33e6: A stored reference value that is an embedded record is no longer a valid id + (#4455). + + `os migrate value-shapes` is the evidence half of the ADR-0104 D1 per-deployment + gate, and its own header names the case it exists for: "a `location` stored as + `{latitude, longitude}` **or a `lookup` holding an expanded record object**". The + second case was not detected. `ReferenceIdValueSchema` was + `z.string().min(1)`, and in a SQL deployment a legacy embedded reference reaches + storage as JSON _text_ in a TEXT column — a non-empty string. So a deployment + carrying exactly the values the gate exists to find ran the scan, was told it was + clean, and closed the gate with `--apply`; because the scan deliberately imports + the write-path predicate, the write path was equally blind and the value survived + future writes too. + + `ReferenceIdValueSchema` now rejects a value whose first non-space character is + `{` or `[`, in both the stored and the expanded form (`$expand` produces an + object, never its serialization). + + The rejection is deliberately narrower than the issue's first suggestion. Its + file sibling `FileReferenceIdValueSchema` can bound its charset because a + `sys_file` id is minted by the platform and by nothing else; a reference id is + whatever the target object's primary key holds, including an external key an + ADR-0015 federated datasource supplies. So this rejects the shape that is + provably not an id (`{"id":"acc_1","name":"embedded"}`) and leaves the id + alphabet to the object that owns it — `CB0-2026-0001`, `SFDC:001xx…` and + `ops/eu-west/tenant-7` all remain valid. Widening it further needs evidence about + real external keys, not a guess. + + Reaches authors through the ADR-0104 warn-first path (a `[value-shape]` log line) + until a deployment opts into strict, so nothing starts rejecting writes on + upgrade — but the scan now counts these values, and a deployment holding them can + no longer close the gate. + +- 5a84d41: fix(automation): `resume` enforces the suspended screen's declared field contract (#4477) + + A `screen` node's `config.fields` is a complete input contract — the author + declares the keys, their `required`-ness, and (via `visibleWhen`) when a field + is even asked for. The RENDER half honoured all of it: the paused result and + `GET …/runs/:runId/screen` carry `required` and `visibleWhen` intact. There was + no VALIDATION half — `POST …/runs/:runId/resume` folded whatever bag it was + handed straight into the flow variables, so a caller that skipped the dialog and + posted here directly was unconstrained by every `required` the author wrote. + Missing required fields, and keys the screen never declared, all completed the + run with `success: true`. + + Screen flows are the one place where the declared field contract is the ONLY + contract — no object schema sits behind a screen node to catch a bad bag + downstream. The platform already enforces the analogous contract everywhere else + this seam appears: action params (ADR-0104 D2), record writes (ADR-0113), + approval `decisionOutputs` (#3447). This is that rule for screen resume, built in + the same shape. + + `resume` now refuses a non-conforming submission with the new + `AutomationResult.code` `'INVALID_SCREEN_INPUT'` (a transport maps it to **400**, + as the automation domain route now does) and an `Invalid screen input: …` message + that names each violation and lists the declared field names. The refusal happens + BEFORE the suspension is consumed, so the pause stays live and the legitimate + submission still lands. + + `visibleWhen` is evaluated against the SUBMITTED values first (layered over the + run's variable snapshot), so a hidden field's `required` never fires — enforcing + it would dead-end the run at a field the user was never shown, which is #3528 + reproduced server-side. A predicate that cannot be evaluated is logged and + treated as hidden rather than visible: the client decides what the user saw, and + a broken predicate is not evidence a field was on screen. + + Scope, deliberately narrow — three shapes keep the historical pass-through: + + - an **object-form** screen (`kind: 'object-form'`), whose `fields` is empty by + construction because the client renders the object's own form and the write + path enforces that object's `required` fields itself; + - a **message-only** screen (`waitForInput: true`, no fields), which declares no + keys and so constrains none — the same pass-through `enforceActionParams` + gives a param-less action; + - `signal.output`, the node-OUTPUT namespace, which belongs to the approval-style + resume envelope rather than to the screen's collected-values channel. + +- 04f1182: docs(spec): SystemFieldName says which columns are actually injected (#4430) + + `SystemFieldName` presents itself as the canonical protocol-level names for + system fields, but it was neither the injected set nor a complete one — and it + had the most load-bearing entry backwards. `TENANT_ID` was documented as + "Tenant isolation key" while the column the registry actually provisions is + `organization_id`, which had no constant at all. Nor did `created_by` / + `updated_by`, the other half of the audit-provenance family. Two of the seven + entries (`user_id`, `deleted_at`) are not injected either, with nothing in the + table saying so. + + Consumers hand-copying a system-field list read the table as the injection set + and drifted accordingly. cloud#982 found three such copies in one package + carrying `tenant_id`, `org_id` and `space` between them — three spellings no + injection site produces — and cloud#979 was one of those copies claiming a + business field named `owner`, so every seeded row of a user's app shipped its + 负责人 column blank. + + **Additive only. No entry removed, no value changed**, so existing + `SystemFieldName.X` references are unaffected. + + - Adds `ORGANIZATION_ID`, `CREATED_BY` and `UPDATED_BY` — the three injected + columns the table was missing. + - Records per entry whether open-core actually injects it, so the legacy + (`tenant_id`, stamped from the session's _organization_ id only on an object + that declares it) and authored (`user_id`) names can no longer be mistaken + for provisioned ones. + - States in the module doc that this is a NAME registry, not the injected set. + `applySystemFields` decides that per object from `ownership` / `tenancy` / + `systemFields`, so the same name is a system column on one object and + business data on the next. A consumer asking "is this field system-managed on + THIS object" branches on `Field.system` — already published for exactly that + purpose, and now pointed at from here. + + `@objectstack/lint`'s `SYSTEM_FIELDS` is unchanged in content: it unions this + table with `FIELD_GROUP_SYSTEM_FIELDS`, which already carried all three added + names. + ## 17.0.0-rc.1 ### Major Changes @@ -1471,8 +3754,8 @@ schedule }`), not on the flow. being a hand-listed union and a bare `string` respectively and reference the catalog, so the three cannot drift apart. - Lowercase is deliberate, not an oversight against ADR-0112's SCREAMING_SNAKE: a - top-level code names the condition the _request_ hit, while a field-level code + Lowercase is deliberate, not an oversight against ADR-0112's SCREAMING*SNAKE: a + top-level code names the condition the \_request* hit, while a field-level code names the _constraint_ the value violated — and constraints are declared in the metadata's own snake_case, so `max_length` the code and `max_length: 50` the property are the same word on purpose. diff --git a/packages/spec/package.json b/packages/spec/package.json index 0c2e663482..fe973e3c43 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/spec", - "version": "17.0.0-rc.1", + "version": "17.0.0-rc.2", "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 eb6cdcc225..f56a7a4e3c 100644 --- a/packages/triggers/trigger-api/CHANGELOG.md +++ b/packages/triggers/trigger-api/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/trigger-api +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/triggers/trigger-api/package.json b/packages/triggers/trigger-api/package.json index 4a89263ab8..7406d69163 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.1", + "version": "17.0.0-rc.2", "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 03895cf920..860e2b52f1 100644 --- a/packages/triggers/trigger-record-change/CHANGELOG.md +++ b/packages/triggers/trigger-record-change/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/plugin-trigger-record-change +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/triggers/trigger-record-change/package.json b/packages/triggers/trigger-record-change/package.json index bfa23a801f..3811d191c9 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.1", + "version": "17.0.0-rc.2", "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 069b9d7c64..9d8f3dc650 100644 --- a/packages/triggers/trigger-schedule/CHANGELOG.md +++ b/packages/triggers/trigger-schedule/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/plugin-trigger-schedule +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/triggers/trigger-schedule/package.json b/packages/triggers/trigger-schedule/package.json index 1ab493e71d..e295e88399 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.1", + "version": "17.0.0-rc.2", "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 3a4c7dbc8c..4ed90edd45 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,69 @@ # @objectstack/types +## 17.0.0-rc.2 + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [e6b1b69] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [04f1182] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Minor Changes diff --git a/packages/types/package.json b/packages/types/package.json index 326d52cffb..8453906b38 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/types", - "version": "17.0.0-rc.1", + "version": "17.0.0-rc.2", "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 2877d25465..aa84eade9b 100644 --- a/packages/verify/CHANGELOG.md +++ b/packages/verify/CHANGELOG.md @@ -1,5 +1,135 @@ # @objectstack/verify +## 17.0.0-rc.2 + +### Minor Changes + +- c2a1134: fix(verify): stop the harness pinning `suspendedRunStore: 'memory'` (#4470) + + `bootStack` hardcoded `suspendedRunStore: 'memory'` when it registered + `@objectstack/service-automation`. That made the DB-backed suspended-run store + **structurally unreachable** from every dogfood/e2e fixture — not under-tested, + untestable. The coverage map had a clean seam nothing crossed: + + - unit tests covered ENGINE-side persistence (`suspended-run-store.test.ts` + drives suspend → restart → resume against a fake table); + - e2e covered the BUSINESS chain (approvals), but single-process and wholly in + memory; + - the ASSEMBLY between them — is `sys_automation_run` registered, is its table + created, is the store actually attached to the engine — was covered by + neither. + + #4420 grew in precisely that seam: the store hung off a table that was never + created, every write failed into a `warn` nobody read, the pause reported + success, and the run died at the next restart. #4460 added assembly unit tests; + this makes the e2e half possible. + + The harness now boots the plugin's own `'auto'` default — the same wiring + `objectstack dev` / `serve` get — so fixtures exercise the real assembly. Two + new knobs: + + - `automation` accepts `{ suspendedRunStore: 'auto' | 'memory' }` as well as + `true`, so a fixture that wants the old in-memory behaviour asks for it + explicitly rather than getting it by default. + - `databaseFile` backs the in-process SQLite database with a file instead of + `:memory:`, so state can outlive a kernel. + + Answering the question the issue raised — was `'memory'` pinned for speed or + because persistence could not run there? **Speed/simplicity.** The durable path + works in this harness: the accompanying dogfood proof boots with it, and the + whole existing dogfood suite passes on it unchanged (38 files, 239 tests). Note + `databaseFile` does not yet deliver a true cold boot: a second `bootStack` over + the same file reads a database whose tables exist but whose rows are gone — + ordinary records do not survive it either, so it is a harness/driver persistence + gap rather than anything to do with suspended runs, and it is filed as #4518. + +### Patch Changes + +- Updated dependencies [80334c7] +- Updated dependencies [a7163ea] +- Updated dependencies [e6e9379] +- Updated dependencies [257d97a] +- Updated dependencies [e6b1b69] +- Updated dependencies [328ccc5] +- Updated dependencies [2826d1e] +- Updated dependencies [5a84d41] +- Updated dependencies [20b1a9e] +- Updated dependencies [4820f55] +- Updated dependencies [462d9c4] +- Updated dependencies [5b843fb] +- Updated dependencies [b4487aa] +- Updated dependencies [67bf2e2] +- Updated dependencies [6117f7b] +- Updated dependencies [cdf4d9a] +- Updated dependencies [63b33e6] +- Updated dependencies [9ca2d85] +- Updated dependencies [a52e2ef] +- Updated dependencies [5293114] +- Updated dependencies [ff17642] +- Updated dependencies [20bc357] +- Updated dependencies [2382580] +- Updated dependencies [3c7bcc0] +- Updated dependencies [4b6cac7] +- Updated dependencies [7631964] +- Updated dependencies [ac471a0] +- Updated dependencies [60ae58e] +- Updated dependencies [ce92674] +- Updated dependencies [07a4e26] +- Updated dependencies [ec975f1] +- Updated dependencies [68c02c2] +- Updated dependencies [eb4204b] +- Updated dependencies [4f13be2] +- Updated dependencies [ce92674] +- Updated dependencies [cf2c9b7] +- Updated dependencies [0f9faa2] +- Updated dependencies [7cf42fe] +- Updated dependencies [8aacf94] +- Updated dependencies [a2cd18a] +- Updated dependencies [4638aaa] +- Updated dependencies [0222d3c] +- Updated dependencies [7bba90b] +- Updated dependencies [061406d] +- Updated dependencies [9c93465] +- Updated dependencies [ebb209c] +- Updated dependencies [63b33e6] +- Updated dependencies [2a44c1d] +- Updated dependencies [1ee48bc] +- Updated dependencies [f3141d8] +- Updated dependencies [5a84d41] +- Updated dependencies [fd3013a] +- Updated dependencies [ba5ff2f] +- Updated dependencies [304423e] +- Updated dependencies [e5e7ee0] +- Updated dependencies [800bdb0] +- Updated dependencies [26bb053] +- Updated dependencies [04f1182] +- Updated dependencies [c03108c] +- Updated dependencies [d6bd5a1] +- Updated dependencies [38f7e4f] +- Updated dependencies [c57f3cf] +- Updated dependencies [97faca3] +- Updated dependencies [ad5fe25] +- Updated dependencies [ea90179] +- Updated dependencies [ce92674] +- Updated dependencies [5ef0b5b] +- Updated dependencies [48fbacb] +- Updated dependencies [355e951] +- Updated dependencies [dadb43f] + - @objectstack/spec@17.0.0-rc.2 + - @objectstack/objectql@17.0.0-rc.2 + - @objectstack/plugin-security@17.0.0-rc.2 + - @objectstack/service-analytics@17.0.0-rc.2 + - @objectstack/service-automation@17.0.0-rc.2 + - @objectstack/rest@17.0.0-rc.2 + - @objectstack/runtime@17.0.0-rc.2 + - @objectstack/service-datasource@17.0.0-rc.2 + - @objectstack/plugin-sharing@17.0.0-rc.2 + - @objectstack/platform-objects@17.0.0-rc.2 + - @objectstack/plugin-auth@17.0.0-rc.2 + - @objectstack/core@17.0.0-rc.2 + - @objectstack/plugin-hono-server@17.0.0-rc.2 + - @objectstack/service-settings@17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/verify/package.json b/packages/verify/package.json index 468e1ad038..2430b3deef 100644 --- a/packages/verify/package.json +++ b/packages/verify/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/verify", - "version": "17.0.0-rc.1", + "version": "17.0.0-rc.2", "license": "Apache-2.0", "description": "Boot any ObjectStack app in-process and verify it through the real HTTP stack — auto-derived CRUD round-trip fidelity plus the cross-owner RLS invariant. Catches runtime regressions that static checks miss.", "type": "module", diff --git a/packages/vscode-objectstack/CHANGELOG.md b/packages/vscode-objectstack/CHANGELOG.md index fb0f0de3f9..6b4ccf50be 100644 --- a/packages/vscode-objectstack/CHANGELOG.md +++ b/packages/vscode-objectstack/CHANGELOG.md @@ -1,5 +1,7 @@ # objectstack-vscode +## 17.0.0-rc.2 + ## 17.0.0-rc.1 ### Patch Changes diff --git a/packages/vscode-objectstack/package.json b/packages/vscode-objectstack/package.json index cdae7a02ac..074133e3ef 100644 --- a/packages/vscode-objectstack/package.json +++ b/packages/vscode-objectstack/package.json @@ -2,7 +2,7 @@ "name": "objectstack-vscode", "displayName": "ObjectStack", "description": "ObjectStack Protocol — Autocomplete, validation, and inline diagnostics for .object.ts, .view.ts, and objectstack.config.ts files", - "version": "17.0.0-rc.1", + "version": "17.0.0-rc.2", "publisher": "objectstack", "license": "Apache-2.0", "repository": {