{"id":49412074,"url":"https://github.com/jesssullivan/scheduling-bridge","last_synced_at":"2026-06-12T04:01:25.745Z","repository":{"id":346656830,"uuid":"1189011356","full_name":"Jesssullivan/scheduling-bridge","owner":"Jesssullivan","description":"Why pay for an API to rebuild a closed source wizard when you *are* a wizard?  Serverside, headless browser based Modal + container native middleware for migrating off of Acuity scheduling, on mass with zero downtime.","archived":false,"fork":false,"pushed_at":"2026-06-11T03:18:17.000Z","size":1042,"stargazers_count":1,"open_issues_count":9,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-11T03:19:47.290Z","etag":null,"topics":["brute-force","effect-ts","functional-design","modal-labs","no-api-no-problem","playwrite-in-production","scheduling-lifecycle","wip","work-harder-not-smarter"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Jesssullivan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-03-22T21:53:06.000Z","updated_at":"2026-05-14T20:37:02.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Jesssullivan/scheduling-bridge","commit_stats":null,"previous_names":["jesssullivan/acuity-middleware","jesssullivan/scheduling-bridge"],"tags_count":27,"template":false,"template_full_name":null,"purl":"pkg:github/Jesssullivan/scheduling-bridge","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Jesssullivan%2Fscheduling-bridge","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Jesssullivan%2Fscheduling-bridge/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Jesssullivan%2Fscheduling-bridge/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Jesssullivan%2Fscheduling-bridge/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Jesssullivan","download_url":"https://codeload.github.com/Jesssullivan/scheduling-bridge/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Jesssullivan%2Fscheduling-bridge/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34228097,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-12T02:00:06.859Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["brute-force","effect-ts","functional-design","modal-labs","no-api-no-problem","playwrite-in-production","scheduling-lifecycle","wip","work-harder-not-smarter"],"created_at":"2026-04-29T01:07:16.885Z","updated_at":"2026-06-12T04:01:25.739Z","avatar_url":"https://github.com/Jesssullivan.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# scheduling-bridge\n\n\u003c!-- markdownlint-disable MD013 MD040 MD060 --\u003e\n\nBackend-agnostic scheduling adapter hub. Currently bridges Acuity Scheduling via Playwright browser automation, with architecture designed to support additional scheduling backends.\n\n\u003e Formerly `acuity-middleware`. Historical GitHub URLs may redirect, but the\n\u003e canonical repo is `Jesssullivan/scheduling-bridge`.\n\n## Architecture\n\nAn HTTP server wrapping Playwright wizard flows that automate the Acuity booking UI. The bridge uses Effect TS for resource lifecycle management (browser/page acquisition and release).\n\n```\nHTTP Request\n  -\u003e server/handler.ts (route matching, auth, JSON serialization)\n    -\u003e acuity-service-catalog.ts (static env catalog -\u003e BUSINESS -\u003e scraper fallback)\n    -\u003e steps/ (Effect TS programs for each wizard stage)\n      -\u003e browser-service.ts (Playwright lifecycle via Effect Layer)\n        -\u003e selectors.ts (CSS selector registry with fallback chains)\n```\n\n### Key Components\n\n- **server/handler.ts** -- Standalone Node.js HTTP server with Bearer token auth\n- **acuity-service-catalog.ts** -- Shared service source order and cache for static config, BUSINESS extraction, and scraper fallback\n- **browser-service.ts** -- Effect TS Layers for a warm shared browser process plus request-scoped page sessions\n- **acuity-wizard.ts** -- Full `SchedulingAdapter` implementation (local Playwright or remote HTTP proxy)\n- **remote-adapter.ts** -- HTTP client adapter for proxying to a remote middleware instance\n- **selectors.ts** -- Single source of truth for all Acuity DOM selectors\n- **steps/** -- Individual wizard step programs plus BUSINESS extraction helpers\n- **acuity-scraper.ts** -- Deprecated read fallback for services, dates, and time slots\n\n## Endpoints\n\n| Method | Path                                     | Description                                                                |\n| ------ | ---------------------------------------- | -------------------------------------------------------------------------- |\n| GET    | `/health`                                | Health check (no auth required)                                            |\n| GET    | `/services`                              | List appointment types via `SERVICES_JSON` -\u003e BUSINESS -\u003e scraper fallback |\n| GET    | `/services/:id`                          | Get a specific service                                                     |\n| POST   | `/availability/dates`                    | Available dates for a service                                              |\n| POST   | `/availability/slots`                    | Time slots for a specific date                                             |\n| POST   | `/availability/check`                    | Check if a slot is available                                               |\n| POST   | `/availability/refresh`                  | Enqueue async availability refresh                                         |\n| GET    | `/availability/snapshot`                 | Read latest durable availability snapshot                                  |\n| GET    | `/internal/availability/snapshot-canary` | Auth-gated durable snapshot layer proof                                    |\n| POST   | `/internal/availability/heartbeat`       | Auth-gated bounded availability refresh heartbeat                          |\n| POST   | `/internal/availability/readiness`       | Auth-gated read-only snapshot/queue readiness check                        |\n| POST   | `/internal/availability/wait-ready`      | Auth-gated bounded heartbeat + readiness wait for deploy gates             |\n| POST   | `/booking/create`                        | Create a booking (standard)                                                |\n| POST   | `/booking/create-with-payment`           | Deprecated sync paid booking endpoint; returns `410 ASYNC_REQUIRED`        |\n| POST   | `/booking/jobs`                          | Enqueue async paid booking job                                             |\n| GET    | `/jobs/:operationId`                     | Read async job status                                                      |\n\nAvailability date/slot request handlers are snapshot-first after Redis read-cache\nmisses: a fresh durable snapshot returns immediately, a stale-but-not-expired\nsnapshot returns immediately and queues an async refresh, and an expired/missing\nsnapshot falls through to the Acuity read path. Serving a stale snapshot does\nnot re-stamp it as freshly observed; only successful Acuity reads or worker\nrefresh jobs advance snapshot freshness.\n\n`GET /internal/availability/snapshot-canary?kind=dates|slots\u0026serviceId=...\u0026scope=...`\nis an operator canary for K8s/runtime proof. It is hidden unless `AUTH_TOKEN`\nis configured, requires bearer auth when enabled, bypasses the Redis read cache,\nreads the durable snapshot store directly, and returns only metadata, count, and\nduration. Successful canary hits increment\n`acuity_availability_snapshot_served_total` and\n`acuity_availability_snapshot_read_duration_seconds`, so operators can prove the\nbridge snapshot layer separately from app Redis hits and bridge Redis hits.\n\n`POST /internal/availability/heartbeat` is the operator/cron entrypoint for\nqueue-driven availability refresh. It is hidden unless `AUTH_TOKEN` is\nconfigured and requires bearer auth when enabled. The request body contains\nweighted demand:\n\n```json\n{\n\t\"maxJobs\": 12,\n\t\"idempotencyWindowMs\": 300000,\n\t\"demands\": [\n\t\t{\n\t\t\t\"serviceId\": \"53178494\",\n\t\t\t\"serviceName\": \"TMD single session\",\n\t\t\t\"weight\": 10,\n\t\t\t\"months\": [\"2026-06\", \"2026-07\"],\n\t\t\t\"dates\": [\"2026-06-15\"]\n\t\t}\n\t]\n}\n```\n\nThe heartbeat uses weighted fairness across service/request groups before\n`maxJobs` is applied. A higher `weight` biases additional work toward that\ndemand, but every active demand group receives early representation before one\nhigh-weight service can consume the whole enqueue budget. Equal-weight demand is\nround-robin interleaved by request order. The handler skips fresh durable\nsnapshots, enqueues stale/expired/missing date and slot refresh jobs up to\n`maxJobs`, and uses a time-windowed idempotency key so frequent cron runs do not\ncreate duplicate job storms. It does not run browser automation on the HTTP\nrequest path; the async worker owns the Acuity read. If an idempotency key\nresolves to a retryable failed job, heartbeat requeues that existing operation\nbefore reporting it as work; non-retryable terminal jobs are reported under\n`skipped` instead of masquerading as newly enqueued refreshes.\n\n### Queue Hygiene\n\nBridge `0.5.13` supports bounded worker drain concurrency through\n`BRIDGE_WORKER_CONCURRENCY`. The package default remains `1`; the MassageIthaca\nK8s deployment currently opts into `2` through Blahaj/OpenTofu after proving the\ndatepicker readiness gate remains green.\n\nReadiness and queue stats are related but not identical. A scoped datepicker\nreadiness check can be green while historical retryable refresh failures remain\nin the async store until the store TTL expires. Live K8s sampling after the\n`0.5.9` rollout found fresh datepicker snapshots and no runnable backlog, but\nalso retained failed refresh records from transient browser/network failures:\n\n- `NETWORK` / `PAGE_FAILED` on date and slot refresh jobs\n- `SCRAPE_FAILED` calendar-load timeouts on date refresh jobs\n\nTrack this as queue-hygiene work, not a regression of the sustained datepicker\ngate. `Jesssullivan/scheduling-bridge#129` owns the next package pass: failed\nrefresh observability, retention/TTL configuration, retry/stat semantics, and\ntests.\n\n`POST /internal/availability/readiness` is the operator read path for\ncutover/deploy proof. It accepts the same demand shape as heartbeat, does not\nenqueue jobs, and returns `200` when every requested date/slot scope has a\nsnapshot newer than the configured freshness floor and the async queue is\nhealthy. It returns `409` with explicit blockers when any scope is missing,\nstale, expired, retryable-failed, or when the oldest runnable queue item is too\nold. Defaults are `snapshotFreshnessFloorMs=90000` and\n`maxOldestQueuedAgeMs=120000`.\n\n`POST /internal/availability/wait-ready` is the bounded deploy/operator action.\nIt runs the existing heartbeat enqueue/requeue logic once, then polls the\nread-only readiness evaluator until ready or timeout. It never runs Acuity\nbrowser automation in the HTTP request; workers still own Acuity reads. Defaults\nare `timeoutMs=60000` and `pollMs=1000`.\n\n### Health Contract\n\n`GET /health` is the stable downstream runtime-truth surface.\n\nIn addition to basic runtime data, it now publishes:\n\n- release tuple:\n  - `releaseSha`\n  - `releaseRef`\n  - `releaseVersion`\n  - `releaseBuiltAt`\n  - nested `release.{ sha, ref, version, builtAt, modalEnvironment }`\n- protocol tuple:\n  - `protocolVersion`\n  - nested `protocol.version`\n  - `protocol.flowOwner = \"scheduling-bridge\"`\n  - `protocol.backend = \"acuity\"`\n  - `protocol.transport = \"http-json\"`\n  - `protocol.endpoints`\n  - `protocol.capabilities`\n\nDownstream apps should use this tuple to assert which bridge release and protocol\nsurface they are talking to during beta validation and rollout claims.\n\nThis tuple is the supported runtime truth surface for adopters. Downstream apps\nshould not infer bridge ownership from package metadata, branch names, or Modal\ndashboard state when `/health` is available.\n\n## Environment Variables\n\n| Variable                                      | Required                                 | Default                                     | Description                                                                                                                  |\n| --------------------------------------------- | ---------------------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |\n| `PORT`                                        | No                                       | `3001`                                      | Server port                                                                                                                  |\n| `ACUITY_BASE_URL`                             | No                                       | `https://example.as.me`                     | Acuity scheduling page URL                                                                                                   |\n| `BRIDGE_DATABASE_URL`                         | For strict async runtime                 | --                                          | Postgres queue/snapshot store for async jobs; takes precedence over Redis                                                    |\n| `BRIDGE_DATABASE_SSL`                         | No                                       | `false`                                     | Enable SSL for `BRIDGE_DATABASE_URL`                                                                                         |\n| `BRIDGE_DATABASE_MIGRATE`                     | No                                       | `true`                                      | Run async queue/snapshot schema creation at startup                                                                          |\n| `REDIS_URL`                                   | For K8s read cache / Redis async runtime | --                                          | Redis read cache plus async queue/snapshot store when `BRIDGE_DATABASE_URL` is unset                                         |\n| `REDIS_PASSWORD`                              | No                                       | --                                          | Password for `REDIS_URL`                                                                                                     |\n| `BRIDGE_REDIS_ASYNC_PREFIX`                   | No                                       | `bridge-async:v1`                           | Redis key prefix for async jobs and snapshots                                                                                |\n| `BRIDGE_REDIS_ASYNC_JOB_TTL_SECONDS`          | No                                       | `604800`                                    | Redis TTL for async job and idempotency records; lower in K8s if historical retryable refresh failures make queue stats noisy |\n| `BRIDGE_INLINE_WORKER_ENABLED`                | No                                       | `true` when Postgres or Redis is configured | Drain async jobs inside the HTTP container; set `false` only when a separate worker deployment is active                     |\n| `BRIDGE_WORKER_POLL_MS`                       | No                                       | `1000`                                      | Worker queue poll interval                                                                                                   |\n| `BRIDGE_WORKER_BATCH_SIZE`                    | No                                       | `5`                                         | Maximum jobs drained per worker poll                                                                                         |\n| `BRIDGE_WORKER_CONCURRENCY`                   | No                                       | `1`                                         | Maximum jobs executed concurrently within one worker poll; raise only when browser/page limiter and queue metrics support it |\n| `BRIDGE_SNAPSHOT_STALE_MS`                    | No                                       | `300000`                                    | Age after which a durable availability snapshot is served stale and refresh is queued                                        |\n| `BRIDGE_SNAPSHOT_EXPIRES_MS`                  | No                                       | `1800000`                                   | Age after which a durable availability snapshot is ignored and a live Acuity read is required                                |\n| `BRIDGE_HEARTBEAT_MAX_JOBS`                   | No                                       | `12`                                        | Default max refresh jobs enqueued by one internal heartbeat request; request values are capped at `100`                      |\n| `BRIDGE_HEARTBEAT_IDEMPOTENCY_WINDOW_MS`      | No                                       | `300000`                                    | Default time bucket for heartbeat idempotency keys                                                                           |\n| `BRIDGE_READINESS_FRESHNESS_FLOOR_MS`         | No                                       | `90000`                                     | Default required snapshot freshness for internal readiness gates                                                             |\n| `BRIDGE_READINESS_MAX_OLDEST_QUEUED_AGE_MS`   | No                                       | `120000`                                    | Default maximum oldest runnable queue age before readiness fails                                                             |\n| `BRIDGE_READINESS_WAIT_TIMEOUT_MS`            | No                                       | `60000`                                     | Default timeout for `/internal/availability/wait-ready`                                                                      |\n| `BRIDGE_READINESS_WAIT_POLL_MS`               | No                                       | `1000`                                      | Default poll interval for `/internal/availability/wait-ready`                                                                |\n| `AUTH_TOKEN`                                  | Recommended                              | --                                          | Bearer token for all endpoints (except /health)                                                                              |\n| `ACUITY_BYPASS_COUPON`                        | For payment bypass                       | --                                          | 100% gift certificate code                                                                                                   |\n| `PLAYWRIGHT_HEADLESS`                         | No                                       | `true`                                      | Run browser headless                                                                                                         |\n| `PLAYWRIGHT_TIMEOUT`                          | No                                       | `30000`                                     | Page operation timeout (ms)                                                                                                  |\n| `CHROMIUM_EXECUTABLE_PATH`                    | No                                       | --                                          | Custom Chromium path (for Lambda/serverless)                                                                                 |\n| `CHROMIUM_LAUNCH_ARGS`                        | No                                       | --                                          | Comma-separated Chromium args                                                                                                |\n| `SERVICES_JSON`                               | No                                       | --                                          | Optional static service catalog to bypass live Acuity reads                                                                  |\n| `ACUITY_SERVICE_CACHE_TTL_MS`                 | No                                       | `300000`                                    | TTL for cached live service catalogs before BUSINESS/scraper refresh                                                         |\n| `ACUITY_URL_READ_NETWORK_IDLE_MS`             | No                                       | `1500`                                      | Bounded post-navigation network-idle settle for direct URL availability reads; set `0` to skip                               |\n| `ACUITY_DATE_PREWARM_MONTHS`                  | No                                       | `1`                                         | Number of future months queued for async date refresh after a successful date read; max `3`, set `0` to disable              |\n| `ACUITY_SLOT_PREWARM_LIMIT`                   | No                                       | `1`                                         | Number of first available dates to warm in the slots cache after a successful Acuity dates read; max `3`, set `0` to disable |\n| `SCHEDULING_BRIDGE_SLOT_PROFILE_THRESHOLD_MS` | No                                       | `1500`                                      | Threshold in ms for logging long-tail slot-read profile events                                                               |\n| `SCHEDULING_BRIDGE_PROFILE_SLOT_READS`        | No                                       | `false`                                     | Force logging of slot-read profile events even when under threshold                                                          |\n| `MIDDLEWARE_RELEASE_SHA`                      | No                                       | --                                          | Release commit SHA exposed via `/health`                                                                                     |\n| `MIDDLEWARE_RELEASE_REF`                      | No                                       | --                                          | Release ref/tag exposed via `/health`                                                                                        |\n| `MIDDLEWARE_RELEASE_VERSION`                  | No                                       | --                                          | Release version exposed via `/health`                                                                                        |\n| `MIDDLEWARE_RELEASE_BUILT_AT`                 | No                                       | --                                          | Build timestamp exposed via `/health`                                                                                        |\n| `MIDDLEWARE_BUILD_TIMESTAMP`                  | No                                       | --                                          | Legacy fallback build timestamp for `/health`                                                                                |\n\n### Observability\n\nThe bridge emits NDJSON logs to stdout/stderr for runtime analysis.\n\n- `/health` remains the authoritative runtime-truth surface for downstream apps\n- request handlers emit request-scoped structured events, including `requestId`\n- long-tail slot reads emit `slot_read_profile` events with phase timings\n- `SCHEDULING_BRIDGE_PROFILE_SLOT_READS=1` forces profile emission for all slot reads\n- internal readiness emits queue depth, oldest queue age, snapshot age, readiness\n  result, and per-scope freshness metrics\n\n## Deployment\n\n### Runtime Provider Truth\n\nThe stable bridge contract is the Node HTTP server, protocol surface, and\n`/health` tuple. Provider names are deployment details, not the consumer\ncontract.\n\n- Accepted next-production route: K8s/container runtime managed from\n  infrastructure.\n- Legacy proofing provider: Modal. Automatic Modal deploys are disabled; the\n  manual workflow is retained only for deliberate decommissioning or\n  forensic fallback while TIN-981 closes the surface.\n- Compatibility target: Docker image with the same `dist/server/handler.js`\n  entrypoint.\n- Consumer apps should configure the remote bridge with\n  `SCHEDULING_BRIDGE_URL` and `SCHEDULING_BRIDGE_AUTH_TOKEN`; legacy\n  `MODAL_*` names are transition aliases in consumer/infra repos.\n\n## Node Runtime Policy\n\nThe npm package supports active downstream consumer runtimes on Node 22 and\nNode 24. CI validates the host test suite on both majors.\n\nThe bridge-owned Bazel toolchain, Nix dev shell, Docker/K8s image, and publish\nworkflow intentionally stay on Node 24. Downstream apps should not infer that\nthey must also run Node 24 unless they deploy the bridge runtime itself.\n\n### Standalone Node.js\n\n```bash\npnpm install\npnpm dev           # Development with tsx against src/server/handler.ts\n# or\npnpm build \u0026\u0026 pnpm start  # Materialize Bazel-derived dist/ and start it\n```\n\n### Docker\n\n```bash\ndocker build -t scheduling-bridge .\ndocker run -p 3001:3001 \\\n  -e AUTH_TOKEN=your-secret-token \\\n  -e ACUITY_BASE_URL=https://YourBusiness.as.me \\\n  -e ACUITY_BYPASS_COUPON=your-coupon-code \\\n  scheduling-bridge\n```\n\n### Legacy Modal Labs\n\n```bash\n# Automatic Modal deploys are disabled. The manual workflow requires an\n# explicit legacy-modal acknowledgement and should only be used for\n# decommissioning or forensic fallback while TIN-981 is open.\n# The Modal workflow materializes the Bazel-derived pkg/ before deploy.\nmodal deploy modal-app.py\n```\n\n#### Supported fallback deployment path\n\nThe Modal deployment path is legacy-only:\n\n1. manually dispatch `.github/workflows/deploy-modal.yml`\n2. type `legacy-modal` in `acknowledge_legacy_modal`\n3. inject `MIDDLEWARE_RELEASE_SHA`, `MIDDLEWARE_RELEASE_REF`,\n   `MIDDLEWARE_RELEASE_VERSION`, and `MIDDLEWARE_RELEASE_BUILT_AT`\n4. verify the resulting bridge tuple via `GET /health`\n\nOperationally, this means:\n\n- Modal deployment is not automatic release truth\n- the live bridge should be identified by the `/health` release + protocol tuple\n- downstream apps should validate the tuple they expect before making rollout claims\n\n### Nix\n\n```bash\nnix develop   # Enter dev shell with Node.js + Playwright\npnpm install\npnpm dev\n```\n\n\u003c!-- markdownlint-enable MD013 MD040 MD060 --\u003e\n\n## Release Authority\n\nCurrent release authority:\n\n- canonical repo: `Jesssullivan/scheduling-bridge`\n- SSOT delivery mechanism: the Bzlmod module graph\n  (`tummycrypt_scheduling_bridge`) via `tinyland-inc/bazel-registry`\n- derived package: GitHub Packages `@jesssullivan/scheduling-bridge`, built\n  from the Bazel `//:pkg` artifact\n- npmjs (`@tummycrypt/scheduling-bridge`): retired for new versions, frozen at\n  `0.5.11`; `npm_publish_mode: disabled` is permanent policy\n- `@tummycrypt/scheduling-kit` is resolved from the Bzlmod module graph\n  (`bazel_dep` on `tummycrypt_scheduling_kit`), not from npm; it is deliberately\n  absent from `package.json` `dependencies`, but it is declared as a required\n  `peerDependency` so npm-style consumers of the derived GitHub Packages\n  artifact get an explicit, enforced kit requirement instead of a silent\n  runtime import failure\n- out-of-ecosystem (npm-style) consumers of the GitHub Packages artifact must\n  satisfy that peer themselves with both aliases:\n\n  ```json\n  {\n    \"dependencies\": {\n      \"@tummycrypt/scheduling-bridge\": \"npm:@jesssullivan/scheduling-bridge@^0.5.14\",\n      \"@tummycrypt/scheduling-kit\": \"npm:@jesssullivan/scheduling-kit@^0.9.1\"\n    }\n  }\n  ```\n\n  The kit alias is the required companion of the bridge's\n  `@tummycrypt/scheduling-kit` peer dependency: the bridge's\n  `capabilities` surface unconditionally imports\n  `@tummycrypt/scheduling-kit/payments` at runtime. Bazel consumers need\n  neither alias — the module graph supplies the kit.\n\nThe current publish + deploy shape is:\n\n1. release metadata declared once\n2. Bazel validates/builds the publishable artifact\n3. CI dry-runs the extracted Bazel package surface before release\n4. GitHub Actions publishes the derived GitHub Packages artifact while the\n   Bazel registry carries the module graph truth\n5. infrastructure can deploy the K8s/container runtime from the same package\n   artifact and image entrypoint\n6. downstream apps consume the published package and verify the live runtime\n   tuple via `/health`\n\nThis repo is the sole owner of Acuity automation concerns. App repos and shared\npackages may consume the bridge and assert its runtime tuple, but they should\nnot duplicate bridge runtime ownership or release truth logic.\n\n## Runner Authority\n\nPackage CI and publish currently use the shared `js-bazel-package` workflow with\n`runner_mode: shared` and `publish_mode: same_runner`. The publish workflow sets\n`npm_publish_mode: disabled` and carries no npm token; npmjs publication is\nretired.\n\nThe concrete shared-runner labels come from repository Actions variables and\nmust be proven by green workflow runs before they are treated as operational\ntruth. Keep private runner topology and apply details out of this public repo.\n\n## Development\n\n```bash\npnpm install      # Install dependencies\npnpm dev          # Start dev server with tsx\npnpm typecheck    # Run Bazel typecheck target\npnpm build        # Materialize local pkg/ and dist/ from bazel-bin/pkg\npnpm test         # Run Bazel test target\npnpm docs:generate\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjesssullivan%2Fscheduling-bridge","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjesssullivan%2Fscheduling-bridge","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjesssullivan%2Fscheduling-bridge/lists"}