@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
@@ -111,7 +111,7 @@ export function buildAppSchema(registry, options = {}) {
111
111
  // instead so a leaked function renderer is actually caught.
112
112
  const offender = findNonJsonSafePath(schema, "schema");
113
113
  if (offender !== null) {
114
- log.error(`buildAppSchema: Output ist nicht JSON-safe — nicht-serialisierbarer Wert bei "${offender}" (Funktions-Renderer oder Klassen-Instanz im Schema?).`, { offender });
114
+ log.error(`buildAppSchema: output is not JSON-safe, non-serializable value at "${offender}" (a function renderer or class instance in the schema?).`, { offender });
115
115
  }
116
116
  }
117
117
  return schema;
@@ -446,136 +446,96 @@ function assertValidScreenMultilineRows(featureName, shortId, screen) {
446
446
  check(screen.confirm.fields);
447
447
  }
448
448
  }
449
- // Per-field whitelist. Every forwarded value must be JSON-safe (literal, or
450
- // an array/plain-object built only from JSON-safe values) — even though the
451
- // FieldDefinition types only type `default` as a literal, the sample-authoring
452
- // pattern `as unknown as EntityDefinition` has already let function-valued
453
- // defaults slip through. This whitelist is defense-in-depth that catches them
454
- // BEFORE JSON.stringify silently drops them in the browser-injection pipeline.
455
- // The same check gates the structured properties below (`multiline`,
456
- // `derived`, `totals`, `totalsMatch`) — a top-level `Array.isArray`/plain-
457
- // object check alone would let a function survive one level deep.
458
- //
459
- // Cast at the exit `as EntityFieldDefinition`: type-system-wise our out-map only
460
- // satisfies the discriminated union with an unnarrowed `type` string — the
461
- // cast bridges the variant inference TS can't recompute from a generic
462
- // Record.
463
- // kumiko-lint-ignore complexity-budget flat per-property whitelist walk, one independent `if` per FieldDefinition property — same shape as schema-builder.ts's field→zod switch
464
- function projectField(fieldDef) {
465
- const def = fieldDef; // @cast-boundary schema-walk
466
- const out = {};
467
- if (typeof def["type"] === "string")
468
- out["type"] = def["type"];
469
- if (typeof def["required"] === "boolean")
470
- out["required"] = def["required"];
471
- if (typeof def["sortable"] === "boolean")
472
- out["sortable"] = def["sortable"];
449
+ const isStringValue = (value) => typeof value === "string";
450
+ const isBooleanValue = (value) => typeof value === "boolean";
451
+ const isNumberValue = (value) => typeof value === "number";
452
+ const isTrueValue = (value) => value === true;
453
+ const isNumberOrStringValue = (value) => typeof value === "number" || typeof value === "string";
454
+ const isBooleanOrJsonSafeObject = (value) => typeof value === "boolean" || isJsonSafeObject(value);
455
+ const isUnitSpec = (value) => typeof value === "string" || (isPlainObject(value) && typeof value["field"] === "string");
456
+ // Forwarded in this order, so the client schema keeps its key order.
457
+ const FORWARDED_FIELD_PROPERTIES = [
458
+ ["type", isStringValue],
459
+ ["required", isBooleanValue],
460
+ ["sortable", isBooleanValue],
473
461
  // filterable controls the faceted filter dropdowns in the renderer (select/
474
462
  // boolean), so it must be included in the client schema.
475
- if (typeof def["filterable"] === "boolean")
476
- out["filterable"] = def["filterable"];
477
- if (typeof def["searchable"] === "boolean")
478
- out["searchable"] = def["searchable"];
479
- if (isJsonSafeValue(def["default"]))
480
- out["default"] = def["default"];
463
+ ["filterable", isBooleanValue],
464
+ ["searchable", isBooleanValue],
465
+ ["default", isJsonSafeValue],
481
466
  // Select: the options list is plain JSON, so forward it.
482
- if (Array.isArray(def["options"]))
483
- out["options"] = def["options"];
484
- if (isJsonSafeObject(def["optionTones"]))
485
- out["optionTones"] = def["optionTones"];
467
+ ["options", Array.isArray],
468
+ ["optionTones", isJsonSafeObject],
486
469
  // Reference: entity target, labelField, and multiple are required by the
487
470
  // renderer. ReferenceInput builds the options query from refEntity/refFeature
488
471
  // and resolves the label through labelField; without these properties, the
489
472
  // dropdown is empty (the QN becomes `<feature>:query::list` → 404).
490
- if (typeof def["entity"] === "string")
491
- out["entity"] = def["entity"];
492
- if (typeof def["labelField"] === "string")
493
- out["labelField"] = def["labelField"];
473
+ ["entity", isStringValue],
474
+ ["labelField", isStringValue],
494
475
  // Author-supplied picker source — without it the client falls back to the list handler.
495
- if (typeof def["optionsQuery"] === "string")
496
- out["optionsQuery"] = def["optionsQuery"];
476
+ ["optionsQuery", isStringValue],
497
477
  // Select: tier/tenant availability of the static options, loaded by the renderer.
498
- if (typeof def["optionsAvailabilityQuery"] === "string")
499
- out["optionsAvailabilityQuery"] = def["optionsAvailabilityQuery"];
478
+ ["optionsAvailabilityQuery", isStringValue],
500
479
  // Select: options only offered while a sibling-field condition holds; the renderer filters, the write path enforces.
501
- if (Array.isArray(def["conditionalOptions"]))
502
- out["conditionalOptions"] = def["conditionalOptions"];
503
- if (typeof def["multiple"] === "boolean")
504
- out["multiple"] = def["multiple"];
480
+ ["conditionalOptions", Array.isArray],
481
+ ["multiple", isBooleanValue],
505
482
  // MultiSelect: display picks checkboxes vs. combobox in the renderer,
506
483
  // columns/maxRows size the checkbox grid — without these the renderer
507
484
  // always falls back to the combobox. Select: display picks
508
485
  // "radio" | "dropdown" the same way.
509
- if (typeof def["display"] === "string")
510
- out["display"] = def["display"];
511
- if (typeof def["columns"] === "number")
512
- out["columns"] = def["columns"];
513
- if (typeof def["maxRows"] === "number")
514
- out["maxRows"] = def["maxRows"];
486
+ ["display", isStringValue],
487
+ ["columns", isNumberValue],
488
+ ["maxRows", isNumberValue],
515
489
  // text/longText: textarea row count — DefaultInput renders a single-line
516
490
  // input otherwise.
517
- if (typeof def["multiline"] === "boolean" || isJsonSafeObject(def["multiline"]))
518
- out["multiline"] = def["multiline"];
491
+ ["multiline", isBooleanOrJsonSafeObject],
519
492
  // number: Zod write-boundary bounds. date/timestamp/locatedTimestamp:
520
493
  // ISO-string picker bounds — same keys, different literal type.
521
- if (typeof def["min"] === "number" || typeof def["min"] === "string")
522
- out["min"] = def["min"];
523
- if (typeof def["max"] === "number" || typeof def["max"] === "string")
524
- out["max"] = def["max"];
494
+ ["min", isNumberOrStringValue],
495
+ ["max", isNumberOrStringValue],
525
496
  // date/timestamp/locatedTimestamp: display/parsing locale override.
526
- if (typeof def["locale"] === "string")
527
- out["locale"] = def["locale"];
497
+ ["locale", isStringValue],
528
498
  // image/images: which camera a mobile capture opens.
529
- if (typeof def["capture"] === "string")
530
- out["capture"] = def["capture"];
499
+ ["capture", isStringValue],
531
500
  // number: display-only suffix, static or read from a sibling field of the row.
532
- const unit = def["unit"];
533
- if (typeof unit === "string" || (isPlainObject(unit) && typeof unit["field"] === "string"))
534
- out["unit"] = unit;
501
+ ["unit", isUnitSpec],
535
502
  // number/decimal/bigInt: `false` drops thousands separators (e.g. a year).
536
- if (typeof def["grouping"] === "boolean")
537
- out["grouping"] = def["grouping"];
503
+ ["grouping", isBooleanValue],
538
504
  // text: "password" masks the input and blocks URL prefill in the renderer.
539
- if (typeof def["format"] === "string")
540
- out["format"] = def["format"];
505
+ ["format", isStringValue],
541
506
  // Write-response redaction stays server-side; the renderer needs the flag to
542
507
  // refuse any prefill (URL or handoff) into the field.
543
- if (def["sensitive"] === true)
544
- out["sensitive"] = true;
508
+ ["sensitive", isTrueValue],
545
509
  // Without it the renderer shows a plain text input for a field whose value never leaves the server.
546
- if (def["writeOnly"] === true)
547
- out["writeOnly"] = true;
510
+ ["writeOnly", isTrueValue],
548
511
  // timestamp: the edit view-model derives its wall-clock input mode from this.
549
- if (typeof def["locatedBy"] === "string")
550
- out["locatedBy"] = def["locatedBy"];
512
+ ["locatedBy", isStringValue],
551
513
  // file/image/images: upload picker constraints; the first variant key picks
552
514
  // the preview variant.
553
- if (isJsonSafeArray(def["accept"]))
554
- out["accept"] = def["accept"];
555
- if (typeof def["maxSize"] === "string")
556
- out["maxSize"] = def["maxSize"];
557
- if (isJsonSafeObject(def["variants"]))
558
- out["variants"] = def["variants"];
515
+ ["accept", isJsonSafeArray],
516
+ ["maxSize", isStringValue],
517
+ ["variants", isJsonSafeObject],
559
518
  // decimal, incl. embedded sub-fields: rounding of derived embedded-list cells.
560
- if (typeof def["scale"] === "number")
561
- out["scale"] = def["scale"];
519
+ ["scale", isNumberValue],
562
520
  // money: declared currency source (MoneyCurrencySource) — without
563
521
  // it the renderer can't tell a "currency: { kind: 'tenant' }" field apart
564
522
  // from an undeclared one and always falls back to entity.defaultCurrency.
565
- if (isJsonSafeObject(def["currency"]))
566
- out["currency"] = def["currency"];
523
+ ["currency", isJsonSafeObject],
567
524
  // embedded lists: row-count bounds, computed cells, totals row, and the
568
525
  // sibling-money-field totals check.
569
- if (typeof def["minItems"] === "number")
570
- out["minItems"] = def["minItems"];
571
- if (typeof def["maxItems"] === "number")
572
- out["maxItems"] = def["maxItems"];
573
- if (isJsonSafeObject(def["derived"]))
574
- out["derived"] = def["derived"];
575
- if (isJsonSafeArray(def["totals"]))
576
- out["totals"] = def["totals"];
577
- if (isJsonSafeObject(def["totalsMatch"]))
578
- out["totalsMatch"] = def["totalsMatch"];
526
+ ["minItems", isNumberValue],
527
+ ["maxItems", isNumberValue],
528
+ ["derived", isJsonSafeObject],
529
+ ["totals", isJsonSafeArray],
530
+ ["totalsMatch", isJsonSafeObject],
531
+ ];
532
+ function projectField(fieldDef) {
533
+ const def = fieldDef; // @cast-boundary schema-walk
534
+ const out = {};
535
+ for (const [key, isForwarded] of FORWARDED_FIELD_PROPERTIES) {
536
+ if (isForwarded(def[key]))
537
+ out[key] = def[key];
538
+ }
579
539
  // embedded lists: the sub-field map itself — without it the client-side
580
540
  // edit view-model iterates `undefined` and the screen crashes.
581
541
  // Recursive per sub-field, NOT a blanket isJsonSafeValue gate: a single
@@ -48,9 +48,16 @@ export function defineFeature(name, setup, settings) {
48
48
  },
49
49
  requires: (() => {
50
50
  const fn = (...args) => {
51
- if (!args.every(isObjectArgument))
51
+ const [first, ...rest] = args;
52
+ if (rest.length > 0 || !isObjectArgument(first)) {
52
53
  throwObjectArgumentRequired(name, "requires");
53
- state.requires.push(...resolveFeatureNamesArgs(args));
54
+ }
55
+ // Untyped callers can pass `{ feature: [...] }`; without this the object would be
56
+ // registered as a dependency name.
57
+ if (!Array.isArray(first.features) || !first.features.every((f) => typeof f === "string")) {
58
+ throw new Error(`[Feature ${name}] r.requires() needs { features: string[] }`);
59
+ }
60
+ state.requires.push(...first.features);
54
61
  };
55
62
  return Object.assign(fn, {
56
63
  projection: (tableName) => {
@@ -57,6 +57,7 @@ export function defineWriteHandler(def,
57
57
  ...(def.unsafeSkipTransitionGuard && { unsafeSkipTransitionGuard: true }),
58
58
  ...(def.rateLimit && { rateLimit: def.rateLimit }),
59
59
  ...(def.additionalRateLimits && { additionalRateLimits: def.additionalRateLimits }),
60
+ ...(def.rateLimitCost && { rateLimitCost: def.rateLimitCost }),
60
61
  ...(def.escapeHatch && { escapeHatch: def.escapeHatch }),
61
62
  ...pickPreTransactionPhases(def),
62
63
  ...(def.tenantlessAnonymous && { tenantlessAnonymous: true }),
@@ -44,6 +44,19 @@ export declare const entityListSchema: z.ZodObject<{
44
44
  }>;
45
45
  value: z.ZodUnknown;
46
46
  }, z.core.$strip>>>;
47
+ aggregate: z.ZodOptional<z.ZodObject<{
48
+ groupBy: z.ZodOptional<z.ZodString>;
49
+ measures: z.ZodDefault<z.ZodArray<z.ZodObject<{
50
+ field: z.ZodString;
51
+ kind: z.ZodEnum<{
52
+ avg: "avg";
53
+ count: "count";
54
+ max: "max";
55
+ min: "min";
56
+ sum: "sum";
57
+ }>;
58
+ }, z.core.$strip>>>;
59
+ }, z.core.$strip>>;
47
60
  }, z.core.$strip>;
48
61
  export declare function defineEntityWriteHandler(name: string, entity: EntityDefinition, options: EntityWriteHandlerOptions): WriteHandlerDef;
49
62
  export declare function defineEntityQueryHandler(name: string, entity: EntityDefinition, options: EntityQueryHandlerOptions): QueryHandlerDef;
@@ -1,13 +1,16 @@
1
1
  import { ENTITY_CONVENTION_QUERY_BRAND } from "@cosmicdrift/kumiko-types/handlers";
2
+ import { listAggregatePayloadSchema, } from "@cosmicdrift/kumiko-types/list-aggregate-types";
2
3
  import * as z from "zod";
3
4
  import { collectReferenceFields, enrichRowWithReferences, enrichWithReferences, } from "../db/eagerload.js";
4
5
  import { createEventStoreExecutor } from "../db/event-store-executor.js";
6
+ import { answerListAggregate } from "../db/list-aggregate-labels.js";
5
7
  import { buildEntityTable } from "../db/table-builder.js";
6
8
  import { acknowledgeConventionCrossTenant, reportConventionCrossTenantUse, } from "../db/tenant-db.js";
7
9
  import { InternalError } from "../errors/index.js";
8
10
  import { isSystemIdentity } from "../pipeline/system-identity-switch.js";
9
11
  import { assertUnreachable } from "../utils/index.js";
10
12
  import { PAGED_QUERY_HANDLER_BRAND } from "./define-handler.js";
13
+ import { entityListRuntime } from "./entity-list-runtime-options.js";
11
14
  import { assertReason } from "./escape-hatch-reason.js";
12
15
  import { dropEmptyWriteOnlyValues } from "./field-access.js";
13
16
  import { instructionFieldNames } from "./instruction-fields.js";
@@ -105,6 +108,7 @@ export const entityListSchema = z.object({
105
108
  value: z.unknown(),
106
109
  }))
107
110
  .optional(),
111
+ aggregate: listAggregatePayloadSchema.optional(),
108
112
  });
109
113
  function parseHandlerName(name, validVerbs) {
110
114
  const colonIdx = name.indexOf(":");
@@ -390,28 +394,13 @@ export function defineEntityQueryHandler(name, entity, options) {
390
394
  // runtime override supplies it.
391
395
  const listPayload = query.payload; // @cast-boundary engine-payload
392
396
  const db = dbFor(ctx);
393
- const result = await executor.list(listPayload, query.user, db, {
394
- ...(ctx.searchAdapter !== undefined && { searchAdapter: ctx.searchAdapter }),
395
- ...(ctx.includeDeleted === true && { includeDeleted: true }),
396
- // Registry access only exists at request time, so the
397
- // searchable-reference cache + entity resolver travel through the
398
- // same runtime-override extension point as searchAdapter above.
399
- referenceSearch: {
400
- fields: ctx.registry.getSearchableReferences(entityName),
401
- resolveEntity: (name) => ctx.registry.getEntity(name),
402
- },
403
- referenceSort: {
404
- fields: ctx.registry.getSortableReferences(entityName),
405
- resolveEntity: (name) => ctx.registry.getEntity(name),
406
- },
407
- parentVisibility: { entities: ctx.registry.getAllEntities() },
408
- });
409
- const eagerLoadViewer = {
410
- user: query.user,
411
- parentVisibility: { entities: ctx.registry.getAllEntities() },
412
- };
397
+ const { runtimeOptions, eagerLoadViewer, resolveEntity } = entityListRuntime(ctx, entityName, query.user);
398
+ if (listPayload.aggregate !== undefined) {
399
+ return answerListAggregate(executor, { ...listPayload, aggregate: listPayload.aggregate }, query.user, db, runtimeOptions, { entity, resolveEntity, viewer: eagerLoadViewer });
400
+ }
401
+ const result = await executor.list(listPayload, query.user, db, runtimeOptions);
413
402
  const enrichedRows = hasRefFields
414
- ? await enrichWithReferences(result.rows, entity, (name) => ctx.registry.getEntity(name), db, eagerLoadViewer)
403
+ ? await enrichWithReferences(result.rows, entity, resolveEntity, db, eagerLoadViewer)
415
404
  : result.rows;
416
405
  return { ...result, rows: augmentDerivedFields(enrichedRows, entity) };
417
406
  };
@@ -0,0 +1,8 @@
1
+ import type { ListRuntimeOptions } from "@cosmicdrift/kumiko-types/event-store-executor-types";
2
+ import type { EagerLoadEntityResolver, EagerLoadViewer } from "../db/eagerload.ts";
3
+ import type { HandlerContext, SessionUser } from "./types/index.ts";
4
+ export declare function entityListRuntime(ctx: HandlerContext, entityName: string, user: SessionUser): {
5
+ readonly runtimeOptions: ListRuntimeOptions;
6
+ readonly eagerLoadViewer: EagerLoadViewer;
7
+ readonly resolveEntity: EagerLoadEntityResolver;
8
+ };
@@ -0,0 +1,18 @@
1
+ // Registry access only exists at request time, so the searchable/sortable-reference
2
+ // caches, the entity resolver and the join-row read gate travel through the
3
+ // runtime-override extension point of the executor verbs.
4
+ export function entityListRuntime(ctx, entityName, user) {
5
+ const parentVisibility = { entities: ctx.registry.getAllEntities() };
6
+ const resolveEntity = (name) => ctx.registry.getEntity(name);
7
+ return {
8
+ runtimeOptions: {
9
+ ...(ctx.searchAdapter !== undefined && { searchAdapter: ctx.searchAdapter }),
10
+ ...(ctx.includeDeleted === true && { includeDeleted: true }),
11
+ referenceSearch: { fields: ctx.registry.getSearchableReferences(entityName), resolveEntity },
12
+ referenceSort: { fields: ctx.registry.getSortableReferences(entityName), resolveEntity },
13
+ parentVisibility,
14
+ },
15
+ eagerLoadViewer: { user, parentVisibility },
16
+ resolveEntity,
17
+ };
18
+ }
@@ -33,6 +33,7 @@ const WRITE_HANDLER_KEY_KINDS = {
33
33
  unsafeSkipTransitionGuard: "modeled",
34
34
  rateLimit: "modeled",
35
35
  additionalRateLimits: "modeled",
36
+ rateLimitCost: "opaque",
36
37
  escapeHatch: "modeled",
37
38
  tenantlessAnonymous: "opaque",
38
39
  reserveBeforeTransaction: "opaque",
@@ -2,19 +2,36 @@ import { SyntaxKind } from "ts-morph";
2
2
  import { describeUnknownFieldType, findUnknownEntityFieldTypes } from "../entity-field-types.js";
3
3
  import { sourceLocationFromNode } from "../source-location.js";
4
4
  import { fail, isPlainObject, isRawRefSentinel, ok, readDataLiteralNode, readNameLiteral, readNameOrRef, } from "./shared.js";
5
+ function readBackingTableRef(property) {
6
+ const initializer = property.asKind(SyntaxKind.PropertyAssignment)?.getInitializer() ??
7
+ property.asKind(SyntaxKind.ShorthandPropertyAssignment)?.getNameNode();
8
+ const reference = initializer ? readDataLiteralNode(initializer) : undefined;
9
+ return isRawRefSentinel(reference) ? reference : undefined;
10
+ }
5
11
  // `{ name: "x", ...xEntity }` — the definition is a reference the parser cannot
6
12
  // inline, so it is kept as a raw-ref sentinel and re-rendered as the same spread.
7
13
  // `backingTable` is runtime wiring (a table identifier), not part of the
8
- // definition data, so it does not take part in the shape check.
14
+ // definition data, so it is carried next to the definition and skipped by the shape check.
9
15
  function readSpreadOnlyDefinition(obj) {
10
- const properties = obj
11
- .getProperties()
12
- .filter((property) => property.asKind(SyntaxKind.PropertyAssignment)?.getName() !== "backingTable");
16
+ const isBackingTable = (property) => {
17
+ const name = property.asKind(SyntaxKind.PropertyAssignment)?.getName() ??
18
+ property.asKind(SyntaxKind.ShorthandPropertyAssignment)?.getName();
19
+ return name === "backingTable";
20
+ };
21
+ const allProperties = obj.getProperties();
22
+ const properties = allProperties.filter((property) => !isBackingTable(property));
13
23
  const spread = properties.length === 2 ? properties[1]?.asKind(SyntaxKind.SpreadAssignment) : undefined;
14
24
  if (!spread)
15
25
  return undefined;
16
26
  const reference = readDataLiteralNode(spread.getExpression());
17
- return isRawRefSentinel(reference) ? reference : undefined;
27
+ if (!isRawRefSentinel(reference))
28
+ return undefined;
29
+ const backingTableProperty = allProperties.find(isBackingTable);
30
+ const backingTable = backingTableProperty ? readBackingTableRef(backingTableProperty) : undefined;
31
+ // A backingTable that is not a plain reference cannot be re-rendered; fall back to the data reader.
32
+ if (backingTableProperty && !backingTable)
33
+ return undefined;
34
+ return { definition: reference, backingTable };
18
35
  }
19
36
  export function extractEntity(call, sourceFile) {
20
37
  const args = call.getArguments();
@@ -32,7 +49,8 @@ export function extractEntity(call, sourceFile) {
32
49
  if (!nameInit) {
33
50
  return fail("entity", sourceLocationFromNode(call, sourceFile), "object form requires a string-literal `name` property");
34
51
  }
35
- const definition = readSpreadOnlyDefinition(obj) ?? readDataLiteralNode(obj);
52
+ const spreadOnly = readSpreadOnlyDefinition(obj);
53
+ const definition = spreadOnly?.definition ?? readDataLiteralNode(obj);
36
54
  if (!isPlainObject(definition)) {
37
55
  return fail("entity", sourceLocationFromNode(call, sourceFile), "definition could not be read as a plain object (contains functions or identifiers)");
38
56
  }
@@ -49,6 +67,7 @@ export function extractEntity(call, sourceFile) {
49
67
  // Field-type membership is checked above; the cast is the extractor's
50
68
  // existing narrowing contract (readDataLiteralNode returns unknown).
51
69
  definition: defWithoutName,
70
+ ...(spreadOnly?.backingTable && { backingTable: spreadOnly.backingTable }),
52
71
  });
53
72
  }
54
73
  const entityName = readNameLiteral(first);
@@ -33,7 +33,8 @@
33
33
  // roadmap C-Notes for the canonical-comment-attach Pattern that would
34
34
  // preserve prefixed `// kumiko-comment:` markers across roundtrips.
35
35
  import { SyntaxKind, } from "ts-morph";
36
- import { readNameLiteral, readNameOrRef, readPropertyKey, resolveSameFileObjectLiteral, } from "./extractors/shared.js";
36
+ import { readPropertyKey } from "./extractors/shared.js";
37
+ import { callMatchesId } from "./pattern-id-matchers.js";
37
38
  import { indent, PATTERN_INDENT, renderPattern, renderValue } from "./render.js";
38
39
  /**
39
40
  * Apply a sequence of changes to the source file in-place. The list is
@@ -523,7 +524,7 @@ function findCallForId(sourceFile, id) {
523
524
  continue;
524
525
  if (propAccess.getName() !== id.kind)
525
526
  continue;
526
- if (callMatchesId(call, id))
527
+ if (callMatchesId(call, id.kind, id))
527
528
  matches.push(call);
528
529
  }
529
530
  if (SINGLETON_KINDS.has(id.kind) && matches.length > 1) {
@@ -536,28 +537,6 @@ const AI_STEP_FACTORY = {
536
537
  "ai.extract": "aiExtractStep",
537
538
  "ai.classify": "aiClassifyStep",
538
539
  };
539
- function readAiStepKey(call) {
540
- const arg = call.getArguments()[0];
541
- if (!arg)
542
- return undefined;
543
- const obj = resolveSameFileObjectLiteral(arg);
544
- if (!obj)
545
- return undefined;
546
- const prop = obj.getProperty("stepKey");
547
- if (!prop)
548
- return undefined;
549
- const assign = prop.asKind(SyntaxKind.PropertyAssignment);
550
- if (assign) {
551
- const init = assign.getInitializer();
552
- if (!init)
553
- return undefined;
554
- return readNameLiteral(init);
555
- }
556
- const shorthand = prop.asKind(SyntaxKind.ShorthandPropertyAssignment);
557
- if (shorthand)
558
- return readNameLiteral(shorthand.getNameNode());
559
- return undefined;
560
- }
561
540
  function findAiStepCall(sourceFile, id) {
562
541
  if (!isAiStepId(id))
563
542
  return undefined;
@@ -566,7 +545,7 @@ function findAiStepCall(sourceFile, id) {
566
545
  for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
567
546
  if (call.getExpression().getText() !== factory)
568
547
  continue;
569
- if (callMatchesId(call, id))
548
+ if (callMatchesId(call, id.kind, id))
570
549
  matches.push(call);
571
550
  }
572
551
  if (matches.length > 1) {
@@ -574,163 +553,6 @@ function findAiStepCall(sourceFile, id) {
574
553
  }
575
554
  return matches[0];
576
555
  }
577
- function callMatchesId(call, id) {
578
- switch (id.kind) {
579
- // Singletons: kind alone identifies the call.
580
- case "requires":
581
- case "optionalRequires":
582
- case "readsConfig":
583
- case "systemScope":
584
- case "toggleable":
585
- case "describe":
586
- case "uiHints":
587
- case "config":
588
- case "translations":
589
- case "authClaims":
590
- case "treeActions":
591
- return true;
592
- case "entity":
593
- return (matchFirstArgString(call, id.entityName) || matchObjectProperty(call, "name", id.entityName));
594
- case "relation":
595
- // Positional: r.relation(entity, name, def) | Object: { entity, name, ... }
596
- if (matchFirstArgString(call, id.entityName)) {
597
- return matchArgString(call, 1, id.relationName);
598
- }
599
- return (matchObjectProperty(call, "entity", id.entityName) &&
600
- matchObjectProperty(call, "name", id.relationName));
601
- case "nav":
602
- case "workspace":
603
- case "screen":
604
- return matchObjectProperty(call, "id", id.id);
605
- case "writeHandler":
606
- case "queryHandler":
607
- case "streamHandler":
608
- return (matchFirstArgString(call, id.handlerName) ||
609
- matchObjectProperty(call, "name", id.handlerName));
610
- case "hook":
611
- // Positional: r.hook(type, target, fn) | Object: { type, target }
612
- if (typeof id.target === "object") {
613
- // Entity-wide { allOf } target — only ever appears as the object
614
- // literal { allOf: entity } (positional or nested in the object
615
- // form), never as a bare string, so check both call shapes.
616
- if (matchFirstArgString(call, id.hookType)) {
617
- return matchAllOfArg(call.getArguments()[1], id.target.allOf);
618
- }
619
- return (matchObjectProperty(call, "type", id.hookType) &&
620
- matchObjectAllOfProperty(call, "target", id.target.allOf));
621
- }
622
- if (matchFirstArgString(call, id.hookType)) {
623
- return matchArgNameOrRef(call, 1, id.target);
624
- }
625
- return (matchObjectProperty(call, "type", id.hookType) &&
626
- matchObjectProperty(call, "target", id.target));
627
- case "metric":
628
- case "secret":
629
- case "secretNamespace":
630
- case "claimKey":
631
- return (matchFirstArgString(call, id.shortName) || matchObjectProperty(call, "name", id.shortName));
632
- case "referenceData":
633
- return (matchFirstArgString(call, id.entityName) ||
634
- matchObjectProperty(call, "entity", id.entityName));
635
- case "useExtension":
636
- // Positional: r.useExtension(name, entity) | Object: { name, entity }
637
- if (matchFirstArgString(call, id.extensionName)) {
638
- return matchArgNameOrRef(call, 1, id.entityName);
639
- }
640
- return (matchObjectProperty(call, "name", id.extensionName) &&
641
- matchObjectProperty(call, "entity", id.entityName));
642
- case "job":
643
- return matchFirstArgString(call, id.jobName) || matchObjectProperty(call, "name", id.jobName);
644
- case "notification":
645
- return (matchFirstArgString(call, id.notificationName) ||
646
- matchObjectProperty(call, "name", id.notificationName));
647
- case "httpRoute":
648
- // Object form only; positional doesn't apply.
649
- return (matchObjectProperty(call, "method", id.method) && matchObjectProperty(call, "path", id.path));
650
- case "projection":
651
- case "multiStreamProjection":
652
- return matchObjectProperty(call, "name", id.name);
653
- case "defineEvent":
654
- return (matchFirstArgString(call, id.eventName) || matchObjectProperty(call, "name", id.eventName));
655
- case "extendsRegistrar":
656
- return matchFirstArgString(call, id.extensionName);
657
- case "ai.generate":
658
- case "ai.extract":
659
- case "ai.classify":
660
- return readAiStepKey(call) === id.stepKey;
661
- default: {
662
- const _exhaustive = id;
663
- return _exhaustive;
664
- }
665
- }
666
- }
667
- // Resolves an argument the same way the extractors do (readNameLiteral):
668
- // a string literal directly, or a bare identifier following its
669
- // declaration (same-file or imported) to a string-literal initializer —
670
- // the dominant naming style in the framework's own bundled-features
671
- // (`r.entity(ENTITY, ...)`, `r.useExtension(EXT_X, ...)`).
672
- //
673
- // Narrow on purpose: every kind's arg-0 (and relation's arg-1) is parsed
674
- // via readNameLiteral, never readNameOrRef (see round2.ts/round3.ts) —
675
- // widening this shared helper to readNameOrRef would let an object-form
676
- // call's first argument (an ObjectLiteralExpression) match here too,
677
- // short-circuiting the object-form branch in callMatchesId. Positions
678
- // where the parser itself accepts an inline `{ name: "..." }` ref use
679
- // `matchArgNameOrRef` below instead.
680
- function matchArgString(call, index, expected) {
681
- const arg = call.getArguments()[index];
682
- if (!arg)
683
- return false;
684
- return readNameLiteral(arg) === expected;
685
- }
686
- // @wrapper-known semantic-alias
687
- function matchFirstArgString(call, expected) {
688
- return matchArgString(call, 0, expected);
689
- }
690
- // Like matchArgString, but via readNameOrRef — for the specific positional
691
- // slots where the parser accepts an inline `{ name: "..." }` object ref in
692
- // addition to a literal/identifier (useExtension's entity arg, hook's
693
- // target arg; see round3.ts:445 / hooks.ts:75). Not a drop-in replacement
694
- // for matchArgString: applying it to an arg-0 position would match an
695
- // object-form call's first (and only) argument, see the comment above.
696
- function matchArgNameOrRef(call, index, expected) {
697
- const arg = call.getArguments()[index];
698
- if (!arg)
699
- return false;
700
- return readNameOrRef(arg) === expected;
701
- }
702
- // Object-form property values are resolved via readNameOrRef, not the
703
- // narrower readNameLiteral — some properties (useExtension's `entity`,
704
- // hook's `target`/`allOf`) accept an inline `{ name: "..." }` ref in the
705
- // parser (see extractors/round3.ts, extractors/hooks.ts), not just a
706
- // literal or identifier. readNameOrRef is a superset of readNameLiteral,
707
- // so this is safe for every other property too.
708
- function matchObjectProperty(call, propName, expected) {
709
- const obj = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
710
- if (!obj)
711
- return false;
712
- const init = obj.getProperty(propName)?.asKind(SyntaxKind.PropertyAssignment)?.getInitializer();
713
- if (!init)
714
- return false;
715
- return readNameOrRef(init) === expected;
716
- }
717
- // Matches an r.hook `{ allOf: entity }` target — as the arg node directly
718
- // (positional call form) or as a nested object property (object call form).
719
- function matchAllOfArg(node, expectedEntity) {
720
- const obj = node?.asKind(SyntaxKind.ObjectLiteralExpression);
721
- const init = obj?.getProperty("allOf")?.asKind(SyntaxKind.PropertyAssignment)?.getInitializer();
722
- if (!init)
723
- return false;
724
- return readNameOrRef(init) === expectedEntity;
725
- }
726
- function matchObjectAllOfProperty(call, propName, expectedEntity) {
727
- const obj = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
728
- const propInit = obj
729
- ?.getProperty(propName)
730
- ?.asKind(SyntaxKind.PropertyAssignment)
731
- ?.getInitializer();
732
- return matchAllOfArg(propInit, expectedEntity);
733
- }
734
556
  // =============================================================================
735
557
  // Format helpers — line boundaries / blank-line collapse
736
558
  // (indent / PATTERN_INDENT live in render.ts and are imported above.)