@cosmicdrift/kumiko-framework 0.333.0 → 0.335.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/auth-routes.js +19 -4
- package/dist/api/client-ip.js +3 -6
- package/dist/api/extra-route.js +3 -2
- package/dist/api/pii-leak-guard.js +7 -4
- package/dist/api/redis-sse-broker.d.ts +2 -0
- package/dist/api/redis-sse-broker.js +6 -7
- package/dist/api/routes.js +16 -17
- package/dist/api/server.js +14 -7
- package/dist/api/sse-broker.d.ts +3 -0
- package/dist/api/sse-broker.js +14 -0
- package/dist/api/sse-route.d.ts +1 -1
- package/dist/api/sse-route.js +39 -6
- package/dist/bun-db/connection.js +0 -2
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +1 -0
- package/dist/bun-db/query.js +15 -4
- package/dist/crypto/kek-source.js +40 -36
- package/dist/crypto/kms-adapter.js +2 -2
- package/dist/db/apply-entity-event.js +8 -2
- package/dist/db/blind-index-cleanup.js +11 -1
- package/dist/db/eagerload.js +2 -1
- package/dist/db/entity-table-meta.js +5 -3
- package/dist/db/event-store-executor-context.js +13 -19
- package/dist/db/event-store-executor-read.js +39 -36
- package/dist/db/event-store-executor-write.js +15 -17
- package/dist/db/migrate-generator.js +27 -28
- package/dist/db/pg-error.js +9 -5
- package/dist/db/queries/backfill-pii.js +15 -5
- package/dist/db/queries/shadow-swap.js +5 -4
- package/dist/db/table-builder.d.ts +1 -0
- package/dist/db/table-builder.js +7 -15
- package/dist/db/tenant-db.js +18 -2
- package/dist/derivatives/variant-key.js +3 -1
- package/dist/engine/boot-validator/access-declarations.js +12 -3
- package/dist/engine/boot-validator/config-deps.js +13 -0
- package/dist/engine/boot-validator/entity-handler.js +13 -1
- package/dist/engine/boot-validator/entity-list-screens.d.ts +1 -0
- package/dist/engine/boot-validator/entity-list-screens.js +2 -2
- package/dist/engine/boot-validator/index.js +6 -6
- package/dist/engine/boot-validator/nav.d.ts +2 -1
- package/dist/engine/boot-validator/nav.js +10 -20
- package/dist/engine/boot-validator/ownership.d.ts +1 -1
- package/dist/engine/boot-validator/ownership.js +2 -1
- package/dist/engine/boot-validator/pii-retention.d.ts +2 -0
- package/dist/engine/boot-validator/pii-retention.js +44 -4
- package/dist/engine/boot-validator/projection-list-screens.js +8 -0
- package/dist/engine/boot-validator/query-output-columns.js +41 -5
- package/dist/engine/boot-validator/screens.d.ts +2 -2
- package/dist/engine/boot-validator/screens.js +138 -36
- package/dist/engine/boot-validator/transfer-graph.js +9 -6
- package/dist/engine/boot-validator/url-prefill-fields.js +8 -6
- package/dist/engine/boot-validator/zod-shape.js +17 -4
- package/dist/engine/build-app-schema.js +65 -19
- package/dist/engine/build-config-feature-schema.js +106 -22
- package/dist/engine/changeset-changes.js +24 -10
- package/dist/engine/config-helpers.d.ts +2 -2
- package/dist/engine/entity-handlers.js +7 -2
- package/dist/engine/feature-ast/extractors/events.js +11 -8
- package/dist/engine/feature-ast/extractors/handlers.js +38 -16
- package/dist/engine/feature-ast/extractors/resolve-import.d.ts +7 -2
- package/dist/engine/feature-ast/extractors/resolve-import.js +42 -15
- package/dist/engine/feature-ast/parse.d.ts +1 -0
- package/dist/engine/feature-ast/parse.js +18 -3
- package/dist/engine/feature-ast/patch.js +80 -61
- package/dist/engine/feature-ast/patcher.d.ts +1 -0
- package/dist/engine/feature-ast/patcher.js +2 -1
- package/dist/engine/feature-ast/render.js +1 -1
- package/dist/engine/feature-entity-handlers.js +22 -0
- package/dist/engine/field-access.d.ts +2 -0
- package/dist/engine/field-access.js +40 -0
- package/dist/engine/index.d.ts +5 -4
- package/dist/engine/index.js +4 -3
- package/dist/engine/ownership.d.ts +1 -0
- package/dist/engine/ownership.js +15 -2
- package/dist/engine/pattern-library/mixed-schemas.js +15 -29
- package/dist/engine/pattern-library/shared-fields.d.ts +2 -0
- package/dist/engine/pattern-library/shared-fields.js +17 -0
- package/dist/engine/registry-facade.js +3 -0
- package/dist/engine/role-assignment.d.ts +7 -1
- package/dist/engine/role-assignment.js +27 -7
- package/dist/engine/schema-builder.d.ts +2 -0
- package/dist/engine/schema-builder.js +20 -3
- package/dist/engine/screen-helpers.js +15 -4
- package/dist/engine/steps/_event-match.js +22 -3
- package/dist/engine/steps/_read-source.js +8 -0
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/engine/where-rule-lint.js +22 -4
- package/dist/env/index.js +21 -1
- package/dist/errors/reasons.d.ts +0 -1
- package/dist/errors/reasons.js +2 -5
- package/dist/event-store/event-store.js +6 -1
- package/dist/event-store/provenance-append.js +4 -1
- package/dist/files/file-ref-entity.d.ts +6 -0
- package/dist/files/file-ref-table.d.ts +6 -0
- package/dist/files/file-routes.d.ts +0 -1
- package/dist/files/file-routes.js +22 -7
- package/dist/files/local-provider.js +13 -10
- package/dist/files/provider-resolver.js +20 -1
- package/dist/files/types.js +16 -1
- package/dist/i18n/request-locale.d.ts +3 -2
- package/dist/i18n/request-locale.js +16 -6
- package/dist/i18n/required-surface-keys.js +28 -35
- package/dist/i18n/settings-hub-keys.js +21 -4
- package/dist/jobs/index.d.ts +2 -2
- package/dist/jobs/index.js +1 -1
- package/dist/jobs/job-runner.d.ts +2 -0
- package/dist/jobs/job-runner.js +150 -59
- package/dist/migrations/index.d.ts +1 -1
- package/dist/migrations/index.js +1 -1
- package/dist/migrations/pending-rebuilds.d.ts +4 -0
- package/dist/migrations/pending-rebuilds.js +23 -1
- package/dist/observability/escape-hatch-report.d.ts +1 -0
- package/dist/observability/escape-hatch-report.js +3 -0
- package/dist/observability/index.d.ts +1 -0
- package/dist/observability/index.js +1 -0
- package/dist/pipeline/dispatch-batch.js +8 -3
- package/dist/pipeline/dispatch-query.d.ts +1 -1
- package/dist/pipeline/dispatch-query.js +2 -2
- package/dist/pipeline/dispatch-shared.d.ts +2 -3
- package/dist/pipeline/dispatch-shared.js +14 -9
- package/dist/pipeline/dispatch-stream.js +2 -5
- package/dist/pipeline/dispatch-write.js +27 -7
- package/dist/pipeline/dispatcher-utils.js +4 -1
- package/dist/pipeline/dispatcher.js +1 -2
- package/dist/pipeline/event-dispatcher-admin.js +12 -2
- package/dist/pipeline/event-dispatcher.d.ts +1 -0
- package/dist/pipeline/event-dispatcher.js +26 -11
- package/dist/pipeline/index.d.ts +1 -1
- package/dist/pipeline/index.js +1 -1
- package/dist/pipeline/member-read-only-transaction.js +1 -14
- package/dist/pipeline/msp-rebuild.d.ts +1 -0
- package/dist/pipeline/msp-rebuild.js +1 -0
- package/dist/pipeline/system-hooks.d.ts +1 -1
- package/dist/pipeline/system-hooks.js +9 -7
- package/dist/pipeline/tenant-timezone-cache.d.ts +2 -0
- package/dist/pipeline/tenant-timezone-cache.js +20 -6
- package/dist/pipeline/write-result-masking.d.ts +6 -0
- package/dist/pipeline/write-result-masking.js +36 -0
- package/dist/redis/pubsub-signal.d.ts +2 -0
- package/dist/redis/pubsub-signal.js +2 -2
- package/dist/schema-cli.js +4 -14
- package/dist/search/meilisearch-adapter.js +17 -6
- package/dist/search/purge-subject.js +8 -3
- package/dist/stack/drain-jobs.js +24 -12
- package/dist/stack/table-helpers.js +10 -8
- package/dist/testing/closed-connection-error.js +2 -3
- package/dist/testing/shared-entities.d.ts +10 -0
- package/dist/testing/wait-for.js +1 -0
- package/dist/ui-types/index.d.ts +2 -2
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-cli.d.ts +1 -0
- package/dist/upgrade-cli.js +36 -15
- package/package.json +4 -4
- package/src/changes.json +218 -0
- package/src/scripts/codemod/migrate-cross-tenant.ts +14 -2
- package/src/scripts/codemod/migrate-db-raw.ts +16 -8
- package/src/scripts/codemod/migrate-open-to-all.ts +7 -1
- package/src/scripts/codemod/pii-personal-migration.ts +56 -19
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +83 -26
- package/dist/scripts/codemod/crypto-shredding-testing-move.d.ts +0 -2
- package/dist/scripts/codemod/crypto-shredding-testing-move.js +0 -109
- package/dist/scripts/codemod/migrate-cross-tenant.d.ts +0 -19
- package/dist/scripts/codemod/migrate-cross-tenant.js +0 -266
- package/dist/scripts/codemod/migrate-db-raw.d.ts +0 -23
- package/dist/scripts/codemod/migrate-db-raw.js +0 -345
- package/dist/scripts/codemod/migrate-open-to-all.d.ts +0 -22
- package/dist/scripts/codemod/migrate-open-to-all.js +0 -194
- package/dist/scripts/codemod/pii-personal-migration.d.ts +0 -11
- package/dist/scripts/codemod/pii-personal-migration.js +0 -615
- package/dist/scripts/codemod/unprocessable-error-details-reason.d.ts +0 -2
- package/dist/scripts/codemod/unprocessable-error-details-reason.js +0 -235
package/src/changes.json
CHANGED
|
@@ -1,4 +1,222 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.335.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Agent tool manifests no longer offer the caller-chosen id on create tools",
|
|
6
|
+
"detail": "Agent tool manifests no longer offer the caller-chosen `id` on create tools, since agent dispatch never runs as a system identity and the value was silently dropped. Combobox options now carry a value-based `data-testid`."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.335.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "Stricter boot checks for list outputSchema envelopes, detail section fields, reference facet list handlers, metric navigate, dashboard screen panels, refEntity columns and checkbox columns",
|
|
12
|
+
"detail": "Stricter boot checks for screens and handlers; apps that carried a latent typo or misconfiguration now fail at boot instead of rendering an empty cell or doing nothing at runtime.\n- A plain (non-paged) query handler whose `outputSchema` declares the row shape instead of `{ rows, nextCursor }` now fails when a projectionList, relatedList, expandableRow or dashboard list panel uses it.\n- `projectionDetail` layout section `fields` are checked against the query's `outputSchema`.\n- A `reference` list facet requires the target's list query handler (`<feature>:query:<entity>:list`, or its central lookup override) to be registered.\n- `MetricSpec.navigate.screen`/`entity` are validated even without `tab`; `navigate.entity` needs `entityId`.\n- A dashboard `screen` panel must not embed an `actionForm`/`secretMint` with `redirect` or `cancelTarget`.\n- `refEntity` on entityList columns and dashboard list-panel columns must resolve to a registered entity.\n- `multiSelect` `display: \"checkboxes\"` rejects `columns` outside 1-4.\n- Personal-data detection for `openToAll` write handlers now also looks inside `z.map`, `z.set`, `z.promise` and `.catchall()` schemas.\n- Fixes false boot failures for cross-feature rowActions / `listScreenId` written as a fully-qualified screen QN, and for where-rules that use a bare outer column next to a qualified subquery.",
|
|
13
|
+
"migration": "Boot now fails for configurations that previously rendered empty or did nothing. Fix the named screen or field: wrap a list handler's outputSchema in { rows, nextCursor }, correct detail section field names, register the entity list handler (defineEntityListHandler) behind a reference facet, add entityId to a metric navigate.entity, drop redirect/cancelTarget on forms embedded in a dashboard, and use a registered entity for refEntity."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"version": "0.335.0",
|
|
17
|
+
"type": "fix",
|
|
18
|
+
"title": "Encrypted-at-rest text config keys derive writeOnly settings fields; boot validator allows writeOnly on configEdit only for those keys"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.335.0",
|
|
22
|
+
"type": "fix",
|
|
23
|
+
"title": "migrate-db-raw codemod lists own-tenant calls with a spread after tenantId for manual review instead of rewriting them",
|
|
24
|
+
"detail": "The `migrate-db-raw` codemod no longer auto-rewrites own-tenant calls whose filter object has a spread after `tenantId`, since the spread can override it at runtime; such sites are listed for manual review."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"version": "0.335.0",
|
|
28
|
+
"type": "fix",
|
|
29
|
+
"title": "switch-tenant validates tenantId, fault logger tolerates non-string type, PII guard matches full ciphertext shape",
|
|
30
|
+
"detail": "`POST /api/auth/switch-tenant` answers 400 `invalid_tenant` for a missing or non-string `tenantId` instead of feeding it into the membership lookup. A non-string `type` in a request body no longer turns a 4xx into a 500 inside the fault logger. The PII ciphertext response guard now matches the full ciphertext shape, so user text containing the bare `kumiko-pii:v` marker no longer 500s reads. `kumiko upgrade --apply` records `pendingManual` in the marker when no codemod runs."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"version": "0.335.0",
|
|
34
|
+
"type": "fix",
|
|
35
|
+
"title": "File-storage boot gate reads the injected env, derivative erasure matches lowercase hashes only",
|
|
36
|
+
"detail": "The `FILE_STORAGE_PROVIDER` boot gate now reads the `env` passed to `validateBoot` (falling back to `process.env`), so an `envSource` handed to `runProdApp` with an empty `process.env` no longer fails the gate. `runDevApp` stops mutating `process.env` for an explicitly wired file provider. The derivative-key recognizer used by forget and tenant-destroy now matches the hash segment case-sensitively."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"version": "0.335.0",
|
|
40
|
+
"type": "breaking",
|
|
41
|
+
"title": "Boot rejects multiple principalStatus providers, updateMany rejects unknown columns again",
|
|
42
|
+
"detail": "Several fail-loud fixes. `buildServer` now also rejects more than one `principalStatus` provider when `auth.membershipQuery` is registered (previously every login 500ed at runtime). `updateMany` throws again on a key that is not a column, and the replay of historical update events skips fields removed from the entity instead. `lt`/`gt`/`lte`/`gte`/`like` on a jsonb column match nothing in the where builder, like the event-store read path.",
|
|
43
|
+
"migration": "`updateMany` now throws when `set` or `where` contains a key that is not a column of the table; remove such keys from your calls. Replay of historical update events still skips removed fields."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"version": "0.335.0",
|
|
47
|
+
"type": "improvement",
|
|
48
|
+
"title": "Boot validator rejects overlapping dateRange facet params, unknown subtitleHref/countField fields and whitespace in redirect.idFrom; clearer refEntity, nav and where-probe diagnostics",
|
|
49
|
+
"detail": "Boot validator follow-ups.\n- A `dateRange` facet param name used by more than one dateRange facet on a projectionList screen now fails boot (the facets overwrote each other's bound).\n- `header.subtitleHref` and a section's `countField` on a projectionDetail screen are checked against the query's `outputSchema`.\n- `redirect.idFrom` with leading or trailing whitespace now fails boot instead of silently navigating without an id.\n- A `refEntity` pointing at a feature that is not mounted now says so and names `r.requires(...)` instead of listing \"(none)\" entities.\n- The nav inversion warning no longer prints `requires roles []` for a leaf with an invalid `openToAll`, and the unreachable-screen warning points workspace apps at `r.workspace({ nav })` instead of a non-existent allowlist.\n- The where-rule boot probe renders `ctx.tableName` with the real table name, matching runtime.",
|
|
50
|
+
"migration": "Boot now fails for configurations that silently misbehaved. Give each dateRange facet on a screen its own from/to params, point header.subtitleHref and section countField at fields of the query's outputSchema, and remove stray whitespace from redirect.idFrom."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"version": "0.335.0",
|
|
54
|
+
"type": "fix",
|
|
55
|
+
"title": "Malformed waitForEvent matchExpr rows are skipped instead of blocking the wakeup projection",
|
|
56
|
+
"detail": "Review round 2 fixes for waitForEvent matching, read steps, env parsing and agent reasons.\n- A pending workflow row with a malformed or unsupported `matchExpr` is now skipped and logged instead of aborting the wakeup projection for every other run waiting on the same event. `evaluateEventMatch` reports a structurally malformed AST as a readable `Malformed EventMatch` error.\n- `read.findOne` and `read.findMany` reject a non-object `unsafeAllTenants` (for example `true`) with an `InternalError` instead of a raw `TypeError`.\n- `parseEnv` reports a refinement that throws as a `KumikoBootError`, and the ciphertext-only slot caveat for refinements is documented.\n- Removed the unused `AgentReasons.permissionDenied` (`agent.permission_denied`) reason and its error-docs pages; nothing emitted it."
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"version": "0.335.0",
|
|
60
|
+
"type": "breaking",
|
|
61
|
+
"title": "AgentReasons.permissionDenied removed",
|
|
62
|
+
"migration": "Drop references to `AgentReasons.permissionDenied` / `\"agent.permission_denied\"`. The framework never emitted this reason."
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"version": "0.335.0",
|
|
66
|
+
"type": "fix",
|
|
67
|
+
"title": "ExtraRouteRejection retryAfterSeconds must be a safe integer; token-request routes stop logging rate_limited",
|
|
68
|
+
"detail": "`ExtraRouteRejection` now rejects a `retryAfterSeconds` beyond the safe-integer range, so the `Retry-After` header is always valid delta-seconds. Password-reset style token-request routes no longer log `rate_limited` handler results as errors."
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"version": "0.335.0",
|
|
72
|
+
"type": "fix",
|
|
73
|
+
"title": "Feature-AST patcher handles quoted keys, stream-handler escapeHatch and shorthand piiFields",
|
|
74
|
+
"detail": "The feature-AST patcher now replaces and unsets string-literal keys such as `\"description\": \"old\"` instead of appending a duplicate key or skipping the unset. `addStreamHandler` accepts `escapeHatch`. The defineEvent extractor accepts a shorthand `{ piiFields }` stance and reports a non-literal options argument with its own message."
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"version": "0.335.0",
|
|
78
|
+
"type": "fix",
|
|
79
|
+
"title": "File byte routes survive undecryptable fileName; loadAggregate and appendProvenanceEvent reject empty type parts",
|
|
80
|
+
"detail": "`GET /files/:id` and the download-url route now fall back to the filename \"download\" when the stored fileName cannot be decrypted (malformed ciphertext, erased subject, annotation drift) instead of returning 500 or leaking ciphertext into headers. `loadAggregate` rejects an empty `aggregateType` instead of loading the unfiltered stream, and `appendProvenanceEvent` rejects event types with an empty owner or name."
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"version": "0.335.0",
|
|
84
|
+
"type": "breaking",
|
|
85
|
+
"title": "FileRef type drops the unreachable isDeleted field",
|
|
86
|
+
"detail": "The exported `FileRef` type no longer carries `isDeleted`. Soft-deleted rows were already filtered out before the guard sees a file, so the field was never reachable. Custom file access guards that read `fileRef.isDeleted` must drop that check.",
|
|
87
|
+
"migration": "Remove reads of `fileRef.isDeleted` in custom file access guards; soft-deleted rows never reach the guard."
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"version": "0.335.0",
|
|
91
|
+
"type": "breaking",
|
|
92
|
+
"title": "File content check covers csv alias, local list scoped to prefix, provider contract checked, locale tags canonicalized",
|
|
93
|
+
"detail": "File storage and locale hardening. `validateFileContent` now rejects binary bytes for an extension with a declared-MIME alias (a real `.xls` renamed to `.csv`), so custom upload routes using `validateFile` + `validateFileContent` are covered. `tenantExportPrefix` and `tenantStoragePrefixes` throw for a tenant id equal to the reserved `exports` segment. The local provider's `list()` only walks the prefix directory instead of the whole storage root. A file-provider plugin whose built provider lacks a required method now fails loudly at resolve time. `canonicalizeLocaleTag` normalizes region and script subtags (`de-AT`, `zh-Hant-TW`), so `X-Locale: DE-at` finds a `de-AT` mail registration.",
|
|
94
|
+
"migration": "A file-provider plugin whose built provider lacks a required method now throws at resolve time; make sure custom file providers implement the full provider interface."
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"version": "0.335.0",
|
|
98
|
+
"type": "breaking",
|
|
99
|
+
"title": "Global-table tenant writes reject loudly, restore maps unique violations, managed unique-index changes recreate",
|
|
100
|
+
"detail": "Tenant-mode `updateMany`/`deleteMany` on a `tenancy: \"global\"` table now reject with an `AccessDeniedError` pointing at `db.global(table)` instead of silently matching zero rows. `restore` on a soft-deleted row whose unique value was re-used now returns a 409 `unique_violation` instead of a raw 500. The migration generator now recreates a managed projection when an existing index becomes unique, changes columns or widens its WHERE.",
|
|
101
|
+
"migration": "Tenant-mode `updateMany`/`deleteMany` on a `tenancy: \"global\"` table now throws instead of matching zero rows; switch those calls to `db.global(table)`."
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"version": "0.335.0",
|
|
105
|
+
"type": "fix",
|
|
106
|
+
"title": "Guard edge cases, extension entityName fallback and dispatcher drain waiting for in-flight passes",
|
|
107
|
+
"detail": "Guards: the runtime-isolation guard now follows value re-exports (`export * from`, `export { x } from`) and reads the `kumiko.runtime` marker in the repo-root `package.json`. The upgrade-state guard prefers the repo-local `node_modules/.bin/kumiko-upgrade` over a PATH entry. The error-reasons guard no longer flags prose reasons passed to `withUnsafeRawGrant`. Remaining German guard messages are now English. Blank `entityName` overrides on extension sections fall back to the host entity. `EventDispatcher.drain()` now also waits for a pass still in its idle pre-check."
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"version": "0.335.0",
|
|
111
|
+
"type": "breaking",
|
|
112
|
+
"title": "i18n key guard covers multiSelect option labels and writeForm section descriptions",
|
|
113
|
+
"detail": "The i18n key guard now requires translations for filterable `multiSelect` option labels and for the `description` of a `projectionDetail` writeForm section. Missing translations used to fall back silently; they now fail the guard.",
|
|
114
|
+
"migration": "The i18n required-surface-keys check now requires translations for filterable `multiSelect` option labels and `projectionDetail` writeForm section descriptions; add the missing keys to your locale files."
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"version": "0.335.0",
|
|
118
|
+
"type": "fix",
|
|
119
|
+
"title": "perTenant fan-out without context.db reports the missing db instead of a missing tenant feature",
|
|
120
|
+
"detail": "A perTenant job on a job runner built without `context.db` now fails with a message that names the missing `context.db` when the tenant feature is mounted, instead of telling the operator to mount it. A readiness failure during `stop()` is now logged at debug level."
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"version": "0.335.0",
|
|
124
|
+
"type": "fix",
|
|
125
|
+
"title": "Key Manager boot resolves slots in parallel, validates the previous-KEK version up front and keeps the fetch error as cause",
|
|
126
|
+
"detail": "The Key Manager boot resolution now resolves all KEK slots in parallel, rejects a previous KEK without `PLATFORM_KEK_PREVIOUS_VERSION` before any Key Manager call, keeps the original network error as `cause`, and names the slot in the partial-wiring error. The boot log line names the source actually taken."
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"version": "0.335.0",
|
|
130
|
+
"type": "fix",
|
|
131
|
+
"title": "Boot column check skips loose/catchall output schemas; schema-walk error names the handler",
|
|
132
|
+
"detail": "The query output-schema column check now skips `z.looseObject()` and `.catchall()` row schemas, since they accept keys beyond the declared shape. The schema-size guard for write-handler input schemas now names the feature and handler in its boot error."
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"version": "0.335.0",
|
|
136
|
+
"type": "fix",
|
|
137
|
+
"title": "Multi-stream projection rebuild honors the shutdown AbortSignal",
|
|
138
|
+
"detail": "`rebuildMultiStreamProjection` now accepts an `AbortSignal` and checks it per replayed event. The boot backfill's shutdown signal reaches it, so a SIGTERM during a long multi-stream replay rolls the transaction back and releases the consumer lock instead of hanging until the pod is killed."
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"version": "0.335.0",
|
|
142
|
+
"type": "fix",
|
|
143
|
+
"title": "multiSelect filters handle empty and non-string values and NULL columns; forget sweeps tolerate missing columns and serial ids",
|
|
144
|
+
"detail": "multiSelect list filters no longer return every row for an empty array value, no longer fail with a 500 on boolean or numeric client values, and `ne` now keeps rows whose column is NULL. The forget sweeps (blind-index nulling, search purge) skip lookupable or searchable fields whose columns are missing from a migrated table instead of aborting after the key was erased, and the self-PII tenant probe ignores serial-id entities instead of throwing on a uuid subject id."
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"version": "0.335.0",
|
|
148
|
+
"type": "fix",
|
|
149
|
+
"title": "App-schema JSON-safety rejects NaN/Infinity and cyclic defaults",
|
|
150
|
+
"detail": "The app-schema JSON-safety check now rejects `NaN`/`Infinity` defaults and guards against self-referencing values. The agent doc lint reports exposed handlers whose input schema cannot be converted to JSON Schema (they were silently left out of the agent manifest). Icon-only list and edit action buttons now also set `title`, so sighted mouse users get the hover tooltip."
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"version": "0.335.0",
|
|
154
|
+
"type": "fix",
|
|
155
|
+
"title": "Executor create/restore validate before connection binding; clearer rebuild and backfill errors",
|
|
156
|
+
"detail": "The event-store executor's `create()` and `restore()` now run their validation (global-tenancy guard, `soft_delete_not_enabled`) before resolving the connection, so callers get the proper 4xx error instead of an internal \"no connection bound\" error. List `totalCount` always counts over the full WHERE, and a PII backfill failure for an unresolvable catalog event subject no longer suggests the entity-only retry options. The shadow-swap RLS and blind-index error messages now state the concrete action (full KMS wiring for the blind-index key)."
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"version": "0.335.0",
|
|
160
|
+
"type": "fix",
|
|
161
|
+
"title": "afterCommit hook writes run in their own transaction, plus cache, nested-write, skip-poison and search-consumer fixes",
|
|
162
|
+
"detail": "Writes from an afterCommit hook now run as their own transaction with their own afterCommit flush, so the nested write's hooks fire and a failing projection rolls it back. The tenant timezone cache no longer stores a value read before a concurrent config write. Nested-write ownership checks complete a partial parent row from the stored row. `skipPoisonEvent` locks the consumer row before computing pending gaps. The search consumer fails an event for retry when its context has no database instead of removing the index entry."
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"version": "0.335.0",
|
|
166
|
+
"type": "fix",
|
|
167
|
+
"title": "Qualified redirects resolve in their named feature, update forms skip untouched required fields, secretMint confirm can restart",
|
|
168
|
+
"detail": "A qualified redirect target (`feature:screen:id`) is now resolved inside the named feature, so a same-id screen in the redirecting feature no longer swallows the created record id. An entityEdit update form only presence-checks required fields the user changed, so a legacy row with an empty, newly required field can still be saved. The secretMint confirm step always offers a cancel that restarts at the mint form when no redirect or cancelTarget is set."
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"version": "0.335.0",
|
|
172
|
+
"type": "breaking",
|
|
173
|
+
"title": "totalsMatch rejects a payload that carries only one side of the pair",
|
|
174
|
+
"detail": "A `totalsMatch` pair is now validated as a unit. A payload that carries only the embedded list or only its sibling money field is rejected with a validation error, because update payloads only carry `changes` and the sum could otherwise drift away from the total.",
|
|
175
|
+
"migration": "Send the embedded list and its sibling money field together in every create and update payload. A partial update that changes only `total` or only the list now fails validation."
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"version": "0.335.0",
|
|
179
|
+
"type": "fix",
|
|
180
|
+
"title": "Transfer-graph boot check rejects a multiple reference only when its target is transferable",
|
|
181
|
+
"detail": "The transfer-graph boot check now rejects a `multiple` reference on a transferable entity only when the target entity is itself transferable. A multiple reference to a plain lookup entity boots again."
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"version": "0.335.0",
|
|
185
|
+
"type": "fix",
|
|
186
|
+
"title": "Empty KUMIKO_TRUSTED_PROXY_HOPS is treated as unset, and dist no longer ships compiled codemods",
|
|
187
|
+
"detail": "`parseTrustedProxyHopsEnv` treats an empty `KUMIKO_TRUSTED_PROXY_HOPS` as unset, so a blank `.env` template line no longer crashes the dev boot while prod ignored it. The published package also no longer ships the unusable compiled `dist/scripts/` codemod copies."
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"version": "0.335.0",
|
|
191
|
+
"type": "fix",
|
|
192
|
+
"title": "Upgrade marker needs an installed version, Meilisearch drop keeps sibling prefixes, drainJobs sees pre-run job failures",
|
|
193
|
+
"detail": "`kumiko upgrade --apply` now refuses to write a marker when no installed Kumiko version can be found under the target, instead of recording the `--from` filter value. It also finds `node_modules/@cosmicdrift/*` packages that are workspace symlinks. `MeilisearchAdapter.dropAllIndexes()` only drops tenant indexes of its own prefix and leaves indexes of a longer sibling prefix alone. `drainJobs()` now rejects when a perTenant fan-out or an unknown job fails before the job body runs."
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"version": "0.335.0",
|
|
197
|
+
"type": "fix",
|
|
198
|
+
"title": "SSE stream closes when access is revoked",
|
|
199
|
+
"detail": "The live event stream (GET /sse) now closes as soon as the session behind it is revoked or the user's tenant roles change or the membership is removed. User-addressed frames such as in-app notifications are delivered only to the addressed user instead of the whole tenant."
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"version": "0.335.0",
|
|
203
|
+
"type": "improvement",
|
|
204
|
+
"title": "Boot validator checks redirect targets of writeHandler record actions",
|
|
205
|
+
"detail": "An unknown `redirect` screen on a projectionDetail or entityEdit action or section action fails boot, like entityEdit `redirect`.",
|
|
206
|
+
"migration": "If a writeHandler record action already carries a `redirect` that does not resolve to a registered screen, fix or remove it."
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"version": "0.334.0",
|
|
210
|
+
"type": "improvement",
|
|
211
|
+
"title": "Settings form gaps: writeForm footer submit, option description/group, dependent optionsQueryPayload, config badge label, writeOnly secret fields",
|
|
212
|
+
"detail": "- A `writeForm` section that fills a whole tab puts its submit button into the pinned form footer.\n- `optionsQuery` rows may carry `description` (muted second line) and `group` (heading). The combobox and the radio list show both. Options with either one never render as segments.\n- `optionsQueryPayload` values may be `{ field: \"<sibling>\" }`. The select reloads when that field changes and clears a value the new rows no longer contain. A cleared config select resets the key, so the inherited value applies again. On config keys, `field` names another key of the same feature on the same settings mask. The boot validator checks the names, and `writeForm` fieldDefs now go through the select checks too.\n- The origin line and cascade level rows on `configEdit` fields show the option label instead of the raw value or id.\n- New `writeOnly: true` on entity text fields with `find: \"secret\"`. Reads return `true` (set) or `null` and never the value. On write, `\"\"` keeps the stored value and `null` clears it. The edit form shows a masked input with a \"set\" placeholder and a remove action. `maskWriteOnlyFields(entity, row)` is exported from `@cosmicdrift/kumiko-framework/engine` for custom query handlers that return executor rows."
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"version": "0.334.0",
|
|
216
|
+
"type": "breaking",
|
|
217
|
+
"title": "Duplicate write/query handler names in one feature fail at definition time",
|
|
218
|
+
"migration": "Rename a custom handler that shares its name with another handler of the same feature, including names r.crud registers (<entity>:list, <entity>:detail, <entity>:create, <entity>:update, <entity>:delete, <entity>:restore). Before, the later registration replaced the earlier one silently."
|
|
219
|
+
},
|
|
2
220
|
{
|
|
3
221
|
"version": "0.332.0",
|
|
4
222
|
"type": "fix",
|
|
@@ -168,8 +168,20 @@ export function migrateCrossTenantSource(
|
|
|
168
168
|
const rewrites: Rewrite[] = [];
|
|
169
169
|
const manual: ManualSite[] = [];
|
|
170
170
|
|
|
171
|
+
// Captured up front: a forgotten node can no longer report its position.
|
|
172
|
+
const lineByAssignment = new Map(targets.map((a) => [a, a.getStartLineNumber()]));
|
|
173
|
+
|
|
171
174
|
for (const assignment of targets) {
|
|
172
|
-
|
|
175
|
+
// A forgotten node has no position or text left; a bare `continue` would
|
|
176
|
+
// drop a security-relevant crossTenant switch from the report entirely.
|
|
177
|
+
if (assignment.wasForgotten()) {
|
|
178
|
+
manual.push({
|
|
179
|
+
file: fileName,
|
|
180
|
+
line: lineByAssignment.get(assignment) ?? 0,
|
|
181
|
+
why: "node was invalidated by an earlier rewrite in this file — run the codemod on this file again",
|
|
182
|
+
});
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
173
185
|
const line = assignment.getStartLineNumber();
|
|
174
186
|
const init = assignment.getInitializer();
|
|
175
187
|
|
|
@@ -327,7 +339,7 @@ async function main(): Promise<void> {
|
|
|
327
339
|
}
|
|
328
340
|
}
|
|
329
341
|
|
|
330
|
-
process.exit(0);
|
|
342
|
+
process.exit(allManual.length > 0 ? 1 : 0);
|
|
331
343
|
}
|
|
332
344
|
|
|
333
345
|
if (import.meta.main) {
|
|
@@ -89,14 +89,22 @@ function isValidOwnTenantIdValue(valueNode: Node): boolean {
|
|
|
89
89
|
|
|
90
90
|
function objectHasValidOwnTenantId(obj: Node): boolean {
|
|
91
91
|
if (!Node.isObjectLiteralExpression(obj)) return false;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
92
|
+
const props = obj.getProperties();
|
|
93
|
+
const tenantIdIndex = props.findIndex(
|
|
94
|
+
(prop) =>
|
|
95
|
+
(Node.isPropertyAssignment(prop) || Node.isShorthandPropertyAssignment(prop)) &&
|
|
96
|
+
prop.getName() === "tenantId",
|
|
97
|
+
);
|
|
98
|
+
if (tenantIdIndex === -1) return false;
|
|
99
|
+
// A later spread can override the literal tenantId at runtime.
|
|
100
|
+
if (props.slice(tenantIdIndex + 1).some((prop) => Node.isSpreadAssignment(prop))) return false;
|
|
101
|
+
const prop = props[tenantIdIndex];
|
|
102
|
+
if (Node.isPropertyAssignment(prop)) {
|
|
103
|
+
const init = prop.getInitializer();
|
|
104
|
+
return init !== undefined && isValidOwnTenantIdValue(init);
|
|
105
|
+
}
|
|
106
|
+
if (Node.isShorthandPropertyAssignment(prop)) {
|
|
107
|
+
return isConstTenantIdBinding(enclosingFunctionScope(prop), "tenantId");
|
|
100
108
|
}
|
|
101
109
|
return false;
|
|
102
110
|
}
|
|
@@ -188,6 +188,12 @@ function collectFiles(paths: string[]): string[] {
|
|
|
188
188
|
return files;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
// Remaining manual sites still fail boot validation, so upgrade-cli must not
|
|
192
|
+
// treat this run as a completed migration.
|
|
193
|
+
export function exitCodeForManualSites(manualCount: number): 0 | 1 {
|
|
194
|
+
return manualCount > 0 ? 1 : 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
191
197
|
async function main(): Promise<void> {
|
|
192
198
|
let options: CliOptions;
|
|
193
199
|
try {
|
|
@@ -241,7 +247,7 @@ async function main(): Promise<void> {
|
|
|
241
247
|
}
|
|
242
248
|
}
|
|
243
249
|
|
|
244
|
-
process.exit(
|
|
250
|
+
process.exit(exitCodeForManualSites(allManual.length));
|
|
245
251
|
}
|
|
246
252
|
|
|
247
253
|
if (import.meta.main) {
|
|
@@ -473,7 +473,14 @@ function reportStanceHasPersonalStance(obj: ObjectLiteralExpression): boolean {
|
|
|
473
473
|
function reportStanceEnclosingFieldName(call: CallExpression): string | undefined {
|
|
474
474
|
let node = call.getParent();
|
|
475
475
|
while (node) {
|
|
476
|
-
if (Node.isPropertyAssignment(node))
|
|
476
|
+
if (Node.isPropertyAssignment(node)) {
|
|
477
|
+
// getName() keeps the quotes of a string-literal key; computed keys have no static name.
|
|
478
|
+
const nameNode = node.getNameNode();
|
|
479
|
+
if (Node.isStringLiteral(nameNode) || Node.isNoSubstitutionTemplateLiteral(nameNode)) {
|
|
480
|
+
return nameNode.getLiteralText();
|
|
481
|
+
}
|
|
482
|
+
return Node.isIdentifier(nameNode) ? nameNode.getText() : undefined;
|
|
483
|
+
}
|
|
477
484
|
node = node.getParent();
|
|
478
485
|
}
|
|
479
486
|
return undefined;
|
|
@@ -506,17 +513,28 @@ function reportStanceResolveEntity(call: CallExpression): string | null {
|
|
|
506
513
|
return varDecl ? varDecl.getName() : null;
|
|
507
514
|
}
|
|
508
515
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
516
|
+
function isSegmentStart(fieldOriginal: string, index: number): boolean {
|
|
517
|
+
return (
|
|
518
|
+
index === 0 || /[A-Z]/.test(fieldOriginal[index] ?? "") || fieldOriginal[index - 1] === "_"
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// A hint only counts when it starts and ends on a segment boundary (index 0 /
|
|
523
|
+
// end of field, an uppercase letter in the original, or `_`) — otherwise a
|
|
524
|
+
// coincidental substring like "text" in "contextId" or "type" in "contentType"
|
|
525
|
+
// would false-positive.
|
|
512
526
|
function hintOccursAtBoundary(fieldLower: string, fieldOriginal: string, hint: string): boolean {
|
|
513
527
|
let searchFrom = 0;
|
|
514
528
|
for (;;) {
|
|
515
529
|
const index = fieldLower.indexOf(hint, searchFrom);
|
|
516
530
|
if (index === -1) return false;
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
531
|
+
const end = index + hint.length;
|
|
532
|
+
if (
|
|
533
|
+
isSegmentStart(fieldOriginal, index) &&
|
|
534
|
+
(end === fieldOriginal.length || isSegmentStart(fieldOriginal, end))
|
|
535
|
+
) {
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
520
538
|
searchFrom = index + 1;
|
|
521
539
|
}
|
|
522
540
|
}
|
|
@@ -532,23 +550,26 @@ function findLongestBoundaryHint(fieldLower: string, fieldOriginal: string): str
|
|
|
532
550
|
|
|
533
551
|
function segmentAlignedSuffixes(fieldOriginal: string): string[] {
|
|
534
552
|
const fieldLower = fieldOriginal.toLowerCase();
|
|
535
|
-
const
|
|
553
|
+
const starts: number[] = [];
|
|
536
554
|
for (let index = 0; index < fieldOriginal.length; index++) {
|
|
537
|
-
|
|
538
|
-
index === 0 || /[A-Z]/.test(fieldOriginal[index] ?? "") || fieldOriginal[index - 1] === "_";
|
|
539
|
-
if (atBoundary) suffixes.push(fieldLower.slice(index));
|
|
555
|
+
if (isSegmentStart(fieldOriginal, index)) starts.push(index);
|
|
540
556
|
}
|
|
541
|
-
|
|
557
|
+
// Hints are lowercase, so a hint's own segment boundary is unknowable. A
|
|
558
|
+
// single-segment tail ("Number", "Owner") is too generic to align with one;
|
|
559
|
+
// two or more segments ("UserId") are specific enough.
|
|
560
|
+
return starts.slice(0, -1).map((index) => fieldLower.slice(index));
|
|
542
561
|
}
|
|
543
562
|
|
|
544
563
|
// The hint sets only carry exact full names, so a suffix variant of one
|
|
545
564
|
// (e.g. "...UserId" of "assigneeUserId") otherwise slips through undetected.
|
|
565
|
+
// The suffix must be the tail of the hint: plain `includes` matched "number"
|
|
566
|
+
// inside "phonenumber" for "orderNumber".
|
|
546
567
|
function findShortestHintContainingSuffix(fieldOriginal: string): string | undefined {
|
|
547
568
|
let best: string | undefined;
|
|
548
569
|
for (const suffix of segmentAlignedSuffixes(fieldOriginal)) {
|
|
549
570
|
if (suffix.length < 5) continue;
|
|
550
571
|
for (const hint of ALL_PII_NAME_HINTS) {
|
|
551
|
-
if (!hint.
|
|
572
|
+
if (hint === suffix || !hint.endsWith(suffix)) continue;
|
|
552
573
|
if (!best || hint.length < best.length) best = hint;
|
|
553
574
|
}
|
|
554
575
|
}
|
|
@@ -556,22 +577,37 @@ function findShortestHintContainingSuffix(fieldOriginal: string): string | undef
|
|
|
556
577
|
}
|
|
557
578
|
|
|
558
579
|
function classifyFieldStance(field: string): { stance: StanceClass; hint: string | undefined } {
|
|
559
|
-
const fieldLower = field.toLowerCase();
|
|
580
|
+
const fieldLower = field.replaceAll("_", "").toLowerCase();
|
|
560
581
|
if (PII_DIRECT_NAME_HINTS.has(fieldLower)) return { stance: "direct", hint: fieldLower };
|
|
561
582
|
if (PII_USER_OWNED_NAME_HINTS.has(fieldLower)) return { stance: "user-owned", hint: fieldLower };
|
|
562
583
|
if (PII_USER_REFERENCE_NAME_HINTS.has(fieldLower))
|
|
563
584
|
return { stance: "user-reference", hint: fieldLower };
|
|
564
|
-
const containmentHint = findLongestBoundaryHint(
|
|
585
|
+
const containmentHint = findLongestBoundaryHint(field.toLowerCase(), field);
|
|
565
586
|
if (containmentHint) return { stance: "near-miss", hint: containmentHint };
|
|
566
587
|
const suffixHint = findShortestHintContainingSuffix(field);
|
|
567
588
|
if (suffixHint) return { stance: "near-miss", hint: suffixHint };
|
|
568
589
|
return { stance: "unclassified", hint: undefined };
|
|
569
590
|
}
|
|
570
591
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
592
|
+
function createReportStanceProject(): Project {
|
|
593
|
+
return new Project({ useInMemoryFileSystem: true, skipFileDependencyResolution: true });
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export function reportStanceForSource(
|
|
597
|
+
source: string,
|
|
598
|
+
filePath: string,
|
|
599
|
+
project: Project = createReportStanceProject(),
|
|
600
|
+
): StanceSite[] {
|
|
601
|
+
const sourceFile = project.createSourceFile(filePath, source, { overwrite: true });
|
|
602
|
+
try {
|
|
603
|
+
return collectStanceSites(sourceFile);
|
|
604
|
+
} finally {
|
|
605
|
+
// A shared project would otherwise keep every scanned file's AST alive.
|
|
606
|
+
project.removeSourceFile(sourceFile);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
574
609
|
|
|
610
|
+
function collectStanceSites(sourceFile: SourceFile): StanceSite[] {
|
|
575
611
|
const sites: StanceSite[] = [];
|
|
576
612
|
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
577
613
|
const exprNode = call.getExpression();
|
|
@@ -634,8 +670,9 @@ function reportStance(rootDir: string): void {
|
|
|
634
670
|
};
|
|
635
671
|
let total = 0;
|
|
636
672
|
|
|
673
|
+
const project = createReportStanceProject();
|
|
637
674
|
for (const file of findReportStanceFiles(rootDir)) {
|
|
638
|
-
const sites = reportStanceForSource(readFileSync(file, "utf8"), file);
|
|
675
|
+
const sites = reportStanceForSource(readFileSync(file, "utf8"), file, project);
|
|
639
676
|
if (sites.length === 0) continue;
|
|
640
677
|
|
|
641
678
|
console.log(`\n${relative(rootDir, file)}`);
|
|
@@ -193,42 +193,99 @@ function processNewExpression(args: readonly Node[], report: (note: string) => v
|
|
|
193
193
|
return "changed";
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
// `failUnprocessable(reason, details)` takes the details literal directly as
|
|
197
|
+
// its second argument (no `{ details }` wrapper like the ctor's opts).
|
|
198
|
+
function processFailUnprocessableCall(
|
|
199
|
+
args: readonly Node[],
|
|
200
|
+
report: (note: string) => void,
|
|
201
|
+
): CallOutcome {
|
|
202
|
+
const detailsArg = args[1];
|
|
203
|
+
if (!detailsArg) return "not-a-match";
|
|
204
|
+
|
|
205
|
+
if (!Node.isObjectLiteralExpression(detailsArg)) {
|
|
206
|
+
report(
|
|
207
|
+
"second argument (details) is not a statically-analyzable object literal — cannot verify details.reason",
|
|
208
|
+
);
|
|
209
|
+
return "skipped";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const removal = removeReasonProperty(detailsArg);
|
|
213
|
+
if (removal.kind === "absent") return "not-a-match";
|
|
214
|
+
if (removal.kind === "skip") {
|
|
215
|
+
report(removal.note);
|
|
216
|
+
return "skipped";
|
|
217
|
+
}
|
|
218
|
+
return "changed";
|
|
219
|
+
}
|
|
220
|
+
|
|
196
221
|
type SkipEntry = { readonly file: string; readonly line: number; readonly note: string };
|
|
197
222
|
|
|
223
|
+
type MigratableCall = {
|
|
224
|
+
readonly exportName: "UnprocessableError" | "failUnprocessable";
|
|
225
|
+
readonly localKind: "class" | "function";
|
|
226
|
+
readonly process: (args: readonly Node[], report: (note: string) => void) => CallOutcome;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const UNPROCESSABLE_ERROR_CTOR: MigratableCall = {
|
|
230
|
+
exportName: "UnprocessableError",
|
|
231
|
+
localKind: "class",
|
|
232
|
+
process: processNewExpression,
|
|
233
|
+
};
|
|
234
|
+
const FAIL_UNPROCESSABLE_CALL: MigratableCall = {
|
|
235
|
+
exportName: "failUnprocessable",
|
|
236
|
+
localKind: "function",
|
|
237
|
+
process: processFailUnprocessableCall,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
function migrateCallSite(
|
|
241
|
+
sourceFile: SourceFile,
|
|
242
|
+
importedNames: ReadonlyMap<string, ImportedBinding>,
|
|
243
|
+
site: Node & { getExpression(): Node; getArguments(): Node[] },
|
|
244
|
+
target: MigratableCall,
|
|
245
|
+
skips: SkipEntry[],
|
|
246
|
+
): boolean {
|
|
247
|
+
const callee = site.getExpression();
|
|
248
|
+
if (!Node.isIdentifier(callee)) return false;
|
|
249
|
+
const localName = callee.getText();
|
|
250
|
+
const line = site.getStartLineNumber();
|
|
251
|
+
const file = sourceFile.getFilePath();
|
|
252
|
+
|
|
253
|
+
const binding = importedNames.get(localName);
|
|
254
|
+
if (binding?.importedName !== target.exportName) {
|
|
255
|
+
if (localName !== target.exportName) return false;
|
|
256
|
+
const note = binding
|
|
257
|
+
? `${target.exportName} is imported from "${binding.moduleSpecifier}", not a @cosmicdrift/kumiko-* package`
|
|
258
|
+
: `${target.exportName} has no @cosmicdrift/kumiko-* import in this file (likely a locally defined ${target.localKind})`;
|
|
259
|
+
skips.push({ file, line, note });
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
if (!KUMIKO_MODULE_RE.test(binding.moduleSpecifier)) {
|
|
263
|
+
skips.push({
|
|
264
|
+
file,
|
|
265
|
+
line,
|
|
266
|
+
note: `${target.exportName} is imported from "${binding.moduleSpecifier}", not a @cosmicdrift/kumiko-* package`,
|
|
267
|
+
});
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const outcome = target.process(site.getArguments(), (note) => skips.push({ file, line, note }));
|
|
272
|
+
return outcome === "changed";
|
|
273
|
+
}
|
|
274
|
+
|
|
198
275
|
/** Migrates one file in place. Returns how many `reason` properties were removed. */
|
|
199
276
|
function migrateFile(sourceFile: SourceFile, skips: SkipEntry[]): number {
|
|
200
277
|
const importedNames = collectImportedNames(sourceFile);
|
|
201
278
|
let removedCount = 0;
|
|
202
279
|
|
|
203
280
|
for (const newExpr of sourceFile.getDescendantsOfKind(SyntaxKind.NewExpression)) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const localName = callee.getText();
|
|
207
|
-
|
|
208
|
-
const binding = importedNames.get(localName);
|
|
209
|
-
const isFrameworkUnprocessableError = binding?.importedName === "UnprocessableError";
|
|
210
|
-
|
|
211
|
-
if (!isFrameworkUnprocessableError) {
|
|
212
|
-
if (localName !== "UnprocessableError") continue;
|
|
213
|
-
const note = binding
|
|
214
|
-
? `UnprocessableError is imported from "${binding.moduleSpecifier}", not a @cosmicdrift/kumiko-* package`
|
|
215
|
-
: "UnprocessableError has no @cosmicdrift/kumiko-* import in this file (likely a locally defined class)";
|
|
216
|
-
skips.push({ file: sourceFile.getFilePath(), line: newExpr.getStartLineNumber(), note });
|
|
217
|
-
continue;
|
|
281
|
+
if (migrateCallSite(sourceFile, importedNames, newExpr, UNPROCESSABLE_ERROR_CTOR, skips)) {
|
|
282
|
+
removedCount++;
|
|
218
283
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
note: `UnprocessableError is imported from "${binding.moduleSpecifier}", not a @cosmicdrift/kumiko-* package`,
|
|
224
|
-
});
|
|
225
|
-
continue;
|
|
284
|
+
}
|
|
285
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
286
|
+
if (migrateCallSite(sourceFile, importedNames, call, FAIL_UNPROCESSABLE_CALL, skips)) {
|
|
287
|
+
removedCount++;
|
|
226
288
|
}
|
|
227
|
-
|
|
228
|
-
const outcome = processNewExpression(newExpr.getArguments(), (note) => {
|
|
229
|
-
skips.push({ file: sourceFile.getFilePath(), line: newExpr.getStartLineNumber(), note });
|
|
230
|
-
});
|
|
231
|
-
if (outcome === "changed") removedCount++;
|
|
232
289
|
}
|
|
233
290
|
|
|
234
291
|
return removedCount;
|