@cosmicdrift/kumiko-framework 0.1.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/README.md +159 -0
- package/package.json +91 -0
- package/src/__tests__/anonymous-access.integration.ts +325 -0
- package/src/__tests__/error-contract.integration.ts +435 -0
- package/src/__tests__/field-access.integration.ts +269 -0
- package/src/__tests__/full-stack.integration.ts +914 -0
- package/src/__tests__/ownership.integration.ts +449 -0
- package/src/__tests__/reference-data.integration.ts +198 -0
- package/src/__tests__/transition-guard.integration.ts +340 -0
- package/src/api/__tests__/api.test.ts +337 -0
- package/src/api/__tests__/auth-middleware-transport.test.ts +80 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +179 -0
- package/src/api/__tests__/batch.integration.ts +404 -0
- package/src/api/__tests__/body-limit.test.ts +88 -0
- package/src/api/__tests__/csrf-middleware.test.ts +97 -0
- package/src/api/__tests__/dispatcher-live.integration.ts +216 -0
- package/src/api/__tests__/metrics-endpoint.test.ts +126 -0
- package/src/api/__tests__/nested-write.integration.ts +213 -0
- package/src/api/__tests__/readiness.test.ts +76 -0
- package/src/api/__tests__/request-id-middleware.test.ts +72 -0
- package/src/api/__tests__/sse-broker.test.ts +58 -0
- package/src/api/__tests__/sse-route.test.ts +112 -0
- package/src/api/anonymous-cookie.ts +60 -0
- package/src/api/api-constants.ts +64 -0
- package/src/api/auth-middleware.ts +418 -0
- package/src/api/auth-routes.ts +982 -0
- package/src/api/csrf-middleware.ts +77 -0
- package/src/api/index.ts +31 -0
- package/src/api/jwt.ts +66 -0
- package/src/api/observability-middleware.ts +89 -0
- package/src/api/readiness.ts +132 -0
- package/src/api/request-context.ts +49 -0
- package/src/api/request-id-middleware.ts +50 -0
- package/src/api/route-registrars.ts +195 -0
- package/src/api/routes.ts +135 -0
- package/src/api/server.ts +640 -0
- package/src/api/sse-broker.ts +71 -0
- package/src/api/sse-route.ts +62 -0
- package/src/api/tokens.ts +16 -0
- package/src/db/__tests__/apply-entity-event-tenant.integration.ts +159 -0
- package/src/db/__tests__/compound-types.test.ts +114 -0
- package/src/db/__tests__/connection-options.test.ts +68 -0
- package/src/db/__tests__/cursor.test.ts +41 -0
- package/src/db/__tests__/db-helpers.test.ts +369 -0
- package/src/db/__tests__/dialect-instant.test.ts +50 -0
- package/src/db/__tests__/drizzle-helpers.integration.ts +186 -0
- package/src/db/__tests__/drizzle-table-types.test.ts +162 -0
- package/src/db/__tests__/encryption.test.ts +39 -0
- package/src/db/__tests__/event-store-executor-list.integration.ts +313 -0
- package/src/db/__tests__/event-store-executor.integration.ts +235 -0
- package/src/db/__tests__/implicit-projection-equivalence.integration.ts +304 -0
- package/src/db/__tests__/located-timestamp.test.ts +184 -0
- package/src/db/__tests__/money.test.ts +199 -0
- package/src/db/__tests__/multi-row-insert.integration.ts +76 -0
- package/src/db/__tests__/parse-auto-verb.test.ts +70 -0
- package/src/db/__tests__/required-not-null-migration-safety.integration.ts +105 -0
- package/src/db/__tests__/row-helpers.test.ts +59 -0
- package/src/db/__tests__/schema-migration.integration.ts +273 -0
- package/src/db/__tests__/table-builder-indexes.test.ts +153 -0
- package/src/db/__tests__/table-builder-required.test.ts +216 -0
- package/src/db/__tests__/tenant-db.integration.ts +606 -0
- package/src/db/__tests__/unique-violation-mapping.integration.ts +166 -0
- package/src/db/apply-entity-event.ts +188 -0
- package/src/db/assert-exists-in.ts +59 -0
- package/src/db/compound-types.ts +47 -0
- package/src/db/connection.ts +104 -0
- package/src/db/cursor.ts +83 -0
- package/src/db/dialect.ts +109 -0
- package/src/db/eagerload.ts +174 -0
- package/src/db/encryption.ts +39 -0
- package/src/db/event-store-executor.ts +906 -0
- package/src/db/index.ts +55 -0
- package/src/db/located-timestamp.ts +114 -0
- package/src/db/money.ts +120 -0
- package/src/db/pg-error.ts +46 -0
- package/src/db/reference-data.ts +77 -0
- package/src/db/row-helpers.ts +53 -0
- package/src/db/schema-inspection.ts +25 -0
- package/src/db/table-builder.ts +475 -0
- package/src/db/tenant-db.ts +434 -0
- package/src/engine/__tests__/auth-claims-registrar.test.ts +74 -0
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +108 -0
- package/src/engine/__tests__/boot-validator.test.ts +1865 -0
- package/src/engine/__tests__/build-app-schema.test.ts +154 -0
- package/src/engine/__tests__/claim-keys.test.ts +274 -0
- package/src/engine/__tests__/config-helpers.test.ts +236 -0
- package/src/engine/__tests__/effective-features.test.ts +86 -0
- package/src/engine/__tests__/engine.test.ts +1461 -0
- package/src/engine/__tests__/entity-handlers.test.ts +274 -0
- package/src/engine/__tests__/event-helpers.test.ts +68 -0
- package/src/engine/__tests__/extends-registrar.test.ts +159 -0
- package/src/engine/__tests__/factories-long-text.test.ts +84 -0
- package/src/engine/__tests__/factories-time.test.ts +158 -0
- package/src/engine/__tests__/field-predicates.test.ts +48 -0
- package/src/engine/__tests__/hook-phases.test.ts +132 -0
- package/src/engine/__tests__/identifiers.test.ts +35 -0
- package/src/engine/__tests__/lifecycle-hooks.test.ts +237 -0
- package/src/engine/__tests__/nav.test.ts +267 -0
- package/src/engine/__tests__/ownership.test.ts +421 -0
- package/src/engine/__tests__/parse-ref-target.test.ts +43 -0
- package/src/engine/__tests__/projection-helpers.test.ts +62 -0
- package/src/engine/__tests__/projection.test.ts +191 -0
- package/src/engine/__tests__/qualified-name.test.ts +264 -0
- package/src/engine/__tests__/resolve-config-or-param.test.ts +315 -0
- package/src/engine/__tests__/run-in.test.ts +38 -0
- package/src/engine/__tests__/schema-builder.test.ts +380 -0
- package/src/engine/__tests__/screen.test.ts +408 -0
- package/src/engine/__tests__/state-machine.test.ts +148 -0
- package/src/engine/__tests__/system-user.test.ts +57 -0
- package/src/engine/__tests__/validation-hooks.test.ts +71 -0
- package/src/engine/access.ts +23 -0
- package/src/engine/boot-validator.ts +1528 -0
- package/src/engine/build-app-schema.ts +125 -0
- package/src/engine/config-helpers.ts +115 -0
- package/src/engine/constants.ts +85 -0
- package/src/engine/create-app.ts +98 -0
- package/src/engine/define-feature.ts +702 -0
- package/src/engine/define-handler.ts +78 -0
- package/src/engine/define-roles.ts +19 -0
- package/src/engine/effective-features.ts +87 -0
- package/src/engine/entity-handlers.ts +364 -0
- package/src/engine/event-helpers.ts +73 -0
- package/src/engine/factories.ts +328 -0
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +416 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +197 -0
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +128 -0
- package/src/engine/feature-ast/__tests__/parse.test.ts +888 -0
- package/src/engine/feature-ast/__tests__/patch.test.ts +360 -0
- package/src/engine/feature-ast/__tests__/patcher.test.ts +469 -0
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +287 -0
- package/src/engine/feature-ast/extractors.ts +2562 -0
- package/src/engine/feature-ast/index.ts +105 -0
- package/src/engine/feature-ast/parse.ts +369 -0
- package/src/engine/feature-ast/patch.ts +525 -0
- package/src/engine/feature-ast/patcher.ts +518 -0
- package/src/engine/feature-ast/patterns.ts +434 -0
- package/src/engine/feature-ast/render.ts +602 -0
- package/src/engine/feature-ast/source-location.ts +45 -0
- package/src/engine/field-access.ts +120 -0
- package/src/engine/index.ts +254 -0
- package/src/engine/ownership.ts +337 -0
- package/src/engine/parse-ref-target.ts +22 -0
- package/src/engine/pattern-library/__tests__/library.test.ts +351 -0
- package/src/engine/pattern-library/index.ts +24 -0
- package/src/engine/pattern-library/library.ts +1117 -0
- package/src/engine/pattern-library/types.ts +255 -0
- package/src/engine/projection-helpers.ts +85 -0
- package/src/engine/qualified-name.ts +122 -0
- package/src/engine/read-claim.ts +31 -0
- package/src/engine/registry.ts +1325 -0
- package/src/engine/resolve-config-or-param.ts +153 -0
- package/src/engine/run-in.ts +29 -0
- package/src/engine/schema-builder.ts +175 -0
- package/src/engine/screen-filter-ops.ts +51 -0
- package/src/engine/state-machine.ts +70 -0
- package/src/engine/system-user.ts +32 -0
- package/src/engine/types/config.ts +306 -0
- package/src/engine/types/event-type-map.ts +37 -0
- package/src/engine/types/feature.ts +574 -0
- package/src/engine/types/fields.ts +422 -0
- package/src/engine/types/handlers.ts +742 -0
- package/src/engine/types/hooks.ts +142 -0
- package/src/engine/types/http-route.ts +54 -0
- package/src/engine/types/identifiers.ts +47 -0
- package/src/engine/types/index.ts +208 -0
- package/src/engine/types/nav.ts +46 -0
- package/src/engine/types/projection.ts +132 -0
- package/src/engine/types/relations.ts +51 -0
- package/src/engine/types/screen.ts +452 -0
- package/src/engine/types/workspace.ts +42 -0
- package/src/engine/validation.ts +33 -0
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.ts +173 -0
- package/src/entrypoint/__tests__/split-deploy.integration.ts +297 -0
- package/src/entrypoint/index.ts +442 -0
- package/src/errors/__tests__/classes.test.ts +371 -0
- package/src/errors/__tests__/write-failures.test.ts +109 -0
- package/src/errors/classes.ts +249 -0
- package/src/errors/i18n/de.yaml +83 -0
- package/src/errors/i18n/en.yaml +80 -0
- package/src/errors/index.ts +41 -0
- package/src/errors/kumiko-error.ts +67 -0
- package/src/errors/reasons.ts +36 -0
- package/src/errors/serialize.ts +136 -0
- package/src/errors/transition-details.ts +30 -0
- package/src/errors/write-error-info.ts +123 -0
- package/src/errors/zod-bridge.ts +49 -0
- package/src/event-store/__tests__/admin-api.integration.ts +361 -0
- package/src/event-store/__tests__/event-store.integration.ts +584 -0
- package/src/event-store/__tests__/get-stream-version-perf.integration.ts +83 -0
- package/src/event-store/__tests__/perf.integration.ts +255 -0
- package/src/event-store/__tests__/snapshot.integration.ts +267 -0
- package/src/event-store/__tests__/upcaster-dead-letter.integration.ts +204 -0
- package/src/event-store/__tests__/upcaster.integration.ts +460 -0
- package/src/event-store/admin-api.ts +257 -0
- package/src/event-store/archive.ts +106 -0
- package/src/event-store/errors.ts +35 -0
- package/src/event-store/event-store.ts +405 -0
- package/src/event-store/events-schema.ts +90 -0
- package/src/event-store/index.ts +50 -0
- package/src/event-store/snapshot.ts +210 -0
- package/src/event-store/upcaster-dead-letter.ts +119 -0
- package/src/event-store/upcaster.ts +147 -0
- package/src/files/__tests__/content-disposition.test.ts +123 -0
- package/src/files/__tests__/file-field-column.integration.ts +103 -0
- package/src/files/__tests__/file-field-pipeline.integration.ts +211 -0
- package/src/files/__tests__/file-handle.test.ts +122 -0
- package/src/files/__tests__/files.integration.ts +830 -0
- package/src/files/__tests__/storage-tracking.integration.ts +153 -0
- package/src/files/content-disposition.ts +55 -0
- package/src/files/file-handle.ts +63 -0
- package/src/files/file-ref-table.ts +22 -0
- package/src/files/file-routes.ts +353 -0
- package/src/files/in-memory-provider.ts +62 -0
- package/src/files/index.ts +29 -0
- package/src/files/local-provider.ts +35 -0
- package/src/files/storage-tracking.ts +60 -0
- package/src/files/types.ts +118 -0
- package/src/i18n/__tests__/i18n.test.ts +72 -0
- package/src/i18n/index.ts +29 -0
- package/src/jobs/__tests__/job-event-trigger.integration.ts +172 -0
- package/src/jobs/__tests__/job-multi-trigger.integration.ts +144 -0
- package/src/jobs/__tests__/jobs.integration.ts +566 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/job-runner.ts +574 -0
- package/src/lifecycle/__tests__/create-test-lifecycle.ts +19 -0
- package/src/lifecycle/__tests__/lifecycle-server.integration.ts +108 -0
- package/src/lifecycle/__tests__/lifecycle.test.ts +212 -0
- package/src/lifecycle/__tests__/signal-handlers.test.ts +106 -0
- package/src/lifecycle/index.ts +13 -0
- package/src/lifecycle/lifecycle.ts +160 -0
- package/src/lifecycle/signal-handlers.ts +62 -0
- package/src/logging/__tests__/pino-trace-bridge.test.ts +50 -0
- package/src/logging/index.ts +3 -0
- package/src/logging/pino-logger.ts +64 -0
- package/src/logging/types.ts +7 -0
- package/src/migrations/__tests__/compare-snapshots.test.ts +150 -0
- package/src/migrations/__tests__/detect-drift.integration.ts +320 -0
- package/src/migrations/__tests__/detect-projections-to-rebuild.integration.ts +134 -0
- package/src/migrations/__tests__/rebuild-marker.test.ts +79 -0
- package/src/migrations/index.ts +28 -0
- package/src/migrations/projection-detection.ts +149 -0
- package/src/migrations/rebuild-marker.ts +64 -0
- package/src/migrations/schema-drift.ts +395 -0
- package/src/observability/__tests__/console-provider.test.ts +67 -0
- package/src/observability/__tests__/metric-validator.test.ts +87 -0
- package/src/observability/__tests__/noop-provider.test.ts +82 -0
- package/src/observability/__tests__/observability.integration.ts +559 -0
- package/src/observability/__tests__/prometheus-meter.test.ts +144 -0
- package/src/observability/__tests__/recording-meter.test.ts +101 -0
- package/src/observability/__tests__/recording-tracer.test.ts +110 -0
- package/src/observability/__tests__/sensitive-filter.test.ts +98 -0
- package/src/observability/console-provider.ts +130 -0
- package/src/observability/context.ts +26 -0
- package/src/observability/fallback.ts +34 -0
- package/src/observability/ids.ts +25 -0
- package/src/observability/index.ts +79 -0
- package/src/observability/metric-validator.ts +86 -0
- package/src/observability/metrics-handle.ts +56 -0
- package/src/observability/noop-provider.ts +146 -0
- package/src/observability/prometheus-meter.ts +284 -0
- package/src/observability/recording-meter.ts +156 -0
- package/src/observability/recording-tracer.ts +198 -0
- package/src/observability/redis-wrapper.ts +132 -0
- package/src/observability/sensitive-filter.ts +108 -0
- package/src/observability/standard-metrics.ts +213 -0
- package/src/observability/types/index.ts +29 -0
- package/src/observability/types/metric.ts +56 -0
- package/src/observability/types/provider.ts +32 -0
- package/src/observability/types/span.ts +64 -0
- package/src/pipeline/__tests__/archive-stream.integration.ts +220 -0
- package/src/pipeline/__tests__/auth-claims-resolver.test.ts +279 -0
- package/src/pipeline/__tests__/cascade-handler.integration.ts +419 -0
- package/src/pipeline/__tests__/cascade-handler.test.ts +52 -0
- package/src/pipeline/__tests__/causation-chain.integration.ts +206 -0
- package/src/pipeline/__tests__/ctx-bridge.integration.ts +234 -0
- package/src/pipeline/__tests__/dispatcher.test.ts +379 -0
- package/src/pipeline/__tests__/distributed-lock.integration.ts +67 -0
- package/src/pipeline/__tests__/domain-events-projections.integration.ts +323 -0
- package/src/pipeline/__tests__/event-dedup.integration.ts +153 -0
- package/src/pipeline/__tests__/event-define-event-strict.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +220 -0
- package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +423 -0
- package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +123 -0
- package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +290 -0
- package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +65 -0
- package/src/pipeline/__tests__/event-dispatcher.integration.ts +287 -0
- package/src/pipeline/__tests__/event-retention.integration.ts +239 -0
- package/src/pipeline/__tests__/fetch-for-writing.integration.ts +281 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +430 -0
- package/src/pipeline/__tests__/load-aggregate-query.integration.ts +266 -0
- package/src/pipeline/__tests__/msp-error-mode.integration.ts +149 -0
- package/src/pipeline/__tests__/msp-multi-hop.integration.ts +228 -0
- package/src/pipeline/__tests__/msp-rebuild.integration.ts +368 -0
- package/src/pipeline/__tests__/multi-stream-projection.integration.ts +341 -0
- package/src/pipeline/__tests__/perf-rebuild.integration.ts +147 -0
- package/src/pipeline/__tests__/projection-rebuild.integration.ts +551 -0
- package/src/pipeline/__tests__/query-projection.integration.ts +201 -0
- package/src/pipeline/__tests__/redis-pipeline.integration.ts +306 -0
- package/src/pipeline/append-event-core.ts +117 -0
- package/src/pipeline/auth-claims-resolver.ts +103 -0
- package/src/pipeline/cascade-handler.ts +113 -0
- package/src/pipeline/dispatcher.ts +1585 -0
- package/src/pipeline/distributed-lock.ts +37 -0
- package/src/pipeline/entity-cache.ts +113 -0
- package/src/pipeline/event-consumer-state.ts +108 -0
- package/src/pipeline/event-dedup.ts +23 -0
- package/src/pipeline/event-dispatcher.ts +1016 -0
- package/src/pipeline/event-retention.ts +154 -0
- package/src/pipeline/idempotency.ts +76 -0
- package/src/pipeline/index.ts +66 -0
- package/src/pipeline/lifecycle-pipeline.ts +409 -0
- package/src/pipeline/msp-rebuild.ts +242 -0
- package/src/pipeline/multi-stream-apply-context.ts +115 -0
- package/src/pipeline/projection-rebuild.ts +334 -0
- package/src/pipeline/projection-state.ts +72 -0
- package/src/pipeline/projections-runner.ts +56 -0
- package/src/pipeline/redis-keys.ts +11 -0
- package/src/pipeline/system-hooks.ts +190 -0
- package/src/random/__tests__/generate.test.ts +149 -0
- package/src/random/generate.ts +141 -0
- package/src/random/index.ts +8 -0
- package/src/random/words.ts +392 -0
- package/src/rate-limit/__tests__/dispatcher-l3.integration.ts +111 -0
- package/src/rate-limit/__tests__/middleware.integration.ts +189 -0
- package/src/rate-limit/__tests__/resolver.integration.ts +189 -0
- package/src/rate-limit/bucket.ts +36 -0
- package/src/rate-limit/index.ts +14 -0
- package/src/rate-limit/middleware.ts +152 -0
- package/src/rate-limit/resolver.ts +267 -0
- package/src/redis/__tests__/redis-options.test.ts +54 -0
- package/src/redis/index.ts +74 -0
- package/src/search/__tests__/meilisearch-adapter.integration.ts +236 -0
- package/src/search/__tests__/search-adapter.test.ts +256 -0
- package/src/search/in-memory-adapter.ts +123 -0
- package/src/search/index.ts +12 -0
- package/src/search/meilisearch-adapter.ts +106 -0
- package/src/search/types.ts +39 -0
- package/src/secrets/__tests__/dek-cache.test.ts +213 -0
- package/src/secrets/__tests__/env-master-key-provider.test.ts +119 -0
- package/src/secrets/__tests__/envelope.test.ts +74 -0
- package/src/secrets/__tests__/leak-guard.test.ts +92 -0
- package/src/secrets/__tests__/rotation.test.ts +149 -0
- package/src/secrets/dek-cache.ts +116 -0
- package/src/secrets/env-master-key-provider.ts +162 -0
- package/src/secrets/envelope.ts +55 -0
- package/src/secrets/index.ts +19 -0
- package/src/secrets/leak-guard.ts +87 -0
- package/src/secrets/rotation.ts +34 -0
- package/src/secrets/types.ts +107 -0
- package/src/stack/db.ts +104 -0
- package/src/stack/event-collector.ts +23 -0
- package/src/stack/index.ts +32 -0
- package/src/stack/redis.ts +44 -0
- package/src/stack/request-helper.ts +168 -0
- package/src/stack/table-helpers.ts +104 -0
- package/src/stack/test-stack.ts +357 -0
- package/src/stack/test-users.ts +37 -0
- package/src/testing/__tests__/e2e-generator.test.ts +230 -0
- package/src/testing/__tests__/ensure-entity-table.integration.ts +54 -0
- package/src/testing/access-assertions.ts +15 -0
- package/src/testing/assertions.ts +35 -0
- package/src/testing/e2e-generator.ts +465 -0
- package/src/testing/expect-error.ts +25 -0
- package/src/testing/handler-context.ts +125 -0
- package/src/testing/http-cookies.ts +52 -0
- package/src/testing/index.ts +41 -0
- package/src/testing/late-bound.ts +39 -0
- package/src/testing/mutable-master-key-provider.ts +31 -0
- package/src/testing/observability-recorder.ts +54 -0
- package/src/testing/shared-entities.ts +49 -0
- package/src/testing/utils.ts +1 -0
- package/src/testing/wait-for.ts +31 -0
- package/src/time/__tests__/polyfill.test.ts +73 -0
- package/src/time/__tests__/tz-context.test.ts +121 -0
- package/src/time/index.ts +21 -0
- package/src/time/polyfill.ts +70 -0
- package/src/time/tz-context.ts +107 -0
- package/src/ui-types/app-schema.ts +57 -0
- package/src/ui-types/index.ts +65 -0
- package/src/utils/__tests__/assert.test.ts +17 -0
- package/src/utils/__tests__/env-parse.test.ts +54 -0
- package/src/utils/assert.ts +18 -0
- package/src/utils/env-parse.ts +16 -0
- package/src/utils/ids.ts +16 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/safe-json.ts +30 -0
- package/src/utils/serialization.ts +7 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
// Patch operations: apply add/replace/remove changes to a feature-file's
|
|
2
|
+
// SourceFile in-place, working at the r.*-call granularity. Custom code
|
|
3
|
+
// (helpers, comments, imports, anything between calls) survives every
|
|
4
|
+
// patch unchanged — the patcher only touches the spans it owns.
|
|
5
|
+
//
|
|
6
|
+
// **Identity model — Natural-Key:** patterns are addressed by the
|
|
7
|
+
// human-readable name they carry: entity-name, handler-name, nav-id,
|
|
8
|
+
// hook-target+type, etc. Reorders and re-renderings don't break IDs;
|
|
9
|
+
// renames are explicit (remove old → add new). For the few singleton
|
|
10
|
+
// patterns (toggleable, requires, systemScope) the kind itself is the
|
|
11
|
+
// key — a feature has at most one of each.
|
|
12
|
+
//
|
|
13
|
+
// **Position semantics:**
|
|
14
|
+
// - addPattern → appended at the end of the setup callback
|
|
15
|
+
// - replacePattern → in place, same indentation as the original call
|
|
16
|
+
// - removePattern → call + leading blank-line whitespace gone
|
|
17
|
+
//
|
|
18
|
+
// **Renderer-driven output.** Every pattern lands in canonical Object-
|
|
19
|
+
// Form (single-arg literal, see render.ts). Existing patterns in legacy
|
|
20
|
+
// positional form get converted on replace; new patterns start
|
|
21
|
+
// canonical. Schema-Version-Header is the renderer's responsibility.
|
|
22
|
+
//
|
|
23
|
+
// **Comment-Preservation — known limitation.** Inline comments INSIDE a
|
|
24
|
+
// pattern (e.g. `// reason: legacy field` on an entity field property)
|
|
25
|
+
// are LOST on replace, because the renderer regenerates the call from
|
|
26
|
+
// the parsed FeaturePattern, which doesn't carry comment-trivia.
|
|
27
|
+
// Comments BETWEEN patterns (helper-functions, top-of-feature notes,
|
|
28
|
+
// imports) survive every patch — only comments authored on lines the
|
|
29
|
+
// patcher rewrites are dropped. Tracked as a future-work item: see
|
|
30
|
+
// roadmap C-Notes for the canonical-comment-attach Pattern that would
|
|
31
|
+
// preserve prefixed `// kumiko-comment:` markers across roundtrips.
|
|
32
|
+
|
|
33
|
+
import { type CallExpression, type Node, type SourceFile, SyntaxKind } from "ts-morph";
|
|
34
|
+
import type { FeaturePattern, FeaturePatternKind } from "./patterns";
|
|
35
|
+
import { indent, PATTERN_INDENT, renderPattern } from "./render";
|
|
36
|
+
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// PatternId — natural-key per pattern kind
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Identifier used by replace/remove. Discriminated union: each pattern
|
|
43
|
+
* kind names the property the patcher must match against. Adding a new
|
|
44
|
+
* pattern kind requires a new entry here so the type system forces the
|
|
45
|
+
* call-site to think about identity (or fall through to "first call of
|
|
46
|
+
* this kind" via the singleton helpers below).
|
|
47
|
+
*/
|
|
48
|
+
export type PatternId =
|
|
49
|
+
| { readonly kind: "entity"; readonly entityName: string }
|
|
50
|
+
| { readonly kind: "relation"; readonly entityName: string; readonly relationName: string }
|
|
51
|
+
| { readonly kind: "nav"; readonly id: string }
|
|
52
|
+
| { readonly kind: "workspace"; readonly id: string }
|
|
53
|
+
| { readonly kind: "screen"; readonly id: string }
|
|
54
|
+
| { readonly kind: "writeHandler"; readonly handlerName: string }
|
|
55
|
+
| { readonly kind: "queryHandler"; readonly handlerName: string }
|
|
56
|
+
| { readonly kind: "hook"; readonly hookType: string; readonly target: string }
|
|
57
|
+
| { readonly kind: "entityHook"; readonly hookType: string; readonly entityName: string }
|
|
58
|
+
| { readonly kind: "metric"; readonly shortName: string }
|
|
59
|
+
| { readonly kind: "secret"; readonly shortName: string }
|
|
60
|
+
| { readonly kind: "claimKey"; readonly shortName: string }
|
|
61
|
+
| { readonly kind: "referenceData"; readonly entityName: string }
|
|
62
|
+
| { readonly kind: "useExtension"; readonly extensionName: string; readonly entityName: string }
|
|
63
|
+
| { readonly kind: "job"; readonly jobName: string }
|
|
64
|
+
| { readonly kind: "notification"; readonly notificationName: string }
|
|
65
|
+
| { readonly kind: "httpRoute"; readonly method: string; readonly path: string }
|
|
66
|
+
| { readonly kind: "projection"; readonly name: string }
|
|
67
|
+
| { readonly kind: "multiStreamProjection"; readonly name: string }
|
|
68
|
+
| { readonly kind: "defineEvent"; readonly eventName: string }
|
|
69
|
+
| {
|
|
70
|
+
readonly kind: "eventMigration";
|
|
71
|
+
readonly eventName: string;
|
|
72
|
+
readonly fromVersion: number;
|
|
73
|
+
readonly toVersion: number;
|
|
74
|
+
}
|
|
75
|
+
| { readonly kind: "extendsRegistrar"; readonly extensionName: string }
|
|
76
|
+
// Singleton patterns — only one per feature, kind alone identifies them.
|
|
77
|
+
| { readonly kind: "requires" }
|
|
78
|
+
| { readonly kind: "optionalRequires" }
|
|
79
|
+
| { readonly kind: "readsConfig" }
|
|
80
|
+
| { readonly kind: "systemScope" }
|
|
81
|
+
| { readonly kind: "toggleable" }
|
|
82
|
+
| { readonly kind: "config" }
|
|
83
|
+
| { readonly kind: "translations" }
|
|
84
|
+
| { readonly kind: "authClaims" };
|
|
85
|
+
|
|
86
|
+
// =============================================================================
|
|
87
|
+
// Change ops — generic apply API
|
|
88
|
+
// =============================================================================
|
|
89
|
+
|
|
90
|
+
export type PatternChange =
|
|
91
|
+
| { readonly op: "add"; readonly pattern: FeaturePattern }
|
|
92
|
+
| { readonly op: "replace"; readonly id: PatternId; readonly pattern: FeaturePattern }
|
|
93
|
+
| { readonly op: "remove"; readonly id: PatternId };
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Apply a sequence of changes to the source file in-place. The list is
|
|
97
|
+
* processed in order; replace/remove failures (id not found) throw so
|
|
98
|
+
* callers can react explicitly — silent no-ops would mask design bugs
|
|
99
|
+
* in the Designer/AI generator. Adds always succeed.
|
|
100
|
+
*
|
|
101
|
+
* The function does NOT save the file — `sourceFile.saveSync()` (or the
|
|
102
|
+
* caller's persistence layer) is expected to follow.
|
|
103
|
+
*/
|
|
104
|
+
export function applyChanges(sourceFile: SourceFile, changes: readonly PatternChange[]): void {
|
|
105
|
+
for (const change of changes) {
|
|
106
|
+
switch (change.op) {
|
|
107
|
+
case "add":
|
|
108
|
+
addPattern(sourceFile, change.pattern);
|
|
109
|
+
break;
|
|
110
|
+
case "replace":
|
|
111
|
+
replacePattern(sourceFile, change.id, change.pattern);
|
|
112
|
+
break;
|
|
113
|
+
case "remove":
|
|
114
|
+
removePattern(sourceFile, change.id);
|
|
115
|
+
break;
|
|
116
|
+
default: {
|
|
117
|
+
const _exhaustive: never = change;
|
|
118
|
+
throw new Error(`applyChanges: unknown op ${String(_exhaustive)}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// =============================================================================
|
|
125
|
+
// Add
|
|
126
|
+
// =============================================================================
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Low-level escape hatch: append a hand-built FeaturePattern at the end
|
|
130
|
+
* of the setup callback's body. **Prefer the typed `add{Kind}` methods
|
|
131
|
+
* on `createFeaturePatcher(sf)`** — they take natural args, build the
|
|
132
|
+
* FeaturePattern internally, and avoid SourceLocation boilerplate.
|
|
133
|
+
*
|
|
134
|
+
* Use this directly when:
|
|
135
|
+
* - Migrating a parsed pattern from another file (already-built
|
|
136
|
+
* FeaturePattern object on hand)
|
|
137
|
+
* - The pattern kind isn't yet covered by a typed `add{Kind}`
|
|
138
|
+
*
|
|
139
|
+
* The pattern is rendered (canonical Object-Form) and inserted as the
|
|
140
|
+
* last statement, separated from the previous one by a blank line —
|
|
141
|
+
* biome-stable formatting that matches the renderFeatureFile output.
|
|
142
|
+
*/
|
|
143
|
+
export function addPattern(sourceFile: SourceFile, pattern: FeaturePattern): void {
|
|
144
|
+
const setup = findSetupCallback(sourceFile);
|
|
145
|
+
if (!setup) {
|
|
146
|
+
throw new Error("addPattern: no defineFeature(name, (r) => { ... }) call found");
|
|
147
|
+
}
|
|
148
|
+
const body = setup.body;
|
|
149
|
+
const rendered = indent(renderPattern(pattern), PATTERN_INDENT);
|
|
150
|
+
|
|
151
|
+
// Find the closing brace of the body to insert just before it. The body
|
|
152
|
+
// is a Block; its last child is the close-brace, so the safe insertion
|
|
153
|
+
// point is the position of the close-brace (insertText pushes it down).
|
|
154
|
+
const closeBracePos = body.getEnd() - 1; // `}`
|
|
155
|
+
const lastStatement = lastNonTriviaChild(body);
|
|
156
|
+
// If the body has at least one statement, prefix with a blank line so
|
|
157
|
+
// every pattern is visually separated. For an empty setup callback,
|
|
158
|
+
// skip the leading newline so the first statement isn't preceded by a
|
|
159
|
+
// gratuitous blank line.
|
|
160
|
+
const needsLeadingBlank = lastStatement !== undefined;
|
|
161
|
+
const text = needsLeadingBlank ? `\n${rendered}\n` : `${rendered}\n`;
|
|
162
|
+
sourceFile.insertText(closeBracePos, text);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// =============================================================================
|
|
166
|
+
// Replace
|
|
167
|
+
// =============================================================================
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Find the call matching `id` and replace the entire CallExpression text
|
|
171
|
+
* with the rendered version of `pattern`. The replacement is reindented
|
|
172
|
+
* to match the original call's column so existing helpers/comments
|
|
173
|
+
* around it stay aligned. Throws when no call matches — callers must
|
|
174
|
+
* handle that case explicitly.
|
|
175
|
+
*/
|
|
176
|
+
export function replacePattern(
|
|
177
|
+
sourceFile: SourceFile,
|
|
178
|
+
id: PatternId,
|
|
179
|
+
pattern: FeaturePattern,
|
|
180
|
+
): void {
|
|
181
|
+
const call = findCallForId(sourceFile, id);
|
|
182
|
+
if (!call) {
|
|
183
|
+
throw new Error(`replacePattern: no call found for ${describeId(id)}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Whole call-statement spans from the CallExpression's start through
|
|
187
|
+
// its enclosing ExpressionStatement (which carries the trailing `;`).
|
|
188
|
+
const enclosingStatement = call.getFirstAncestorByKind(SyntaxKind.ExpressionStatement);
|
|
189
|
+
const startNode = enclosingStatement ?? call;
|
|
190
|
+
|
|
191
|
+
const startPos = startNode.getStart();
|
|
192
|
+
const endPos = startNode.getEnd();
|
|
193
|
+
|
|
194
|
+
// Detect column of the original call's first non-whitespace character;
|
|
195
|
+
// the rendered pattern starts at column 0 and gets indented to match.
|
|
196
|
+
const startLineCol = sourceFile.getLineAndColumnAtPos(startPos);
|
|
197
|
+
const originalIndent = " ".repeat(Math.max(0, startLineCol.column - 1));
|
|
198
|
+
const rendered = indent(renderPattern(pattern), originalIndent).trimStart();
|
|
199
|
+
|
|
200
|
+
sourceFile.replaceText([startPos, endPos], rendered);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// =============================================================================
|
|
204
|
+
// Remove
|
|
205
|
+
// =============================================================================
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Find the call matching `id` and remove it together with its trailing
|
|
209
|
+
* newline. Comments belonging to the pattern are unaffected only when
|
|
210
|
+
* they live BEFORE the call as line-leading trivia — those leading
|
|
211
|
+
* comments are kept (they may belong to surrounding code, the patcher
|
|
212
|
+
* can't disambiguate without semantic markers). Inline comments on the
|
|
213
|
+
* same line as the call are removed with the call.
|
|
214
|
+
*/
|
|
215
|
+
export function removePattern(sourceFile: SourceFile, id: PatternId): void {
|
|
216
|
+
const call = findCallForId(sourceFile, id);
|
|
217
|
+
if (!call) {
|
|
218
|
+
throw new Error(`removePattern: no call found for ${describeId(id)}`);
|
|
219
|
+
}
|
|
220
|
+
const enclosingStatement = call.getFirstAncestorByKind(SyntaxKind.ExpressionStatement);
|
|
221
|
+
const target = enclosingStatement ?? call;
|
|
222
|
+
|
|
223
|
+
// Erase from the start of the line containing the statement (so leading
|
|
224
|
+
// indentation goes with it) through the trailing newline, including the
|
|
225
|
+
// *leading* blank line that addPattern emits — keeps blank-line counts
|
|
226
|
+
// stable under add → remove cycles. We don't touch leading comments.
|
|
227
|
+
const startPos = lineStart(sourceFile, target.getStart());
|
|
228
|
+
const endPos = lineEnd(sourceFile, target.getEnd());
|
|
229
|
+
|
|
230
|
+
// Collapse a preceding blank line if there is one (avoids a double
|
|
231
|
+
// blank line between the now-adjacent statements).
|
|
232
|
+
const collapseStart = collapsePrecedingBlankLine(sourceFile, startPos);
|
|
233
|
+
sourceFile.replaceText([collapseStart, endPos + 1], "");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// =============================================================================
|
|
237
|
+
// Lookup
|
|
238
|
+
// =============================================================================
|
|
239
|
+
|
|
240
|
+
function findSetupCallback(
|
|
241
|
+
sourceFile: SourceFile,
|
|
242
|
+
): { call: CallExpression; body: Node } | undefined {
|
|
243
|
+
for (const stmt of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
244
|
+
if (stmt.getExpression().getText() !== "defineFeature") continue;
|
|
245
|
+
const setupArg = stmt.getArguments()[1];
|
|
246
|
+
if (!setupArg) continue;
|
|
247
|
+
const arrow = setupArg.asKind(SyntaxKind.ArrowFunction);
|
|
248
|
+
if (!arrow) continue;
|
|
249
|
+
return { call: stmt, body: arrow.getBody() };
|
|
250
|
+
}
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Singleton kinds: a feature has at most one of each. The Boot-Validator
|
|
256
|
+
* rejects features that declare two `r.requires(...)` etc. — the patcher
|
|
257
|
+
* asserts the same invariant so a corrupt source file produces an
|
|
258
|
+
* explicit error here, not a silent first-match win.
|
|
259
|
+
*
|
|
260
|
+
* Exported so the pattern-library tests + downstream consumers
|
|
261
|
+
* (Designer, AI-Builder) share one source-of-truth — duplicating this
|
|
262
|
+
* set would let the library's `singleton: true` flags drift silently
|
|
263
|
+
* from the patcher's enforcement.
|
|
264
|
+
*/
|
|
265
|
+
export const SINGLETON_KINDS: ReadonlySet<PatternId["kind"]> = new Set([
|
|
266
|
+
"requires",
|
|
267
|
+
"optionalRequires",
|
|
268
|
+
"readsConfig",
|
|
269
|
+
"systemScope",
|
|
270
|
+
"toggleable",
|
|
271
|
+
"config",
|
|
272
|
+
"translations",
|
|
273
|
+
"authClaims",
|
|
274
|
+
]);
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Return the CallExpression in the setup callback whose call shape
|
|
278
|
+
* matches the given id. Reads the call arguments structurally — same
|
|
279
|
+
* paths the parser walks, no re-parsing through extractors.ts (would
|
|
280
|
+
* be redundant work).
|
|
281
|
+
*
|
|
282
|
+
* For singleton kinds (requires, toggleable, etc.) the patcher
|
|
283
|
+
* additionally asserts that the file contains AT MOST one matching
|
|
284
|
+
* call. Two calls of the same singleton kind would let the first-match
|
|
285
|
+
* silently win; we'd rather throw so Designer/AI surfacing the corrupt
|
|
286
|
+
* feature can fix it explicitly.
|
|
287
|
+
*/
|
|
288
|
+
function findCallForId(sourceFile: SourceFile, id: PatternId): CallExpression | undefined {
|
|
289
|
+
const setup = findSetupCallback(sourceFile);
|
|
290
|
+
if (!setup) return undefined;
|
|
291
|
+
const registrarParam = setup.call
|
|
292
|
+
.getArguments()[1]
|
|
293
|
+
?.asKind(SyntaxKind.ArrowFunction)
|
|
294
|
+
?.getParameters()[0]
|
|
295
|
+
?.getName();
|
|
296
|
+
if (!registrarParam) return undefined;
|
|
297
|
+
|
|
298
|
+
const matches: CallExpression[] = [];
|
|
299
|
+
for (const call of setup.body.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
300
|
+
const propAccess = call.getExpression().asKind(SyntaxKind.PropertyAccessExpression);
|
|
301
|
+
if (!propAccess) continue;
|
|
302
|
+
if (propAccess.getExpression().getText() !== registrarParam) continue;
|
|
303
|
+
if (propAccess.getName() !== id.kind) continue;
|
|
304
|
+
if (callMatchesId(call, id)) matches.push(call);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (SINGLETON_KINDS.has(id.kind) && matches.length > 1) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
`findCallForId: ${id.kind} is a singleton but ${matches.length} calls were found — feature file is corrupt`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return matches[0];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function callMatchesId(call: CallExpression, id: PatternId): boolean {
|
|
316
|
+
switch (id.kind) {
|
|
317
|
+
// Singletons: kind alone identifies the call.
|
|
318
|
+
case "requires":
|
|
319
|
+
case "optionalRequires":
|
|
320
|
+
case "readsConfig":
|
|
321
|
+
case "systemScope":
|
|
322
|
+
case "toggleable":
|
|
323
|
+
case "config":
|
|
324
|
+
case "translations":
|
|
325
|
+
case "authClaims":
|
|
326
|
+
return true;
|
|
327
|
+
|
|
328
|
+
case "entity":
|
|
329
|
+
return (
|
|
330
|
+
matchFirstArgString(call, id.entityName) || matchObjectProperty(call, "name", id.entityName)
|
|
331
|
+
);
|
|
332
|
+
case "relation":
|
|
333
|
+
// Positional: r.relation(entity, name, def) | Object: { entity, name, ... }
|
|
334
|
+
if (matchFirstArgString(call, id.entityName)) {
|
|
335
|
+
const second = call.getArguments()[1]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
336
|
+
return second === id.relationName;
|
|
337
|
+
}
|
|
338
|
+
return (
|
|
339
|
+
matchObjectProperty(call, "entity", id.entityName) &&
|
|
340
|
+
matchObjectProperty(call, "name", id.relationName)
|
|
341
|
+
);
|
|
342
|
+
case "nav":
|
|
343
|
+
case "workspace":
|
|
344
|
+
case "screen":
|
|
345
|
+
return matchObjectProperty(call, "id", id.id);
|
|
346
|
+
case "writeHandler":
|
|
347
|
+
case "queryHandler":
|
|
348
|
+
return (
|
|
349
|
+
matchFirstArgString(call, id.handlerName) ||
|
|
350
|
+
matchObjectProperty(call, "name", id.handlerName)
|
|
351
|
+
);
|
|
352
|
+
case "hook":
|
|
353
|
+
// Positional: r.hook(type, target, fn) | Object: { type, target }
|
|
354
|
+
if (matchFirstArgString(call, id.hookType)) {
|
|
355
|
+
const target = call.getArguments()[1]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
356
|
+
return target === id.target;
|
|
357
|
+
}
|
|
358
|
+
return (
|
|
359
|
+
matchObjectProperty(call, "type", id.hookType) &&
|
|
360
|
+
matchObjectProperty(call, "target", id.target)
|
|
361
|
+
);
|
|
362
|
+
case "entityHook":
|
|
363
|
+
if (matchFirstArgString(call, id.hookType)) {
|
|
364
|
+
const ent = call.getArguments()[1]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
365
|
+
return ent === id.entityName;
|
|
366
|
+
}
|
|
367
|
+
return (
|
|
368
|
+
matchObjectProperty(call, "type", id.hookType) &&
|
|
369
|
+
matchObjectProperty(call, "entity", id.entityName)
|
|
370
|
+
);
|
|
371
|
+
case "metric":
|
|
372
|
+
case "secret":
|
|
373
|
+
case "claimKey":
|
|
374
|
+
return (
|
|
375
|
+
matchFirstArgString(call, id.shortName) || matchObjectProperty(call, "name", id.shortName)
|
|
376
|
+
);
|
|
377
|
+
case "referenceData":
|
|
378
|
+
return (
|
|
379
|
+
matchFirstArgString(call, id.entityName) ||
|
|
380
|
+
matchObjectProperty(call, "entity", id.entityName)
|
|
381
|
+
);
|
|
382
|
+
case "useExtension":
|
|
383
|
+
// Positional: r.useExtension(name, entity) | Object: { name, entity }
|
|
384
|
+
if (matchFirstArgString(call, id.extensionName)) {
|
|
385
|
+
const ent = call.getArguments()[1]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
386
|
+
return ent === id.entityName;
|
|
387
|
+
}
|
|
388
|
+
return (
|
|
389
|
+
matchObjectProperty(call, "name", id.extensionName) &&
|
|
390
|
+
matchObjectProperty(call, "entity", id.entityName)
|
|
391
|
+
);
|
|
392
|
+
case "job":
|
|
393
|
+
return matchFirstArgString(call, id.jobName) || matchObjectProperty(call, "name", id.jobName);
|
|
394
|
+
case "notification":
|
|
395
|
+
return (
|
|
396
|
+
matchFirstArgString(call, id.notificationName) ||
|
|
397
|
+
matchObjectProperty(call, "name", id.notificationName)
|
|
398
|
+
);
|
|
399
|
+
case "httpRoute":
|
|
400
|
+
// Object form only; positional doesn't apply.
|
|
401
|
+
return (
|
|
402
|
+
matchObjectProperty(call, "method", id.method) && matchObjectProperty(call, "path", id.path)
|
|
403
|
+
);
|
|
404
|
+
case "projection":
|
|
405
|
+
case "multiStreamProjection":
|
|
406
|
+
return matchObjectProperty(call, "name", id.name);
|
|
407
|
+
case "defineEvent":
|
|
408
|
+
return (
|
|
409
|
+
matchFirstArgString(call, id.eventName) || matchObjectProperty(call, "name", id.eventName)
|
|
410
|
+
);
|
|
411
|
+
case "eventMigration": {
|
|
412
|
+
// Positional: r.eventMigration(name, from, to, fn)
|
|
413
|
+
if (matchFirstArgString(call, id.eventName)) {
|
|
414
|
+
const from = numericArg(call, 1);
|
|
415
|
+
const to = numericArg(call, 2);
|
|
416
|
+
return from === id.fromVersion && to === id.toVersion;
|
|
417
|
+
}
|
|
418
|
+
// Object: { event, fromVersion, toVersion }
|
|
419
|
+
return (
|
|
420
|
+
matchObjectProperty(call, "event", id.eventName) &&
|
|
421
|
+
matchObjectNumericProperty(call, "fromVersion", id.fromVersion) &&
|
|
422
|
+
matchObjectNumericProperty(call, "toVersion", id.toVersion)
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
case "extendsRegistrar":
|
|
426
|
+
return matchFirstArgString(call, id.extensionName);
|
|
427
|
+
default: {
|
|
428
|
+
const _exhaustive: never = id;
|
|
429
|
+
return _exhaustive;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function matchFirstArgString(call: CallExpression, expected: string): boolean {
|
|
435
|
+
const first = call.getArguments()[0];
|
|
436
|
+
const lit = first?.asKind(SyntaxKind.StringLiteral);
|
|
437
|
+
return lit?.getLiteralValue() === expected;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function matchObjectProperty(call: CallExpression, propName: string, expected: string): boolean {
|
|
441
|
+
const obj = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
442
|
+
if (!obj) return false;
|
|
443
|
+
const init = obj
|
|
444
|
+
.getProperty(propName)
|
|
445
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
446
|
+
?.getInitializer()
|
|
447
|
+
?.asKind(SyntaxKind.StringLiteral);
|
|
448
|
+
return init?.getLiteralValue() === expected;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function matchObjectNumericProperty(
|
|
452
|
+
call: CallExpression,
|
|
453
|
+
propName: string,
|
|
454
|
+
expected: number,
|
|
455
|
+
): boolean {
|
|
456
|
+
const obj = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
457
|
+
if (!obj) return false;
|
|
458
|
+
const init = obj
|
|
459
|
+
.getProperty(propName)
|
|
460
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
461
|
+
?.getInitializer()
|
|
462
|
+
?.asKind(SyntaxKind.NumericLiteral);
|
|
463
|
+
return init !== undefined && Number(init.getText()) === expected;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function numericArg(call: CallExpression, idx: number): number | undefined {
|
|
467
|
+
const lit = call.getArguments()[idx]?.asKind(SyntaxKind.NumericLiteral);
|
|
468
|
+
if (!lit) return undefined;
|
|
469
|
+
return Number(lit.getText());
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// =============================================================================
|
|
473
|
+
// Format helpers — line boundaries / blank-line collapse
|
|
474
|
+
// (indent / PATTERN_INDENT live in render.ts and are imported above.)
|
|
475
|
+
// =============================================================================
|
|
476
|
+
|
|
477
|
+
function lastNonTriviaChild(body: Node): Node | undefined {
|
|
478
|
+
// Block nodes have child[0] = `{`, last = `}`. Find the last
|
|
479
|
+
// SyntaxList element that's an actual statement — that signals
|
|
480
|
+
// whether the body is empty for blank-line decisions.
|
|
481
|
+
if (!body.isKind(SyntaxKind.Block)) return undefined;
|
|
482
|
+
const statements = body.getStatements();
|
|
483
|
+
return statements[statements.length - 1];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function lineStart(sourceFile: SourceFile, pos: number): number {
|
|
487
|
+
const text = sourceFile.getFullText();
|
|
488
|
+
let i = pos;
|
|
489
|
+
while (i > 0 && text[i - 1] !== "\n") i--;
|
|
490
|
+
return i;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function lineEnd(sourceFile: SourceFile, pos: number): number {
|
|
494
|
+
const text = sourceFile.getFullText();
|
|
495
|
+
let i = pos;
|
|
496
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
497
|
+
return i;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function collapsePrecedingBlankLine(sourceFile: SourceFile, startPos: number): number {
|
|
501
|
+
// If the line preceding `startPos` is empty (only whitespace), include
|
|
502
|
+
// it in the deletion range so add → remove leaves a clean file.
|
|
503
|
+
const text = sourceFile.getFullText();
|
|
504
|
+
if (startPos < 2) return startPos;
|
|
505
|
+
const i = startPos - 1; // \n at end of previous line
|
|
506
|
+
if (text[i] !== "\n") return startPos;
|
|
507
|
+
let j = i - 1;
|
|
508
|
+
while (j >= 0 && text[j] !== "\n" && (text[j] === " " || text[j] === "\t")) j--;
|
|
509
|
+
if (j < 0 || text[j] === "\n") {
|
|
510
|
+
// Found an empty (whitespace-only) preceding line — include its
|
|
511
|
+
// newline in the deletion span.
|
|
512
|
+
return j + 1;
|
|
513
|
+
}
|
|
514
|
+
return startPos;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Used only in error messages — stringifies kind + identifying fields
|
|
518
|
+
// in a `kind(field=value, ...)` shape for at-a-glance debugging.
|
|
519
|
+
function describeId(id: PatternId): string {
|
|
520
|
+
const fields = Object.entries(id as Readonly<Record<string, unknown>>)
|
|
521
|
+
.filter(([key]) => key !== "kind")
|
|
522
|
+
.map(([key, value]) => `${key}=${String(value)}`)
|
|
523
|
+
.join(", ");
|
|
524
|
+
return `${id.kind as FeaturePatternKind}(${fields})`;
|
|
525
|
+
}
|