@cosmicdrift/kumiko-framework 0.387.0 → 0.388.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.
Files changed (240) hide show
  1. package/AGENTS.md +6 -1
  2. package/agents/examples/anonymous-access.md +1 -1
  3. package/agents/examples/apex-landing.md +204 -0
  4. package/agents/examples/api-worker-split.md +64 -0
  5. package/agents/examples/app-config.md +14 -0
  6. package/agents/examples/auth-mfa.md +115 -0
  7. package/agents/examples/env-composition.md +28 -1
  8. package/agents/examples/legal-pages.md +1 -1
  9. package/agents/examples/prod-boot-options.md +16 -0
  10. package/agents/examples/rate-limiting.md +48 -0
  11. package/agents/examples/record-detail-layout.md +692 -0
  12. package/agents/examples/seed-migration.md +212 -0
  13. package/agents/examples/wizard-form.md +941 -0
  14. package/agents/examples/writeform-section.md +548 -0
  15. package/agents/features.md +19 -2
  16. package/agents/widgets.md +1 -0
  17. package/dist/api/client-error-route.js +8 -2
  18. package/dist/api/index.d.ts +3 -1
  19. package/dist/api/index.js +3 -1
  20. package/dist/api/request-context.d.ts +1 -1
  21. package/dist/api/request-context.js +1 -1
  22. package/dist/api/server-boot-checks.d.ts +10 -0
  23. package/dist/api/server-boot-checks.js +157 -0
  24. package/dist/api/server-event-dispatcher.d.ts +26 -0
  25. package/dist/api/server-event-dispatcher.js +208 -0
  26. package/dist/api/server-extra-routes.d.ts +11 -0
  27. package/dist/api/server-extra-routes.js +203 -0
  28. package/dist/api/server-middleware.d.ts +20 -0
  29. package/dist/api/server-middleware.js +211 -0
  30. package/dist/api/server-routes.d.ts +19 -0
  31. package/dist/api/server-routes.js +293 -0
  32. package/dist/api/server-websocket-routes.d.ts +4 -0
  33. package/dist/api/server-websocket-routes.js +108 -0
  34. package/dist/api/server-wiring.d.ts +32 -0
  35. package/dist/api/server-wiring.js +236 -0
  36. package/dist/api/server.d.ts +10 -15
  37. package/dist/api/server.js +48 -1251
  38. package/dist/api/session-cookies.js +4 -4
  39. package/dist/api/sse-route.js +16 -12
  40. package/dist/crypto/owner-transition-rekey.d.ts +5 -1
  41. package/dist/crypto/owner-transition-rekey.js +1 -1
  42. package/dist/crypto/pii-field-encryption.d.ts +5 -1
  43. package/dist/crypto/pii-field-encryption.js +3 -1
  44. package/dist/db/aggregate-sql.d.ts +21 -0
  45. package/dist/db/aggregate-sql.js +155 -0
  46. package/dist/db/closed-connection.d.ts +9 -0
  47. package/dist/db/closed-connection.js +95 -0
  48. package/dist/db/dialect.js +1 -1
  49. package/dist/db/distinct-on.d.ts +1 -1
  50. package/dist/db/driver-row-coercion.d.ts +2 -0
  51. package/dist/db/driver-row-coercion.js +186 -0
  52. package/dist/db/driver-value-preparation.d.ts +16 -0
  53. package/dist/db/driver-value-preparation.js +99 -0
  54. package/dist/db/entity-field-encryption.d.ts +1 -0
  55. package/dist/db/entity-field-encryption.js +1 -1
  56. package/dist/db/entity-table-meta.js +103 -110
  57. package/dist/db/event-store-executor-context.d.ts +2 -1
  58. package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
  59. package/dist/db/event-store-executor-read-aggregate.js +112 -0
  60. package/dist/db/event-store-executor-read-filter.d.ts +31 -0
  61. package/dist/db/event-store-executor-read-filter.js +203 -0
  62. package/dist/db/event-store-executor-read-paging.d.ts +14 -0
  63. package/dist/db/event-store-executor-read-paging.js +99 -0
  64. package/dist/db/event-store-executor-read-rows.d.ts +6 -0
  65. package/dist/db/event-store-executor-read-rows.js +32 -0
  66. package/dist/db/event-store-executor-read-search.d.ts +28 -0
  67. package/dist/db/event-store-executor-read-search.js +221 -0
  68. package/dist/db/event-store-executor-read-sort.d.ts +41 -0
  69. package/dist/db/event-store-executor-read-sort.js +130 -0
  70. package/dist/db/event-store-executor-read-where.d.ts +24 -0
  71. package/dist/db/event-store-executor-read-where.js +81 -0
  72. package/dist/db/event-store-executor-read.d.ts +2 -9
  73. package/dist/db/event-store-executor-read.js +48 -701
  74. package/dist/db/event-store-executor-write-steps.d.ts +20 -0
  75. package/dist/db/event-store-executor-write-steps.js +100 -0
  76. package/dist/db/event-store-executor-write.js +395 -499
  77. package/dist/db/index.d.ts +6 -2
  78. package/dist/db/index.js +5 -1
  79. package/dist/db/list-aggregate-labels.d.ts +14 -0
  80. package/dist/db/list-aggregate-labels.js +26 -0
  81. package/dist/db/migrate-generator.js +71 -86
  82. package/dist/db/migrate-runner.js +4 -1
  83. package/dist/db/query.d.ts +2 -15
  84. package/dist/db/query.js +8 -761
  85. package/dist/db/reference-data.js +6 -1
  86. package/dist/db/table-builder.js +113 -166
  87. package/dist/db/table-info.d.ts +10 -0
  88. package/dist/db/table-info.js +45 -0
  89. package/dist/db/temporal-value-guards.d.ts +6 -0
  90. package/dist/db/temporal-value-guards.js +15 -0
  91. package/dist/db/tenant-db-runner.d.ts +1 -0
  92. package/dist/db/tenant-db-runner.js +3 -0
  93. package/dist/db/tenant-db.js +39 -9
  94. package/dist/db/where-clause.d.ts +6 -0
  95. package/dist/db/where-clause.js +236 -0
  96. package/dist/derivatives/delete-stored-file.js +2 -1
  97. package/dist/engine/boot-validator/boot-check.js +1 -1
  98. package/dist/engine/boot-validator/detail-screens.js +2 -2
  99. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  100. package/dist/engine/boot-validator/entity-handler.js +17 -0
  101. package/dist/engine/boot-validator/form-derivations.js +35 -1
  102. package/dist/engine/boot-validator/i18n-keys.js +5 -3
  103. package/dist/engine/boot-validator/index.js +2 -0
  104. package/dist/engine/boot-validator/pii-retention.js +233 -155
  105. package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
  106. package/dist/engine/boot-validator/screens-actions.js +8 -1
  107. package/dist/engine/boot-validator/screens-dashboard.js +2 -5
  108. package/dist/engine/boot-validator/screens-forms.js +2 -6
  109. package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
  110. package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
  111. package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
  112. package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
  113. package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
  114. package/dist/engine/boot-validator/screens-kind-entity-list.js +228 -0
  115. package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
  116. package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
  117. package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
  118. package/dist/engine/boot-validator/screens-kind-projection-list.js +52 -0
  119. package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
  120. package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
  121. package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
  122. package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
  123. package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
  124. package/dist/engine/boot-validator/screens-lists.js +12 -9
  125. package/dist/engine/boot-validator/screens.d.ts +0 -2
  126. package/dist/engine/boot-validator/screens.js +99 -860
  127. package/dist/engine/build-app-schema.js +56 -96
  128. package/dist/engine/define-feature.js +9 -2
  129. package/dist/engine/define-handler.js +1 -0
  130. package/dist/engine/entity-handlers.d.ts +13 -0
  131. package/dist/engine/entity-handlers.js +10 -21
  132. package/dist/engine/entity-list-runtime-options.d.ts +8 -0
  133. package/dist/engine/entity-list-runtime-options.js +18 -0
  134. package/dist/engine/feature-ast/extractors/handlers.js +1 -0
  135. package/dist/engine/feature-ast/extractors/round2.js +25 -6
  136. package/dist/engine/feature-ast/patch.js +4 -182
  137. package/dist/engine/feature-ast/pattern-change-schema.d.ts +3 -0
  138. package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
  139. package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
  140. package/dist/engine/feature-ast/pattern-id-matchers.js +210 -0
  141. package/dist/engine/feature-ast/patterns.d.ts +9 -0
  142. package/dist/engine/feature-ast/patterns.js +43 -51
  143. package/dist/engine/feature-ast/render.js +48 -87
  144. package/dist/engine/feature-entity-handlers.js +3 -0
  145. package/dist/engine/form-derivations.js +18 -7
  146. package/dist/engine/handler-field-references.js +1 -1
  147. package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
  148. package/dist/engine/project-app-schema-for-roles.js +6 -4
  149. package/dist/engine/registry-ingest.js +1 -1
  150. package/dist/engine/schema-builder.js +228 -212
  151. package/dist/engine/screen-helpers.js +1 -1
  152. package/dist/engine/screen-kind.d.ts +7 -0
  153. package/dist/engine/screen-kind.js +1 -0
  154. package/dist/engine/strip-disabled-features-from-app-schema.js +51 -14
  155. package/dist/engine/validate-projection-allowlist.js +2 -1
  156. package/dist/entrypoint/index.d.ts +4 -1
  157. package/dist/entrypoint/index.js +7 -1
  158. package/dist/errors/docs-url.d.ts +1 -1
  159. package/dist/errors/docs-url.js +22 -2
  160. package/dist/es-ops/context.js +1 -1
  161. package/dist/event-store/admin-api.js +1 -1
  162. package/dist/event-store/event-store.js +10 -4
  163. package/dist/files/bind-pending-file-refs.js +3 -3
  164. package/dist/files/create-file-ref-from-bytes.js +4 -1
  165. package/dist/files/file-handle.js +2 -3
  166. package/dist/files/file-routes.js +11 -8
  167. package/dist/files/resolve-content-type.js +43 -0
  168. package/dist/files/upload-ledger.js +7 -0
  169. package/dist/i18n/required-surface-keys.js +182 -206
  170. package/dist/internal/stack-support.d.ts +2 -1
  171. package/dist/internal/stack-support.js +1 -1
  172. package/dist/jobs/job-runner-context.d.ts +32 -0
  173. package/dist/jobs/job-runner-context.js +305 -0
  174. package/dist/jobs/job-runner-execution.d.ts +7 -0
  175. package/dist/jobs/job-runner-execution.js +160 -0
  176. package/dist/jobs/job-runner-gates.d.ts +19 -0
  177. package/dist/jobs/job-runner-gates.js +198 -0
  178. package/dist/jobs/job-runner-helpers.d.ts +17 -3
  179. package/dist/jobs/job-runner-helpers.js +61 -2
  180. package/dist/jobs/job-runner-scope.d.ts +44 -0
  181. package/dist/jobs/job-runner-scope.js +102 -0
  182. package/dist/jobs/job-runner-types.d.ts +12 -0
  183. package/dist/jobs/job-runner.d.ts +0 -1
  184. package/dist/jobs/job-runner.js +47 -657
  185. package/dist/observability/escape-hatch-report.d.ts +3 -0
  186. package/dist/observability/escape-hatch-report.js +12 -1
  187. package/dist/pipeline/cascade-handler.js +165 -138
  188. package/dist/pipeline/dispatch-query.js +10 -3
  189. package/dist/pipeline/dispatch-shared.d.ts +13 -3
  190. package/dist/pipeline/dispatch-shared.js +58 -710
  191. package/dist/pipeline/dispatch-write.js +296 -189
  192. package/dist/pipeline/event-dispatcher-admin.js +2 -1
  193. package/dist/pipeline/event-dispatcher-delivery.js +3 -5
  194. package/dist/pipeline/handler-context-auth.d.ts +9 -0
  195. package/dist/pipeline/handler-context-auth.js +54 -0
  196. package/dist/pipeline/handler-context-config.d.ts +14 -0
  197. package/dist/pipeline/handler-context-config.js +113 -0
  198. package/dist/pipeline/handler-context-db.d.ts +24 -0
  199. package/dist/pipeline/handler-context-db.js +149 -0
  200. package/dist/pipeline/handler-context-dispatch.d.ts +21 -0
  201. package/dist/pipeline/handler-context-dispatch.js +89 -0
  202. package/dist/pipeline/handler-context-events.d.ts +43 -0
  203. package/dist/pipeline/handler-context-events.js +237 -0
  204. package/dist/pipeline/handler-context-notify.d.ts +12 -0
  205. package/dist/pipeline/handler-context-notify.js +55 -0
  206. package/dist/pipeline/handler-context-observability.d.ts +7 -0
  207. package/dist/pipeline/handler-context-observability.js +21 -0
  208. package/dist/pipeline/handler-context-scope.d.ts +48 -0
  209. package/dist/pipeline/handler-context-scope.js +88 -0
  210. package/dist/pipeline/notify-scope.js +12 -1
  211. package/dist/pipeline/pre-handler-gates.js +20 -4
  212. package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
  213. package/dist/pipeline/pre-transaction-reservations.js +9 -3
  214. package/dist/pipeline/projection-rebuild.js +2 -1
  215. package/dist/schema-cli/apply-command.d.ts +2 -0
  216. package/dist/schema-cli/apply-command.js +97 -0
  217. package/dist/schema-cli/baseline-command.d.ts +2 -0
  218. package/dist/schema-cli/baseline-command.js +33 -0
  219. package/dist/schema-cli/generate-command.d.ts +2 -0
  220. package/dist/schema-cli/generate-command.js +90 -0
  221. package/dist/schema-cli/shared.d.ts +39 -0
  222. package/dist/schema-cli/shared.js +95 -0
  223. package/dist/schema-cli/status-command.d.ts +2 -0
  224. package/dist/schema-cli/status-command.js +73 -0
  225. package/dist/schema-cli/validate-boot-input.d.ts +5 -0
  226. package/dist/schema-cli/validate-boot-input.js +130 -0
  227. package/dist/schema-cli/validate-command.d.ts +2 -0
  228. package/dist/schema-cli/validate-command.js +130 -0
  229. package/dist/schema-cli.d.ts +2 -18
  230. package/dist/schema-cli.js +47 -604
  231. package/dist/search/meilisearch-adapter.js +2 -1
  232. package/dist/upgrade-cli.js +149 -121
  233. package/package.json +4 -4
  234. package/src/changes.json +310 -0
  235. package/src/scripts/codemod/feature-layout.ts +10 -2
  236. package/src/scripts/codemod/object-form.ts +41 -4
  237. package/src/scripts/codemod/package-merge.ts +58 -8
  238. package/src/scripts/codemod/pii-personal-migration.ts +175 -145
  239. package/src/scripts/codemod/testing-injection.ts +122 -42
  240. package/src/scripts/codemod/testing-move.ts +29 -7
@@ -177,50 +177,40 @@ function reportRawSubjectLiterals(
177
177
  }
178
178
  }
179
179
 
180
- // kumiko-lint-ignore complexity-budget migration codemod, one-time script — splitting would add risk without benefit
181
- function processObjectLiteral(obj: ObjectLiteralExpression, factoryName: string): void {
182
- // skip: field already carries `personal` — already migrated, idempotent no-op
183
- if (obj.getProperty("personal")) return;
184
-
185
- const piiEncryptedProp = obj.getProperty("piiEncrypted");
186
- if (piiEncryptedProp) {
187
- report(
188
- piiEncryptedProp,
189
- "piiEncrypted: true on an entity field (removed from the type system) — needs a human decision, not a mechanical mapping",
190
- );
191
- // skip: reported above — piiEncrypted needs a human decision, not a mechanical mapping
192
- return;
193
- }
180
+ type SubjectProp = { readonly name: SubjectFlagName; readonly prop: PropertyAssignment };
181
+ type PersonalInit = { readonly personal: string; readonly reason: string | undefined };
182
+ type FindSupport = "text" | "longText" | "none";
183
+ type FindabilityProps = {
184
+ readonly lookupable: PropertyAssignment | undefined;
185
+ readonly searchable: PropertyAssignment | undefined;
186
+ readonly sensitive: PropertyAssignment | undefined;
187
+ };
194
188
 
189
+ function singleSubjectFlag(obj: ObjectLiteralExpression): SubjectProp | undefined {
195
190
  const subjectProps = SUBJECT_FLAG_NAMES.map((n) => ({ name: n, prop: findProp(obj, n) })).filter(
196
191
  (x): x is { name: SubjectFlagName; prop: PropertyAssignment } => !!x.prop,
197
192
  );
198
- const lookupableProp = findProp(obj, "lookupable");
199
- const searchableProp = findProp(obj, "searchable");
200
- const sensitiveProp = findProp(obj, "sensitive");
201
- const anonymizeProp = obj.getProperty("anonymize");
202
-
203
193
  if (subjectProps.length === 0) {
204
194
  // `searchable`/`sensitive` alone remain valid, unrelated FieldDef
205
195
  // properties — left untouched. `lookupable` alone can never compile
206
196
  // (always an excess property without a `personal` to attach `find`
207
197
  // to). `anonymize` alone has no PersonalAnnotations arm to live on.
198
+ const lookupableProp = findProp(obj, "lookupable");
208
199
  if (lookupableProp) {
209
200
  report(
210
201
  lookupableProp,
211
202
  "lookupable without any subject annotation — no `personal` to attach `find` to",
212
203
  );
213
204
  }
205
+ const anonymizeProp = obj.getProperty("anonymize");
214
206
  if (anonymizeProp) {
215
207
  report(
216
208
  anonymizeProp,
217
209
  "anonymize without any subject annotation — no PersonalAnnotations arm accepts anonymize alone",
218
210
  );
219
211
  }
220
- // skip: no subject flag on this field — stray lookupable/anonymize already reported above if present
221
- return;
212
+ return undefined;
222
213
  }
223
-
224
214
  if (subjectProps.length > 1) {
225
215
  const first = subjectProps[0];
226
216
  if (first) {
@@ -229,152 +219,154 @@ function processObjectLiteral(obj: ObjectLiteralExpression, factoryName: string)
229
219
  `multiple subject annotations on one field (${subjectProps.map((s) => s.name).join(", ")}) — needs a human decision on which subject is correct`,
230
220
  );
231
221
  }
232
- // skip: reported above — multiple subject annotations need a human call on which one wins
233
- return;
234
- }
235
-
236
- const subject = subjectProps[0];
237
- if (!subject) {
238
- // skip: length===0 and length>1 already returned — empty [0] is unreachable
239
- return;
222
+ return undefined;
240
223
  }
224
+ return subjectProps[0];
225
+ }
241
226
 
227
+ function findSupportOf(factoryName: string, subject: SubjectProp): FindSupport | undefined {
242
228
  if (NO_PERSONAL_FACTORIES.has(factoryName)) {
243
229
  report(
244
230
  subject.prop,
245
231
  `${factoryName} has no personal-annotation support — subject flag "${subject.name}" cannot be expressed here`,
246
232
  );
247
- // skip: reported above — this factory has no personal-annotation support to migrate into
248
- return;
249
- }
250
- const isTextFind = TEXT_FIND_FACTORIES.has(factoryName);
251
- const isLongTextFind = LONGTEXT_FIND_FACTORIES.has(factoryName);
252
- const isNoFind = NO_FIND_FACTORIES.has(factoryName);
253
- if (!isTextFind && !isLongTextFind && !isNoFind) {
254
- report(
255
- subject.prop,
256
- `unrecognized field factory "${factoryName}" — cannot verify its PersonalAnnotations shape`,
257
- );
258
- // skip: reported above — unknown factory shape, cannot verify safely
259
- return;
233
+ return undefined;
234
+ }
235
+ if (TEXT_FIND_FACTORIES.has(factoryName)) return "text";
236
+ if (LONGTEXT_FIND_FACTORIES.has(factoryName)) return "longText";
237
+ if (NO_FIND_FACTORIES.has(factoryName)) return "none";
238
+ report(
239
+ subject.prop,
240
+ `unrecognized field factory "${factoryName}" — cannot verify its PersonalAnnotations shape`,
241
+ );
242
+ return undefined;
243
+ }
244
+
245
+ function personalFromTrueFlag(
246
+ personal: string,
247
+ ): (subject: SubjectProp) => PersonalInit | undefined {
248
+ return (subject) => {
249
+ if (isTrueLiteral(subject.prop.getInitializer())) return { personal, reason: undefined };
250
+ report(subject.prop, `${subject.name} is set to a non-\`true\` value — cannot infer intent`);
251
+ return undefined;
252
+ };
253
+ }
254
+
255
+ function personalFromAllowPlaintext(subject: SubjectProp): PersonalInit | undefined {
256
+ const init = subject.prop.getInitializer();
257
+ if (!init) {
258
+ report(subject.prop, "allowPlaintext has no value");
259
+ return undefined;
260
260
  }
261
+ const reason = Node.isStringLiteral(init)
262
+ ? `"${normalizeReasonSlug(init.getLiteralText())}"`
263
+ : init.getText();
264
+ return { personal: "false", reason };
265
+ }
261
266
 
262
- let personalInit: string;
263
- let reasonInit: string | undefined;
264
- if (subject.name === "pii") {
265
- if (!isTrueLiteral(subject.prop.getInitializer())) {
266
- report(subject.prop, "pii is set to a non-`true` value — cannot infer intent");
267
- // skip: reported above — non-`true` pii value, can't infer intent
268
- return;
269
- }
270
- personalInit = `"self"`;
271
- } else if (subject.name === "tenantOwned") {
272
- if (!isTrueLiteral(subject.prop.getInitializer())) {
273
- report(subject.prop, "tenantOwned is set to a non-`true` value — cannot infer intent");
274
- // skip: reported above — non-`true` tenantOwned value, can't infer intent
275
- return;
276
- }
277
- personalInit = `"tenant"`;
278
- } else if (subject.name === "subjectRef") {
279
- if (!isTrueLiteral(subject.prop.getInitializer())) {
280
- report(subject.prop, "subjectRef is set to a non-`true` value — cannot infer intent");
281
- // skip: reported above — non-`true` subjectRef value, can't infer intent
282
- return;
283
- }
284
- personalInit = `"ref"`;
285
- } else if (subject.name === "allowPlaintext") {
286
- const init = subject.prop.getInitializer();
287
- if (!init) {
288
- report(subject.prop, "allowPlaintext has no value");
289
- // skip: reported above — allowPlaintext has no value to migrate
290
- return;
291
- }
292
- personalInit = "false";
293
- reasonInit = Node.isStringLiteral(init)
294
- ? `"${normalizeReasonSlug(init.getLiteralText())}"`
295
- : init.getText();
296
- } else {
297
- const init = subject.prop.getInitializer();
298
- if (!init || !Node.isObjectLiteralExpression(init)) {
299
- report(subject.prop, "userOwned value is not an object literal — cannot extract ownerField");
300
- // skip: reported above — userOwned value isn't an object literal, can't extract ownerField
301
- return;
302
- }
303
- const ownerFieldProp = findProp(init, "ownerField");
304
- const ownerFieldInit = ownerFieldProp?.getInitializer();
305
- if (!ownerFieldInit) {
306
- report(subject.prop, 'userOwned is missing an "ownerField" property');
307
- // skip: reported above — userOwned is missing its ownerField property
308
- return;
309
- }
310
- personalInit = `{ of: ${ownerFieldInit.getText()} }`;
267
+ function personalFromUserOwned(subject: SubjectProp): PersonalInit | undefined {
268
+ const init = subject.prop.getInitializer();
269
+ if (!init || !Node.isObjectLiteralExpression(init)) {
270
+ report(subject.prop, "userOwned value is not an object literal — cannot extract ownerField");
271
+ return undefined;
272
+ }
273
+ const ownerFieldInit = findProp(init, "ownerField")?.getInitializer();
274
+ if (!ownerFieldInit) {
275
+ report(subject.prop, 'userOwned is missing an "ownerField" property');
276
+ return undefined;
311
277
  }
278
+ return { personal: `{ of: ${ownerFieldInit.getText()} }`, reason: undefined };
279
+ }
312
280
 
313
- const subjectIsRefOrPlaintext =
314
- subject.name === "subjectRef" || subject.name === "allowPlaintext";
281
+ // One row per subject flag of the mapping table in the file header; a row
282
+ // reports and returns undefined when the flag value cannot be mapped.
283
+ const PERSONAL_INIT_BY_SUBJECT: Record<
284
+ SubjectFlagName,
285
+ (subject: SubjectProp) => PersonalInit | undefined
286
+ > = {
287
+ pii: personalFromTrueFlag(`"self"`),
288
+ userOwned: personalFromUserOwned,
289
+ tenantOwned: personalFromTrueFlag(`"tenant"`),
290
+ subjectRef: personalFromTrueFlag(`"ref"`),
291
+ allowPlaintext: personalFromAllowPlaintext,
292
+ };
315
293
 
316
- if (subjectIsRefOrPlaintext) {
317
- // These PersonalAnnotations arms carry no `find` field at all.
318
- if (lookupableProp) {
319
- report(
320
- lookupableProp,
321
- `lookupable combined with personal:${subject.name === "subjectRef" ? '"ref"' : "false"} — findability doesn't apply to this subject, table has no mapping`,
322
- );
323
- // skip: reported above — lookupable doesn't apply to this subject, no mapping to migrate
324
- return;
325
- }
326
- applyTransform(obj, [subject.prop], {
327
- personal: personalInit,
328
- reason: reasonInit,
329
- find: undefined,
330
- });
331
- counts[subject.name === "subjectRef" ? "ref" : "personal-false"]++;
332
- // skip: already transformed above — nothing left to check for this subject
294
+ // These PersonalAnnotations arms carry no `find` field at all.
295
+ function migrateFindlessSubject(
296
+ obj: ObjectLiteralExpression,
297
+ subject: SubjectProp,
298
+ personal: PersonalInit,
299
+ lookupableProp: PropertyAssignment | undefined,
300
+ ): void {
301
+ const isRef = subject.name === "subjectRef";
302
+ if (lookupableProp) {
303
+ report(
304
+ lookupableProp,
305
+ `lookupable combined with personal:${isRef ? '"ref"' : "false"} — findability doesn't apply to this subject, table has no mapping`,
306
+ );
307
+ // skip: reported above — lookupable doesn't apply to this subject, no mapping to migrate
333
308
  return;
334
309
  }
310
+ applyTransform(obj, [subject.prop], {
311
+ personal: personal.personal,
312
+ reason: personal.reason,
313
+ find: undefined,
314
+ });
315
+ counts[isRef ? "ref" : "personal-false"]++;
316
+ }
335
317
 
336
- if (isNoFind) {
337
- const stray = lookupableProp ?? searchableProp ?? sensitiveProp;
338
- if (stray) {
339
- report(
340
- stray,
341
- `${factoryName} has no \`find\` in its PersonalAnnotations — findability flag present on a non-text field`,
342
- );
343
- // skip: reported above — findability flag on a non-text field, no valid find to attach
344
- return;
345
- }
346
- applyTransform(obj, [subject.prop], {
347
- personal: personalInit,
348
- reason: undefined,
349
- find: undefined,
350
- });
351
- counts["no-find"]++;
352
- // skip: already transformed above — no-find factory has nothing further to check
318
+ function migrateNoFindField(
319
+ obj: ObjectLiteralExpression,
320
+ factoryName: string,
321
+ subject: SubjectProp,
322
+ personal: PersonalInit,
323
+ findability: FindabilityProps,
324
+ ): void {
325
+ const stray = findability.lookupable ?? findability.searchable ?? findability.sensitive;
326
+ if (stray) {
327
+ report(
328
+ stray,
329
+ `${factoryName} has no \`find\` in its PersonalAnnotations — findability flag present on a non-text field`,
330
+ );
331
+ // skip: reported above — findability flag on a non-text field, no valid find to attach
353
332
  return;
354
333
  }
334
+ applyTransform(obj, [subject.prop], {
335
+ personal: personal.personal,
336
+ reason: undefined,
337
+ find: undefined,
338
+ });
339
+ counts["no-find"]++;
340
+ }
355
341
 
356
- const hasLookupable = !!lookupableProp;
357
- const hasSearchable = !!searchableProp;
358
- const hasSensitive = !!sensitiveProp;
342
+ // lookupable + searchable and searchable alone both map to "fuzzy".
343
+ function findValueOf(findability: FindabilityProps): "exact" | "fuzzy" | "none" | "secret" {
344
+ if (findability.sensitive) return "secret";
345
+ if (findability.searchable) return "fuzzy";
346
+ if (findability.lookupable) return "exact";
347
+ return "none";
348
+ }
359
349
 
360
- if (sensitiveProp && (hasLookupable || hasSearchable)) {
350
+ function migrateTextFindField(
351
+ obj: ObjectLiteralExpression,
352
+ subject: SubjectProp,
353
+ personal: PersonalInit,
354
+ findability: FindabilityProps,
355
+ support: Exclude<FindSupport, "none">,
356
+ ): void {
357
+ const { lookupable, searchable, sensitive } = findability;
358
+ if (sensitive && (lookupable || searchable)) {
361
359
  report(
362
- sensitiveProp,
360
+ sensitive,
363
361
  'sensitive combined with lookupable/searchable — two find values ("secret" vs "exact"/"fuzzy"), needs a human decision',
364
362
  );
365
363
  // skip: reported above — sensitive plus lookupable/searchable is an ambiguous find value
366
364
  return;
367
365
  }
368
366
 
369
- let find: "exact" | "fuzzy" | "none" | "secret";
370
- if (hasSensitive) find = "secret";
371
- else if (hasLookupable && hasSearchable) find = "fuzzy";
372
- else if (hasLookupable) find = "exact";
373
- else if (hasSearchable) find = "fuzzy";
374
- else find = "none";
375
-
376
- if (isLongTextFind && (find === "exact" || find === "fuzzy")) {
377
- const findSite = lookupableProp ?? searchableProp;
367
+ const find = findValueOf(findability);
368
+ if (support === "longText" && (find === "exact" || find === "fuzzy")) {
369
+ const findSite = lookupable ?? searchable;
378
370
  if (findSite) {
379
371
  report(
380
372
  findSite,
@@ -388,7 +380,7 @@ function processObjectLiteral(obj: ObjectLiteralExpression, factoryName: string)
388
380
  // searchable-alone -> "fuzzy" makes expandPersonalAnnotations add
389
381
  // `lookupable: true`, which the field didn't carry before — that's a
390
382
  // new `_bidx` column (DDL migration), tracked separately for the report.
391
- if (hasSearchable && !hasLookupable) {
383
+ if (searchable && !lookupable) {
392
384
  newLookupableSites.push({
393
385
  file: obj.getSourceFile().getFilePath(),
394
386
  line: subject.prop.getStartLineNumber(),
@@ -397,13 +389,51 @@ function processObjectLiteral(obj: ObjectLiteralExpression, factoryName: string)
397
389
  });
398
390
  }
399
391
 
400
- const removedProps = [subject.prop, lookupableProp, searchableProp, sensitiveProp].filter(
392
+ const removedProps = [subject.prop, lookupable, searchable, sensitive].filter(
401
393
  (p): p is PropertyAssignment => !!p,
402
394
  );
403
- applyTransform(obj, removedProps, { personal: personalInit, reason: undefined, find });
395
+ applyTransform(obj, removedProps, { personal: personal.personal, reason: undefined, find });
404
396
  counts[find]++;
405
397
  }
406
398
 
399
+ function processObjectLiteral(obj: ObjectLiteralExpression, factoryName: string): void {
400
+ // skip: field already carries `personal` — already migrated, idempotent no-op
401
+ if (obj.getProperty("personal")) return;
402
+
403
+ const piiEncryptedProp = obj.getProperty("piiEncrypted");
404
+ if (piiEncryptedProp) {
405
+ report(
406
+ piiEncryptedProp,
407
+ "piiEncrypted: true on an entity field (removed from the type system) — needs a human decision, not a mechanical mapping",
408
+ );
409
+ // skip: reported above — piiEncrypted needs a human decision, not a mechanical mapping
410
+ return;
411
+ }
412
+
413
+ const subject = singleSubjectFlag(obj);
414
+ // skip: no subject flag, or several, reported by singleSubjectFlag where needed
415
+ if (!subject) return;
416
+ const support = findSupportOf(factoryName, subject);
417
+ // skip: factory without a verifiable PersonalAnnotations shape, reported by findSupportOf
418
+ if (!support) return;
419
+ const personal = PERSONAL_INIT_BY_SUBJECT[subject.name](subject);
420
+ // skip: flag value cannot be mapped, reported by its PERSONAL_INIT_BY_SUBJECT row
421
+ if (!personal) return;
422
+
423
+ const findability: FindabilityProps = {
424
+ lookupable: findProp(obj, "lookupable"),
425
+ searchable: findProp(obj, "searchable"),
426
+ sensitive: findProp(obj, "sensitive"),
427
+ };
428
+ if (subject.name === "subjectRef" || subject.name === "allowPlaintext") {
429
+ migrateFindlessSubject(obj, subject, personal, findability.lookupable);
430
+ } else if (support === "none") {
431
+ migrateNoFindField(obj, factoryName, subject, personal, findability);
432
+ } else {
433
+ migrateTextFindField(obj, subject, personal, findability, support);
434
+ }
435
+ }
436
+
407
437
  function applyTransform(
408
438
  obj: ObjectLiteralExpression,
409
439
  removedProps: readonly PropertyAssignment[],
@@ -12,7 +12,14 @@
12
12
  // Without a mode flag it writes (kumiko-upgrade --apply calls it that way). --check exits 1
13
13
  // while any rewritable or manual site is left; --dry-run only reports.
14
14
 
15
- import { type ImportDeclaration, Project, SyntaxKind } from "ts-morph";
15
+ import {
16
+ type ExportDeclaration,
17
+ type ExportSpecifier,
18
+ type ImportDeclaration,
19
+ type ImportSpecifier,
20
+ Project,
21
+ SyntaxKind,
22
+ } from "ts-morph";
16
23
  import {
17
24
  type CodemodResult,
18
25
  ensureNamedImport,
@@ -64,6 +71,115 @@ function reportNamespaceUses(
64
71
  }
65
72
  }
66
73
 
74
+ function reportStarReexport(
75
+ declaration: ExportDeclaration,
76
+ filePath: string,
77
+ result: SiteLists,
78
+ ): void {
79
+ result.manual.push({
80
+ file: filePath,
81
+ line: declaration.getStartLineNumber(),
82
+ description: `export * from ${declaration.getModuleSpecifierValue()}: re-export the moved names from ${TARGET_MODULE} by hand`,
83
+ });
84
+ }
85
+
86
+ function moveNamedReexports(
87
+ declaration: ExportDeclaration,
88
+ movedExports: readonly ExportSpecifier[],
89
+ filePath: string,
90
+ result: SiteLists,
91
+ ): void {
92
+ const line = declaration.getStartLineNumber();
93
+ const snapshots = movedExports.map((named) => ({
94
+ name: named.getName(),
95
+ alias: named.getAliasNode()?.getText(),
96
+ isTypeOnly: named.isTypeOnly(),
97
+ }));
98
+ for (const named of movedExports) named.remove();
99
+ const typeOnlyDeclaration = declaration.isTypeOnly();
100
+ if (declaration.getNamedExports().length === 0) declaration.remove();
101
+ declaration.getSourceFile().addExportDeclaration({
102
+ moduleSpecifier: TARGET_MODULE,
103
+ namedExports: snapshots,
104
+ isTypeOnly: typeOnlyDeclaration,
105
+ });
106
+ result.rewrites.push({
107
+ file: filePath,
108
+ line,
109
+ description: `export { ${snapshots.map((snapshot) => snapshot.name).join(", ")} } -> ${TARGET_MODULE}`,
110
+ });
111
+ }
112
+
113
+ function rewriteReexports(
114
+ declaration: ExportDeclaration,
115
+ moved: ReadonlySet<string>,
116
+ filePath: string,
117
+ result: SiteLists,
118
+ ): void {
119
+ const namedExports = declaration.getNamedExports();
120
+ const movedExports = namedExports.filter((named) => moved.has(named.getName()));
121
+ if (namedExports.length === 0) reportStarReexport(declaration, filePath, result);
122
+ else if (movedExports.length > 0) moveNamedReexports(declaration, movedExports, filePath, result);
123
+ }
124
+
125
+ function moveNamedImports(
126
+ declaration: ImportDeclaration,
127
+ movedImports: readonly ImportSpecifier[],
128
+ filePath: string,
129
+ result: SiteLists,
130
+ ): void {
131
+ const line = declaration.getStartLineNumber();
132
+ const typeOnlyDeclaration = declaration.isTypeOnly();
133
+ const sourceFile = declaration.getSourceFile();
134
+ const names = movedImports.map((named) => named.getName());
135
+ // Collected first: removing a specifier invalidates the nodes of its siblings.
136
+ const snapshots = movedImports.map((named) => ({
137
+ name: named.getName(),
138
+ alias: named.getAliasNode()?.getText(),
139
+ typeOnly: named.isTypeOnly(),
140
+ }));
141
+ for (const named of movedImports) named.remove();
142
+ if (
143
+ declaration.getNamedImports().length === 0 &&
144
+ declaration.getNamespaceImport() === undefined &&
145
+ declaration.getDefaultImport() === undefined
146
+ ) {
147
+ declaration.remove();
148
+ }
149
+ for (const snapshot of snapshots) {
150
+ const typeOnly = typeOnlyDeclaration || snapshot.typeOnly;
151
+ if (snapshot.alias === undefined) {
152
+ ensureNamedImport(sourceFile, TARGET_MODULE, snapshot.name, typeOnly);
153
+ } else {
154
+ sourceFile.addImportDeclaration({
155
+ moduleSpecifier: TARGET_MODULE,
156
+ namedImports: [{ name: snapshot.name, alias: snapshot.alias }],
157
+ isTypeOnly: typeOnly,
158
+ });
159
+ }
160
+ }
161
+ result.rewrites.push({
162
+ file: filePath,
163
+ line,
164
+ description: `${names.join(", ")} -> ${TARGET_MODULE}`,
165
+ });
166
+ }
167
+
168
+ function rewriteImportDeclaration(
169
+ declaration: ImportDeclaration,
170
+ filePath: string,
171
+ result: SiteLists,
172
+ ): void {
173
+ const moved = movedNamesOf(declaration);
174
+ if (moved !== undefined) {
175
+ reportNamespaceUses(declaration, moved, filePath, result);
176
+ const movedImports = declaration
177
+ .getNamedImports()
178
+ .filter((named) => moved.has(named.getName()));
179
+ if (movedImports.length > 0) moveNamedImports(declaration, movedImports, filePath, result);
180
+ }
181
+ }
182
+
67
183
  export function transformTestingInjectionSource(
68
184
  source: string,
69
185
  filePath = "source.ts",
@@ -77,47 +193,11 @@ export function transformTestingInjectionSource(
77
193
  });
78
194
  const sourceFile = project.createSourceFile(filePath, source);
79
195
  for (const declaration of sourceFile.getImportDeclarations()) {
80
- const moved = movedNamesOf(declaration);
81
- if (moved === undefined) continue;
82
- reportNamespaceUses(declaration, moved, filePath, result);
83
- const movedImports = declaration
84
- .getNamedImports()
85
- .filter((named) => moved.has(named.getName()));
86
- if (movedImports.length === 0) continue;
87
- const line = declaration.getStartLineNumber();
88
- const typeOnlyDeclaration = declaration.isTypeOnly();
89
- const names = movedImports.map((named) => named.getName());
90
- // Collected first: removing a specifier invalidates the nodes of its siblings.
91
- const snapshots = movedImports.map((named) => ({
92
- name: named.getName(),
93
- alias: named.getAliasNode()?.getText(),
94
- typeOnly: named.isTypeOnly(),
95
- }));
96
- for (const named of movedImports) named.remove();
97
- if (
98
- declaration.getNamedImports().length === 0 &&
99
- declaration.getNamespaceImport() === undefined &&
100
- declaration.getDefaultImport() === undefined
101
- ) {
102
- declaration.remove();
103
- }
104
- for (const snapshot of snapshots) {
105
- const typeOnly = typeOnlyDeclaration || snapshot.typeOnly;
106
- if (snapshot.alias === undefined) {
107
- ensureNamedImport(sourceFile, TARGET_MODULE, snapshot.name, typeOnly);
108
- } else {
109
- sourceFile.addImportDeclaration({
110
- moduleSpecifier: TARGET_MODULE,
111
- namedImports: [{ name: snapshot.name, alias: snapshot.alias }],
112
- isTypeOnly: typeOnly,
113
- });
114
- }
115
- }
116
- result.rewrites.push({
117
- file: filePath,
118
- line,
119
- description: `${names.join(", ")} -> ${TARGET_MODULE}`,
120
- });
196
+ rewriteImportDeclaration(declaration, filePath, result);
197
+ }
198
+ for (const declaration of sourceFile.getExportDeclarations()) {
199
+ const moved = MOVED_NAMES_BY_MODULE.get(declaration.getModuleSpecifierValue() ?? "");
200
+ if (moved !== undefined) rewriteReexports(declaration, moved, filePath, result);
121
201
  }
122
202
  const output = result.rewrites.length > 0 ? sourceFile.getFullText() : source;
123
203
  return { output, ...result };
@@ -172,7 +172,9 @@ function repointDeclaration(
172
172
  function isWholeModuleReference(declaration: NamedDeclaration): boolean {
173
173
  if (Node.isImportDeclaration(declaration)) {
174
174
  return (
175
- declaration.getNamespaceImport() !== undefined || declaration.getDefaultImport() !== undefined
175
+ declaration.getNamespaceImport() !== undefined ||
176
+ declaration.getDefaultImport() !== undefined ||
177
+ declaration.getNamedImports().length === 0
176
178
  );
177
179
  }
178
180
  return declaration.isNamespaceExport() || declaration.getNamedExports().length === 0;
@@ -186,14 +188,34 @@ function usesSplitName(
186
188
  const namespaceName = Node.isImportDeclaration(declaration)
187
189
  ? declaration.getNamespaceImport()?.getText()
188
190
  : undefined;
191
+ if (
192
+ Node.isImportDeclaration(declaration) &&
193
+ declaration.getDefaultImport() === undefined &&
194
+ namespaceName === undefined
195
+ ) {
196
+ return false; // side-effect import: nothing is used, so any package of the old module serves
197
+ }
189
198
  if (namespaceName === undefined) return true;
190
199
  return sourceFile
191
- .getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)
192
- .some(
193
- (access) =>
194
- access.getExpression().getText() === namespaceName &&
195
- targetForName(old, access.getName()) !== mainTarget(old),
196
- );
200
+ .getDescendantsOfKind(SyntaxKind.Identifier)
201
+ .some((identifier) => namespaceUseNeedsSplit(identifier, namespaceName, old));
202
+ }
203
+
204
+ // Conservative: any use other than `ns.name` / `ns.name` in a type position (destructuring,
205
+ // passing the namespace on, typeof) cannot be attributed to one target package.
206
+ function namespaceUseNeedsSplit(identifier: Node, namespaceName: string, old: OldModule): boolean {
207
+ if (identifier.getText() !== namespaceName) return false;
208
+ if (identifier.getFirstAncestorByKind(SyntaxKind.ImportDeclaration) !== undefined) return false;
209
+ const parent = identifier.getParent();
210
+ if (Node.isPropertyAccessExpression(parent)) {
211
+ if (parent.getNameNode() === identifier) return false;
212
+ return targetForName(old, parent.getName()) !== mainTarget(old);
213
+ }
214
+ if (Node.isQualifiedName(parent)) {
215
+ if (parent.getRight() === identifier) return false;
216
+ return targetForName(old, parent.getRight().getText()) !== mainTarget(old);
217
+ }
218
+ return true;
197
219
  }
198
220
 
199
221
  function repointWholeModule(