@aglyn/aglyn 1.0.0-beta.217 → 1.0.0-beta.219
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/package.json +14 -14
- package/src/lib/app-utils/actions.d.ts +25 -374
- package/src/lib/app-utils/actions.js +101 -561
- package/src/lib/app-utils/actions.js.map +1 -1
- package/src/lib/app-utils/activity-presenter.d.ts +3 -3
- package/src/lib/app-utils/activity-presenter.js +36 -52
- package/src/lib/app-utils/activity-presenter.js.map +1 -1
- package/src/lib/app-utils/admin-audit-index.js +2 -2
- package/src/lib/app-utils/admin-audit-index.js.map +1 -1
- package/src/lib/app-utils/analytics-events.d.ts +11 -2
- package/src/lib/app-utils/analytics-events.js +11 -2
- package/src/lib/app-utils/analytics-events.js.map +1 -1
- package/src/lib/app-utils/analytics-provider.d.ts +182 -0
- package/src/lib/app-utils/analytics-provider.js +133 -0
- package/src/lib/app-utils/analytics-provider.js.map +1 -0
- package/src/lib/app-utils/api-plugins.d.ts +46 -0
- package/src/lib/app-utils/api-plugins.js +19 -0
- package/src/lib/app-utils/api-plugins.js.map +1 -1
- package/src/lib/app-utils/{marketplace-overrides.d.ts → artifact-overrides.d.ts} +9 -6
- package/src/lib/app-utils/{marketplace-overrides.js → artifact-overrides.js} +10 -7
- package/src/lib/app-utils/artifact-overrides.js.map +1 -0
- package/src/lib/app-utils/{marketplace-provenance.d.ts → artifact-provenance.d.ts} +12 -10
- package/src/lib/app-utils/{marketplace-provenance.js → artifact-provenance.js} +9 -7
- package/src/lib/app-utils/artifact-provenance.js.map +1 -0
- package/src/lib/app-utils/assist-credits.js +2 -1
- package/src/lib/app-utils/assist-credits.js.map +1 -1
- package/src/lib/app-utils/binding-token-catalog.d.ts +5 -13
- package/src/lib/app-utils/binding-token-catalog.js +17 -41
- package/src/lib/app-utils/binding-token-catalog.js.map +1 -1
- package/src/lib/app-utils/binding-tokens.js +1 -1
- package/src/lib/app-utils/binding-tokens.js.map +1 -1
- package/src/lib/app-utils/brand-support-line.d.ts +36 -0
- package/src/lib/app-utils/brand-support-line.js +38 -0
- package/src/lib/app-utils/brand-support-line.js.map +1 -0
- package/src/lib/app-utils/campaign-touch.d.ts +5 -5
- package/src/lib/app-utils/campaign-touch.js +8 -8
- package/src/lib/app-utils/campaign-touch.js.map +1 -1
- package/src/lib/app-utils/collection-entries.d.ts +2 -2
- package/src/lib/app-utils/collection-entries.js.map +1 -1
- package/src/lib/app-utils/consent-group-change.d.ts +4 -0
- package/src/lib/app-utils/consent-group-change.js.map +1 -1
- package/src/lib/app-utils/console-record-links.d.ts +10 -89
- package/src/lib/app-utils/console-record-links.js +3 -107
- package/src/lib/app-utils/console-record-links.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +16 -7
- package/src/lib/app-utils/console-routes.js +9 -3
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/contacts.d.ts +23 -1
- package/src/lib/app-utils/contacts.js +21 -5
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/cross-pool-desync.d.ts +39 -0
- package/src/lib/app-utils/cross-pool-desync.js +47 -0
- package/src/lib/app-utils/cross-pool-desync.js.map +1 -0
- package/src/lib/app-utils/csv-upload.d.ts +29 -0
- package/src/lib/app-utils/csv-upload.js +29 -0
- package/src/lib/app-utils/csv-upload.js.map +1 -0
- package/src/lib/app-utils/dataset-models.d.ts +5 -5
- package/src/lib/app-utils/dataset-models.js +19 -2
- package/src/lib/app-utils/dataset-models.js.map +1 -1
- package/src/lib/app-utils/datasets.d.ts +65 -11
- package/src/lib/app-utils/datasets.js +74 -11
- package/src/lib/app-utils/datasets.js.map +1 -1
- package/src/lib/app-utils/docs-deflection.d.ts +6 -6
- package/src/lib/app-utils/docs-deflection.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +12 -6
- package/src/lib/app-utils/docs-help.generated.js +21 -6
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.d.ts +2 -2
- package/src/lib/app-utils/docs-index.generated.js +514 -318
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/docs-retrieval.d.ts +3 -3
- package/src/lib/app-utils/docs-retrieval.js +14 -10
- package/src/lib/app-utils/docs-retrieval.js.map +1 -1
- package/src/lib/app-utils/duplicate-resource.d.ts +4 -1
- package/src/lib/app-utils/duplicate-resource.js +7 -4
- package/src/lib/app-utils/duplicate-resource.js.map +1 -1
- package/src/lib/app-utils/dynamic-list-rule.d.ts +1 -1
- package/src/lib/app-utils/dynamic-list-rule.js.map +1 -1
- package/src/lib/app-utils/element-ui.d.ts +1 -1
- package/src/lib/app-utils/element-ui.js +1 -1
- package/src/lib/app-utils/element-ui.js.map +1 -1
- package/src/lib/app-utils/email-starter-policy.js +3 -3
- package/src/lib/app-utils/email-starter-policy.js.map +1 -1
- package/src/lib/app-utils/entity-picker-context.d.ts +10 -5
- package/src/lib/app-utils/entity-picker-context.js.map +1 -1
- package/src/lib/app-utils/error-beacon.d.ts +75 -6
- package/src/lib/app-utils/error-beacon.js +160 -21
- package/src/lib/app-utils/error-beacon.js.map +1 -1
- package/src/lib/app-utils/expand-repeatables.d.ts +10 -10
- package/src/lib/app-utils/expand-repeatables.js +10 -10
- package/src/lib/app-utils/expand-repeatables.js.map +1 -1
- package/src/lib/app-utils/host-events.d.ts +82 -0
- package/src/lib/app-utils/host-events.js +62 -0
- package/src/lib/app-utils/host-events.js.map +1 -0
- package/src/lib/app-utils/lockdown.d.ts +33 -1
- package/src/lib/app-utils/lockdown.js +45 -4
- package/src/lib/app-utils/lockdown.js.map +1 -1
- package/src/lib/app-utils/margin-utilization.d.ts +4 -69
- package/src/lib/app-utils/margin-utilization.js +82 -69
- package/src/lib/app-utils/margin-utilization.js.map +1 -1
- package/src/lib/app-utils/node-definition-sanitizer.d.ts +16 -16
- package/src/lib/app-utils/node-definition-sanitizer.js +24 -20
- package/src/lib/app-utils/node-definition-sanitizer.js.map +1 -1
- package/src/lib/app-utils/node-interactions.d.ts +7 -7
- package/src/lib/app-utils/node-interactions.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +78 -47
- package/src/lib/app-utils/notifications.js +63 -44
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/payments-configured.d.ts +8 -21
- package/src/lib/app-utils/payments-configured.js +8 -23
- package/src/lib/app-utils/payments-configured.js.map +1 -1
- package/src/lib/app-utils/plan-entitlements.d.ts +43 -128
- package/src/lib/app-utils/plan-entitlements.js +145 -304
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/platform-brand.d.ts +10 -0
- package/src/lib/app-utils/platform-brand.js +14 -3
- package/src/lib/app-utils/platform-brand.js.map +1 -1
- package/src/lib/app-utils/plugin-api-rate-limit.d.ts +7 -1
- package/src/lib/app-utils/plugin-api-rate-limit.js +11 -3
- package/src/lib/app-utils/plugin-api-rate-limit.js.map +1 -1
- package/src/lib/app-utils/plugin-bundle-checks.js +2 -2
- package/src/lib/app-utils/plugin-bundle-checks.js.map +1 -1
- package/src/lib/app-utils/plugin-host-events.generated.d.ts +12 -0
- package/src/lib/app-utils/plugin-host-events.generated.js +169 -0
- package/src/lib/app-utils/plugin-host-events.generated.js.map +1 -0
- package/src/lib/app-utils/plugin-release-flags.generated.d.ts +12 -0
- package/src/lib/app-utils/plugin-release-flags.generated.js +106 -0
- package/src/lib/app-utils/plugin-release-flags.generated.js.map +1 -0
- package/src/lib/app-utils/realm-host-surface.generated.js +2 -0
- package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
- package/src/lib/app-utils/release-flags.d.ts +14 -5
- package/src/lib/app-utils/release-flags.js +21 -125
- package/src/lib/app-utils/release-flags.js.map +1 -1
- package/src/lib/app-utils/repeat-sources.d.ts +9 -0
- package/src/lib/app-utils/repeat-sources.js.map +1 -1
- package/src/lib/app-utils/retained-refusals.d.ts +73 -0
- package/src/lib/app-utils/retained-refusals.js +186 -0
- package/src/lib/app-utils/retained-refusals.js.map +1 -0
- package/src/lib/app-utils/scope-tokens.d.ts +27 -0
- package/src/lib/app-utils/scope-tokens.js +61 -0
- package/src/lib/app-utils/scope-tokens.js.map +1 -1
- package/src/lib/app-utils/screen-link-context.js +3 -3
- package/src/lib/app-utils/screen-link-context.js.map +1 -1
- package/src/lib/app-utils/screen-route.d.ts +15 -0
- package/src/lib/app-utils/screen-route.js +21 -1
- package/src/lib/app-utils/screen-route.js.map +1 -1
- package/src/lib/app-utils/seo-audit.d.ts +132 -0
- package/src/lib/app-utils/seo-audit.js +246 -0
- package/src/lib/app-utils/seo-audit.js.map +1 -0
- package/src/lib/app-utils/seo-keywords.d.ts +48 -0
- package/src/lib/app-utils/seo-keywords.js +60 -0
- package/src/lib/app-utils/seo-keywords.js.map +1 -0
- package/src/lib/app-utils/seo-page-facts.d.ts +66 -0
- package/src/lib/app-utils/seo-page-facts.js +155 -0
- package/src/lib/app-utils/seo-page-facts.js.map +1 -0
- package/src/lib/app-utils/seo-site-scan.d.ts +126 -0
- package/src/lib/app-utils/seo-site-scan.js +162 -0
- package/src/lib/app-utils/seo-site-scan.js.map +1 -0
- package/src/lib/app-utils/seo-title-variables.js +1 -1
- package/src/lib/app-utils/seo-title-variables.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +6 -9
- package/src/lib/app-utils/server.js +17 -27
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/site-interactions.d.ts +501 -0
- package/src/lib/app-utils/site-interactions.js +554 -0
- package/src/lib/app-utils/site-interactions.js.map +1 -0
- package/src/lib/app-utils/{marketplace-theme.d.ts → site-theme.d.ts} +13 -2
- package/src/lib/app-utils/{marketplace-theme.js → site-theme.js} +5 -2
- package/src/lib/app-utils/site-theme.js.map +1 -0
- package/src/lib/app-utils/sitemap.d.ts +4 -3
- package/src/lib/app-utils/sitemap.js +9 -7
- package/src/lib/app-utils/sitemap.js.map +1 -1
- package/src/lib/app-utils/stale-build-error.d.ts +51 -0
- package/src/lib/app-utils/stale-build-error.js +100 -1
- package/src/lib/app-utils/stale-build-error.js.map +1 -1
- package/src/lib/app-utils/starter-templates.d.ts +20 -0
- package/src/lib/app-utils/starter-templates.js +49 -0
- package/src/lib/app-utils/starter-templates.js.map +1 -1
- package/src/lib/app-utils/tax-jurisdiction-figures.d.ts +118 -0
- package/src/lib/app-utils/tax-jurisdiction-figures.js +123 -0
- package/src/lib/app-utils/tax-jurisdiction-figures.js.map +1 -0
- package/src/lib/app-utils/theme-library.d.ts +10 -3
- package/src/lib/app-utils/theme-library.js +13 -4
- package/src/lib/app-utils/theme-library.js.map +1 -1
- package/src/lib/app-utils/{campaign-attribution.d.ts → utm-attribution.d.ts} +11 -6
- package/src/lib/app-utils/{campaign-attribution.js → utm-attribution.js} +13 -8
- package/src/lib/app-utils/utm-attribution.js.map +1 -0
- package/src/lib/app-utils/{campaign-forwarding.d.ts → utm-forwarding.d.ts} +14 -14
- package/src/lib/app-utils/{campaign-forwarding.js → utm-forwarding.js} +26 -26
- package/src/lib/app-utils/utm-forwarding.js.map +1 -0
- package/src/lib/app-utils/video-object.js +6 -5
- package/src/lib/app-utils/video-object.js.map +1 -1
- package/src/lib/app-utils/visitor-consent.d.ts +41 -164
- package/src/lib/app-utils/visitor-consent.js +66 -277
- package/src/lib/app-utils/visitor-consent.js.map +1 -1
- package/src/lib/app-utils/web-vitals-rum.d.ts +7 -7
- package/src/lib/app-utils/web-vitals-rum.js +31 -63
- package/src/lib/app-utils/web-vitals-rum.js.map +1 -1
- package/src/lib/app-utils/where-used.d.ts +11 -6
- package/src/lib/app-utils/where-used.js +3 -8
- package/src/lib/app-utils/where-used.js.map +1 -1
- package/src/lib/canvas-manager/canvas-manager.js +3 -3
- package/src/lib/canvas-manager/canvas-manager.js.map +1 -1
- package/src/lib/foundation/definitions/components.types.d.ts +1 -1
- package/src/lib/foundation/definitions/components.types.js +1 -1
- package/src/lib/foundation/definitions/components.types.js.map +1 -1
- package/src/lib/foundation/definitions/org-billing.types.d.ts +18 -135
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/foundation/definitions/organization.types.d.ts +6 -3
- package/src/lib/foundation/definitions/organization.types.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +27 -5
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/lib/foundation/definitions/write-deny-coverage.util.d.ts +16 -0
- package/src/lib/foundation/definitions/write-deny-coverage.util.js +20 -0
- package/src/lib/foundation/definitions/write-deny-coverage.util.js.map +1 -1
- package/src/lib/foundation/index.d.ts +1 -1
- package/src/lib/foundation/index.js.map +1 -1
- package/src/lib/plugin-manager/besigner-documents.d.ts +96 -0
- package/src/lib/plugin-manager/besigner-documents.js +61 -0
- package/src/lib/plugin-manager/besigner-documents.js.map +1 -0
- package/src/lib/plugin-manager/computed-variables.d.ts +71 -0
- package/src/lib/plugin-manager/computed-variables.js +74 -0
- package/src/lib/plugin-manager/computed-variables.js.map +1 -0
- package/src/lib/plugin-manager/custom-fields.d.ts +29 -2
- package/src/lib/plugin-manager/custom-fields.js +47 -1
- package/src/lib/plugin-manager/custom-fields.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +292 -20
- package/src/lib/plugin-manager/feature-plugins.js +105 -17
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +123 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js +785 -14
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-activity-actions.d.ts +12 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js +25 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-contributions.d.ts +1 -1
- package/src/lib/plugin-manager/plugin-contributions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-declarations-repair.d.ts +36 -0
- package/src/lib/plugin-manager/plugin-declarations-repair.js +64 -0
- package/src/lib/plugin-manager/plugin-declarations-repair.js.map +1 -0
- package/src/lib/plugin-manager/plugin-dependents.d.ts +101 -0
- package/src/lib/plugin-manager/plugin-dependents.js +75 -0
- package/src/lib/plugin-manager/plugin-dependents.js.map +1 -0
- package/src/lib/plugin-manager/plugin-distribution.d.ts +55 -0
- package/src/lib/plugin-manager/plugin-distribution.js +45 -0
- package/src/lib/plugin-manager/plugin-distribution.js.map +1 -0
- package/src/lib/plugin-manager/plugin-host-collections.d.ts +17 -0
- package/src/lib/plugin-manager/plugin-host-collections.js +6 -0
- package/src/lib/plugin-manager/plugin-host-collections.js.map +1 -1
- package/src/lib/plugin-manager/plugin-host-resources.d.ts +133 -0
- package/src/lib/plugin-manager/plugin-host-resources.js +52 -0
- package/src/lib/plugin-manager/plugin-host-resources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-org-erasure.d.ts +28 -2
- package/src/lib/plugin-manager/plugin-org-erasure.js +61 -2
- package/src/lib/plugin-manager/plugin-org-erasure.js.map +1 -1
- package/src/lib/plugin-manager/plugin-plan-entitlements.d.ts +123 -0
- package/src/lib/plugin-manager/plugin-plan-entitlements.js +101 -0
- package/src/lib/plugin-manager/plugin-plan-entitlements.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-index.d.ts +95 -0
- package/src/lib/plugin-manager/plugin-record-index.js +51 -0
- package/src/lib/plugin-manager/plugin-record-index.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-written.d.ts +58 -0
- package/src/lib/plugin-manager/plugin-record-written.js +42 -0
- package/src/lib/plugin-manager/plugin-record-written.js.map +1 -0
- package/src/lib/plugin-manager/plugin-revenue-sources.d.ts +213 -0
- package/src/lib/plugin-manager/plugin-revenue-sources.js +194 -0
- package/src/lib/plugin-manager/plugin-revenue-sources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-site-bundle.d.ts +145 -0
- package/src/lib/plugin-manager/plugin-site-bundle.js +78 -0
- package/src/lib/plugin-manager/plugin-site-bundle.js.map +1 -0
- package/src/lib/plugin-manager/plugin-site-cache.d.ts +12 -0
- package/src/lib/plugin-manager/plugin-site-cache.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-export.d.ts +81 -0
- package/src/lib/plugin-manager/plugin-site-export.js +40 -0
- package/src/lib/plugin-manager/plugin-site-export.js.map +1 -0
- package/src/lib/plugin-manager/plugin-sitemap-sections.d.ts +88 -0
- package/src/lib/plugin-manager/plugin-sitemap-sections.js +29 -0
- package/src/lib/plugin-manager/plugin-sitemap-sections.js.map +1 -0
- package/src/lib/plugin-manager/plugin-tax-profile.d.ts +20 -4
- package/src/lib/plugin-manager/plugin-tax-profile.js.map +1 -1
- package/src/lib/plugin-manager/plugin-tax-return-sources.d.ts +255 -0
- package/src/lib/plugin-manager/plugin-tax-return-sources.js +191 -0
- package/src/lib/plugin-manager/plugin-tax-return-sources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-template-sources.d.ts +75 -0
- package/src/lib/plugin-manager/plugin-template-sources.js +37 -0
- package/src/lib/plugin-manager/plugin-template-sources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-usage-axes.d.ts +107 -0
- package/src/lib/plugin-manager/plugin-usage-axes.js +107 -0
- package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -0
- package/src/lib/plugin-manager/plugin-zones.d.ts +4 -5
- package/src/lib/plugin-manager/plugin-zones.js.map +1 -1
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js +2 -0
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
- package/src/lib/plugin-manager/record-captured-contact.d.ts +1 -14
- package/src/lib/plugin-manager/record-captured-contact.js +10 -32
- package/src/lib/plugin-manager/record-captured-contact.js.map +1 -1
- package/src/lib/plugin-manager/repeat-rows.d.ts +124 -0
- package/src/lib/plugin-manager/repeat-rows.js +108 -0
- package/src/lib/plugin-manager/repeat-rows.js.map +1 -0
- package/src/lib/plugin-manager/submission-record-target.d.ts +144 -0
- package/src/lib/plugin-manager/submission-record-target.js +125 -0
- package/src/lib/plugin-manager/submission-record-target.js.map +1 -0
- package/src/lib/plugin-manager/video-embed-provider.d.ts +108 -0
- package/src/lib/plugin-manager/video-embed-provider.js +165 -0
- package/src/lib/plugin-manager/video-embed-provider.js.map +1 -0
- package/src/lib/types/nodes.d.ts +2 -2
- package/src/lib/types/nodes.js.map +1 -1
- package/src/lib/app-utils/campaign-attribution.js.map +0 -1
- package/src/lib/app-utils/campaign-forwarding.js.map +0 -1
- package/src/lib/app-utils/contact-holder.d.ts +0 -68
- package/src/lib/app-utils/contact-holder.js +0 -129
- package/src/lib/app-utils/contact-holder.js.map +0 -1
- package/src/lib/app-utils/contact-merge.d.ts +0 -136
- package/src/lib/app-utils/contact-merge.js +0 -412
- package/src/lib/app-utils/contact-merge.js.map +0 -1
- package/src/lib/app-utils/crm-csv.d.ts +0 -239
- package/src/lib/app-utils/crm-csv.js +0 -467
- package/src/lib/app-utils/crm-csv.js.map +0 -1
- package/src/lib/app-utils/crm-digest.d.ts +0 -185
- package/src/lib/app-utils/crm-digest.js +0 -272
- package/src/lib/app-utils/crm-digest.js.map +0 -1
- package/src/lib/app-utils/form-dataset-binding.d.ts +0 -50
- package/src/lib/app-utils/form-dataset-binding.js +0 -74
- package/src/lib/app-utils/form-dataset-binding.js.map +0 -1
- package/src/lib/app-utils/list-import.d.ts +0 -158
- package/src/lib/app-utils/list-import.js +0 -332
- package/src/lib/app-utils/list-import.js.map +0 -1
- package/src/lib/app-utils/marketplace-listing-visibility.d.ts +0 -122
- package/src/lib/app-utils/marketplace-listing-visibility.js +0 -147
- package/src/lib/app-utils/marketplace-listing-visibility.js.map +0 -1
- package/src/lib/app-utils/marketplace-overrides.js.map +0 -1
- package/src/lib/app-utils/marketplace-provenance.js.map +0 -1
- package/src/lib/app-utils/marketplace-theme.js.map +0 -1
- package/src/lib/app-utils/marketplace-update-state.d.ts +0 -119
- package/src/lib/app-utils/marketplace-update-state.js +0 -132
- package/src/lib/app-utils/marketplace-update-state.js.map +0 -1
- package/src/lib/app-utils/publisher-agreement.d.ts +0 -289
- package/src/lib/app-utils/publisher-agreement.js +0 -321
- package/src/lib/app-utils/publisher-agreement.js.map +0 -1
- package/src/lib/app-utils/wistia-embed.d.ts +0 -104
- package/src/lib/app-utils/wistia-embed.js +0 -138
- package/src/lib/app-utils/wistia-embed.js.map +0 -1
- package/src/lib/app-utils/workflows.d.ts +0 -136
- package/src/lib/app-utils/workflows.js +0 -346
- package/src/lib/app-utils/workflows.js.map +0 -1
- package/src/lib/plugin-manager/commerce-zone-props.d.ts +0 -172
- package/src/lib/plugin-manager/commerce-zone-props.js +0 -31
- package/src/lib/plugin-manager/commerce-zone-props.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/artifact-overrides.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n stableStringify,\n type ResolvedProvenance,\n} from './artifact-provenance'\n\n/**\n * The override layer (AGL-1019) — own the patch, never the copy.\n *\n * Customising an installed artifact and updating it are only compatible if\n * they are two different documents. Today they are one: you change a theme's\n * primary colour and that edits the theme, so the publisher's v2 can be taken\n * only by discarding your colour, or refused only by missing their fix.\n *\n * Here the publisher's version stays untouched as the **base** — AGL-1015\n * already stores it, content-addressed and immutable, in\n * `marketplaceArtifactBases/{sha256}` — the user's changes are a **sparse\n * patch** stored beside it, and readers resolve `base ⊕ patch`. Then:\n *\n * * updating replaces the base wholesale and the patch still applies;\n * * \"what did I change\" IS the patch, with no diffing at read time;\n * * \"reset to the publisher's version\" is deleting the patch;\n * * a conflict is narrow: only where the patch touches a path the update also\n * changed, which the marketplace plugin's three-way update plan already names.\n *\n * Pure and content-shaped: it knows nothing about Firestore, artifact types\n * or who installed the artifact, so one resolver serves a theme, a node tree\n * and a plugin settings map, and every rule below is testable without a\n * database. The platform's rather than the installer's (AGL-3080), because the\n * readers are: the site's theme resolution, the theme editor and its library.\n * The three-way update plan that writes a new base under a patch is the\n * installer's own (the marketplace's `model/artifact-merge.ts`).\n *\n * ## Which \"base\" a reader resolves against\n *\n * The installed document's OWN content fields — whatever the install route\n * wrote — are the base at read time, and {@link resolveArtifactContent} takes\n * them directly. `marketplaceArtifactBases` is not that: it is closed to\n * clients in both directions (a base can hold paid content, and its key is on\n * the installed document, so a readable collection would hand out any listing's\n * content by hash). It is the immutable snapshot the server diffs and resets\n * against, nothing more.\n *\n * The two agree because an update writes the publisher's new version onto the\n * document and stamps a base at that same hash in one operation — which is the\n * property that makes \"never mutate the vendored artifact\" enforceable rather\n * than merely intended. The moment a user edit lands in those content fields\n * instead of in the override, both the diff and this resolver are wrong.\n *\n * ## Two rules that are load-bearing\n *\n * **Deletion needs a sentinel, and it is not `null`.** A sparse patch says\n * nothing about paths it omits, so \"omit it to remove it\" is unavailable —\n * omission already means \"inherit\". `null` cannot serve either: it is a real\n * value in this codebase's content, and the besigner's `null`-means-cleared\n * convention is what took every `/product/*` page down in AGL-1226. So removal\n * is spelled with {@link OVERRIDE_DELETE}, a value nothing else produces.\n *\n * **Arrays are never merged by index.** An insert at position 0 shifts every\n * element after it, so index-wise merging reports — and then writes — a change\n * to every one of them. Arrays therefore replace wholesale by default, which is\n * also how {@link planArtifactUpdate} compares them. Where elements carry a\n * stable identity the patch may instead use the keyed form\n * ({@link keyedArrayPatch}) and edit one element by key, so a publisher who\n * inserts an element does not collide with a user who edited a different one.\n */\n\n/**\n * The removal sentinel: a patch value of exactly this string removes the path\n * from the resolved output.\n *\n * A string rather than a wrapper object because a patch travels through\n * Firestore, JSON responses and React state unchanged, and a scalar survives\n * all three without anyone needing to know it is special. The cost is that an\n * artifact whose real content is this exact string cannot be expressed; that is\n * a deliberate trade for a sentinel that cannot be produced by accident, and\n * {@link diffOverride} is the only thing that emits it.\n *\n * Note this is a field *value*, never a field *name* — Firestore rejects field\n * names matching `__.*__`, and this deliberately has no trailing underscores so\n * that a careless refactor into a key still fails loudly rather than silently.\n */\nexport const OVERRIDE_DELETE = '__aglynOverrideDelete'\n\n/**\n * The field on an installed artifact document that holds its override.\n *\n * Beside the base, never inside it. The document's content fields keep holding\n * the publisher's version verbatim, which is what makes an update a wholesale\n * replacement of those fields and nothing else. Writing the user's edits into\n * the content instead is precisely the state this issue exists to leave.\n */\nexport const ARTIFACT_OVERRIDE_FIELD = 'overrides'\n\n/** The stored override, as written under {@link ARTIFACT_OVERRIDE_FIELD}. */\nexport interface ArtifactOverride {\n /** The sparse patch. Resolved over the artifact's content at read time. */\n patch: unknown\n /**\n * The base the patch was authored against — `installedFrom.sha256`.\n *\n * Recorded, not enforced: a patch stays valid across an update by design, and\n * the only thing this answers is \"which publisher version was on screen when\n * these edits were made\", which is what a conflict report needs to be honest.\n */\n baseSha256: string | null\n updatedAt?: unknown\n updatedBy?: string | null\n}\n\n/**\n * A patch node that edits an array BY KEY instead of replacing it.\n *\n * Only ever produced for arrays whose elements are objects carrying a unique\n * value under one identity field, because that is the only case where \"the\n * third element\" and \"the element the publisher shipped third\" are different\n * questions with the same answer.\n */\nexport interface KeyedArrayPatch {\n /** Discriminator. Present only on this form. */\n keyedBy: string\n /** Per-key sub-patches; a value of {@link OVERRIDE_DELETE} removes the element. */\n entries: Record<string, unknown>\n /**\n * The full key order, when the user reordered the array.\n *\n * Absent when they did not — and absence is the point: an array with no\n * `order` lets a publisher insert an element and have it appear, while an\n * array with one pins the order the user chose. Keys present in the base but\n * missing from `order` keep their relative position at the end, so a stale\n * `order` never drops an element.\n */\n order?: string[]\n}\n\n/** Identity fields tried, in order, when deciding whether an array is keyed. */\nconst KEY_FIELDS = ['id', 'key', 'slug', 'name'] as const\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n value != null &&\n typeof value === 'object' &&\n !Array.isArray(value) &&\n (Object.getPrototypeOf(value) === Object.prototype ||\n Object.getPrototypeOf(value) === null)\n\nconst same = (a: unknown, b: unknown): boolean =>\n stableStringify(a) === stableStringify(b)\n\nexport const isDeleteSentinel = (value: unknown): boolean =>\n value === OVERRIDE_DELETE\n\nexport function isKeyedArrayPatch(value: unknown): value is KeyedArrayPatch {\n return (\n isPlainObject(value) &&\n typeof value['keyedBy'] === 'string' &&\n isPlainObject(value['entries'])\n )\n}\n\n/**\n * The identity field this array is keyed by, or null when it is not keyed.\n *\n * Every element must be a plain object carrying the same field, and every value\n * must be a non-empty string that appears once. A single duplicate or missing\n * key makes the whole array unkeyed rather than partly keyed — a patch that can\n * address only some elements is worse than one that addresses none, because the\n * gaps are invisible at the call site.\n */\nexport function arrayKeyField(value: unknown): string | null {\n if (!Array.isArray(value) || !value.length) return null\n if (!value.every(isPlainObject)) return null\n for (const field of KEY_FIELDS) {\n const keys = value.map((entry) => (entry as Record<string, unknown>)[field])\n if (!keys.every((key) => typeof key === 'string' && key !== '')) continue\n if (new Set(keys as string[]).size !== keys.length) continue\n return field\n }\n return null\n}\n\n/** Builds a keyed-array patch, dropping it entirely when it says nothing. */\nfunction keyedArrayPatch(\n keyedBy: string,\n entries: Record<string, unknown>,\n order: string[] | undefined,\n): KeyedArrayPatch | undefined {\n if (!Object.keys(entries).length && !order) return undefined\n return { keyedBy, entries, ...(order ? { order } : {}) }\n}\n\n/**\n * `base ⊕ patch` — the resolved view every reader sees.\n *\n * Deep-merges plain objects; everything else the patch names replaces what the\n * base holds. The base is never mutated: each level is copied on write, so a\n * caller can resolve the same base against two patches and get two answers.\n *\n * A patch that names a path the base does not have simply adds it. That is what\n * makes the same function serve \"override a publisher's field\" and \"set a field\n * the publisher never shipped\", and it is why a resolved artifact can be richer\n * than its base rather than only a recolouring of it.\n */\nexport function resolveOverride<T = unknown>(base: unknown, patch: unknown): T {\n return resolve(base, patch) as T\n}\n\nfunction resolve(base: unknown, patch: unknown): unknown {\n if (patch === undefined) return base\n if (isDeleteSentinel(patch)) return undefined\n\n if (isKeyedArrayPatch(patch)) return resolveKeyedArray(base, patch)\n\n if (isPlainObject(patch) && isPlainObject(base)) {\n const merged: Record<string, unknown> = { ...base }\n for (const [key, value] of Object.entries(patch)) {\n const resolved = resolve(merged[key], value)\n if (resolved === undefined) delete merged[key]\n else merged[key] = resolved\n }\n return merged\n }\n\n // A patch object over a non-object base still applies — the patch wins, and\n // its own delete sentinels are honoured so that `{a: DELETE}` over `null`\n // resolves to `{}` rather than smuggling the sentinel into the output.\n if (isPlainObject(patch)) return resolve({}, patch)\n\n return patch\n}\n\nfunction resolveKeyedArray(base: unknown, patch: KeyedArrayPatch): unknown[] {\n const source = Array.isArray(base) ? base : []\n const keyOf = (entry: unknown): string | undefined => {\n const key = isPlainObject(entry) ? entry[patch.keyedBy] : undefined\n return typeof key === 'string' && key !== '' ? key : undefined\n }\n\n const resolved: Array<{ key: string | undefined; value: unknown }> = []\n for (const entry of source) {\n const key = keyOf(entry)\n // An unkeyed element in an otherwise keyed array is unaddressable, so it is\n // carried through untouched rather than dropped: the patch never claimed to\n // describe it.\n if (key === undefined || !(key in patch.entries)) {\n resolved.push({ key, value: entry })\n continue\n }\n const value = resolve(entry, patch.entries[key])\n if (value !== undefined) resolved.push({ key, value })\n }\n\n // Entries the base does not hold are additions, appended in patch order.\n const present = new Set(resolved.map((entry) => entry.key))\n for (const [key, value] of Object.entries(patch.entries)) {\n if (present.has(key) || isDeleteSentinel(value)) continue\n const added = resolve(undefined, value)\n if (added !== undefined) resolved.push({ key, value: added })\n }\n\n if (!patch.order) return resolved.map((entry) => entry.value)\n\n // Ordered: the listed keys first, in the order given, then everything the\n // order did not mention in its existing relative position. A key in `order`\n // that no longer exists is skipped rather than inserted as a hole.\n const rank = new Map(patch.order.map((key, index) => [key, index]))\n return resolved\n .map((entry, index) => ({\n ...entry,\n rank: entry.key != null && rank.has(entry.key)\n ? (rank.get(entry.key) as number)\n : patch.order!.length + index,\n }))\n .sort((a, b) => a.rank - b.rank)\n .map((entry) => entry.value)\n}\n\n/**\n * \"What did I change\" — the sparse patch that turns `base` into `edited`.\n *\n * The inverse of {@link resolveOverride}, and held to it:\n * `resolveOverride(base, diffOverride(base, edited))` equals `edited` for every\n * shape this codebase stores. That round trip is the module's central property\n * and the thing its tests exist to defend, because every consumer downstream —\n * themes, theme overrides, host variables, plugin settings — inherits it.\n *\n * Returns `undefined` when nothing changed, so an unedited artifact stores no\n * override at all rather than an empty husk that reads as \"customised\".\n */\nexport function diffOverride(base: unknown, edited: unknown): unknown {\n if (same(base, edited)) return undefined\n if (edited === undefined) return OVERRIDE_DELETE\n\n if (isPlainObject(base) && isPlainObject(edited)) {\n const patch: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(edited)) {\n const child = diffOverride(base[key], value)\n if (child !== undefined) patch[key] = child\n }\n for (const key of Object.keys(base)) {\n if (!(key in edited)) patch[key] = OVERRIDE_DELETE\n }\n return patch\n }\n\n if (Array.isArray(base) && Array.isArray(edited)) {\n const keyed = diffKeyedArray(base, edited)\n if (keyed) return keyed\n }\n\n return edited\n}\n\n/**\n * A per-element patch for two keyed arrays, or null when they are not both\n * keyed by the same field — in which case the caller falls back to replacing\n * the array wholesale, which is always correct if sometimes coarse.\n */\nfunction diffKeyedArray(\n base: unknown[],\n edited: unknown[],\n): KeyedArrayPatch | undefined | null {\n const field = arrayKeyField(base)\n if (!field || arrayKeyField(edited) !== field) return null\n\n const keyOf = (entry: unknown) =>\n String((entry as Record<string, unknown>)[field])\n const baseByKey = new Map(base.map((entry) => [keyOf(entry), entry]))\n const editedKeys = edited.map(keyOf)\n\n const entries: Record<string, unknown> = {}\n for (const entry of edited) {\n const key = keyOf(entry)\n const child = diffOverride(baseByKey.get(key), entry)\n if (child !== undefined) entries[key] = child\n }\n for (const key of baseByKey.keys()) {\n if (!editedKeys.includes(key)) entries[key] = OVERRIDE_DELETE\n }\n\n // Order is stored only when the resolver would not already produce it —\n // surviving elements in the base's order, then additions in the order they\n // appear. So the common case (a value edit, or an element appended) stores no\n // order and leaves the publisher free to insert elements of their own, while\n // a genuine reorder or a mid-list insert is pinned.\n const natural = [\n ...base.map(keyOf).filter((key) => editedKeys.includes(key)),\n ...editedKeys.filter((key) => !baseByKey.has(key)),\n ]\n const reordered = !same(natural, editedKeys)\n\n return keyedArrayPatch(field, entries, reordered ? editedKeys : undefined)\n}\n\n/**\n * Every path the patch touches, dotted, for \"what did I change\" in a UI and for\n * pairing an override against an update plan's conflicts — whose paths\n * {@link planArtifactUpdate} spells the same way.\n *\n * A keyed array contributes `field[key]` segments rather than indices, because\n * an index is exactly the thing that stops meaning the same element the moment\n * the publisher inserts one.\n */\nexport function overridePaths(patch: unknown, prefix = ''): string[] {\n if (patch === undefined) return []\n if (isKeyedArrayPatch(patch)) {\n return Object.entries(patch.entries).flatMap(([key, value]) =>\n overridePaths(value, `${prefix}[${key}]`),\n )\n }\n if (isPlainObject(patch) && !isDeleteSentinel(patch)) {\n const keys = Object.keys(patch)\n if (keys.length) {\n return keys.flatMap((key) =>\n overridePaths(patch[key], prefix ? `${prefix}.${key}` : key),\n )\n }\n }\n return prefix ? [prefix] : []\n}\n\n/**\n * True when the patch says nothing — the artifact is the publisher's, as shipped.\n *\n * `undefined` is the only absence. A `null` is a VALUE the user chose, and\n * calling it empty here would silently discard \"set this field to null\" on\n * every save — the same conflation between \"cleared\" and \"absent\" that took\n * every `/product/*` page down in AGL-1226.\n */\nexport function isEmptyOverride(patch: unknown): boolean {\n if (patch === undefined) return true\n if (isKeyedArrayPatch(patch)) {\n return !patch.order && Object.values(patch.entries).every(isEmptyOverride)\n }\n if (isDeleteSentinel(patch)) return false\n if (isPlainObject(patch)) return Object.values(patch).every(isEmptyOverride)\n return false\n}\n\n/**\n * Is this shape storable as a whole-artifact override?\n *\n * A patch is a *sparse* description of edits to a structured artifact, so its\n * top level is an object or a keyed array and nothing else. A stored scalar —\n * above all a top-level `null` — would resolve the entire artifact away, which\n * is never something a user did on purpose and always something a half-written\n * or hand-edited field can produce. Nested nulls stay values; only the root is\n * constrained, and constraining it is what keeps a junk field from blanking a\n * live site.\n */\nfunction isStorablePatch(patch: unknown): boolean {\n return (\n (isKeyedArrayPatch(patch) || isPlainObject(patch)) && !isEmptyOverride(patch)\n )\n}\n\nexport interface OverrideEligibility {\n ok: boolean\n /** Why not, in the words the surface refusing it should use. */\n reason?: string\n}\n\n/**\n * Can this artifact hold an override at all? (AGL-1015's provenance, answered.)\n *\n * An override is a patch over the publisher's version, so an artifact with no\n * recorded base has nothing to be a patch *of*. Storing one anyway would\n * produce a patch against whatever the document happened to hold — including\n * the user's own earlier edits — and the first update would resolve it against\n * a base it was never written for. Saying so plainly is the whole point of the\n * `updatable` flag; this is the sentence to put next to the disabled control.\n */\nexport function canOverrideArtifact(\n provenance: ResolvedProvenance | null | undefined,\n): OverrideEligibility {\n if (!provenance || provenance.state === 'unknown') {\n return {\n ok: false,\n reason:\n 'This was not installed from the marketplace, so there is no ' +\n 'publisher’s version to keep your changes separate from. Edit it ' +\n 'directly — it is already yours.',\n }\n }\n if (!provenance.updatable) {\n return {\n ok: false,\n reason:\n 'This was installed before update tracking recorded an original, so ' +\n 'there is nothing to layer your changes over. Re-installing it from ' +\n 'the listing records one, and your changes can be kept separately ' +\n 'from then on.',\n }\n }\n return { ok: true }\n}\n\n/**\n * The value to write under {@link ARTIFACT_OVERRIDE_FIELD} — or `null`, meaning\n * \"reset to the publisher's version\", when the patch says nothing.\n *\n * ## Write this WHOLESALE. Never with `merge: true`.\n *\n * Firestore's `merge: true` deep-merges maps, and a patch is a map, so merging\n * a new patch onto an old one takes their union: every path the user has ever\n * overridden stays overridden, and removing an override becomes impossible\n * through the one write that looks like it should do it. `merge: true` on the\n * *document* is fine and expected — it is this *field* that must be replaced,\n * which is what passing this value under a `FieldPath` to `update`, or under\n * `set(..., {mergeFields})`, does:\n *\n * ```ts\n * await ref.set(\n * { [ARTIFACT_OVERRIDE_FIELD]: overrideWriteValue(patch, sha256), updatedAt },\n * { mergeFields: [ARTIFACT_OVERRIDE_FIELD, 'updatedAt'] },\n * )\n * ```\n *\n * The `null` for an empty patch is deliberate over a field delete: a reader\n * that sees `null` knows the override was considered and is empty, which reads\n * the same as absent and needs no second code path.\n */\nexport function overrideWriteValue(\n patch: unknown,\n baseSha256: string | null,\n meta?: { updatedAt?: unknown; updatedBy?: string | null },\n): ArtifactOverride | null {\n if (!isStorablePatch(patch)) return null\n return {\n patch,\n baseSha256,\n ...(meta?.updatedAt !== undefined ? { updatedAt: meta.updatedAt } : {}),\n ...(meta?.updatedBy !== undefined ? { updatedBy: meta.updatedBy } : {}),\n }\n}\n\n/**\n * Reads a stored override off a document, tolerating the junk a client-writable\n * field can hold.\n *\n * Returns `undefined` for anything that is not a well-formed override, so a\n * corrupt field resolves to the publisher's version rather than throwing on a\n * page that would otherwise render.\n */\nexport function readArtifactOverride(\n doc: Record<string, unknown> | null | undefined,\n): ArtifactOverride | undefined {\n const stored = doc?.[ARTIFACT_OVERRIDE_FIELD]\n if (!isPlainObject(stored)) return undefined\n if (!isStorablePatch(stored['patch'])) return undefined\n const sha = stored['baseSha256']\n return {\n patch: stored['patch'],\n baseSha256: typeof sha === 'string' && sha ? sha : null,\n ...(stored['updatedAt'] !== undefined ? { updatedAt: stored['updatedAt'] } : {}),\n ...(typeof stored['updatedBy'] === 'string'\n ? { updatedBy: stored['updatedBy'] }\n : {}),\n }\n}\n\n/**\n * The one call a reader makes: the publisher's content with this document's\n * override resolved over it.\n *\n * `content` is the artifact's own fields — whatever shape the install route\n * wrote, unchanged — so a caller never has to know whether an override exists.\n */\nexport function resolveArtifactContent<T = unknown>(\n content: unknown,\n doc: Record<string, unknown> | null | undefined,\n): T {\n const override = readArtifactOverride(doc)\n return (override ? resolveOverride(content, override.patch) : content) as T\n}\n\n/**\n * The paths where an incoming update collides with what the user overrode.\n *\n * Narrow by construction, which is the payoff the whole layer was built for: a\n * patch touching four paths can conflict in at most four places no matter how\n * much of the artifact the publisher rewrote, so the question put to the user is\n * \"the publisher also changed your heading colour — whose wins?\" rather than\n * \"this update rewrites 300 nodes, continue?\".\n *\n * A path whose incoming value already equals what the override sets is not a\n * conflict — the publisher adopted the user's change, and there is nothing to\n * ask about.\n */\nexport function overrideConflicts(\n base: unknown,\n patch: unknown,\n incoming: unknown,\n): string[] {\n const overridden = resolveOverride(base, patch)\n return overridePaths(patch).filter((path) => {\n const baseValue = readPath(base, path)\n const incomingValue = readPath(incoming, path)\n if (same(baseValue, incomingValue)) return false\n return !same(readPath(overridden, path), incomingValue)\n })\n}\n\n/** Reads a dotted path, understanding the `field[key]` segments keyed arrays use. */\nfunction readPath(value: unknown, path: string): unknown {\n let cursor: unknown = value\n for (const segment of path.match(/[^.[\\]]+/g) ?? []) {\n if (Array.isArray(cursor)) {\n const field = arrayKeyField(cursor)\n cursor = field\n ? cursor.find(\n (entry) => (entry as Record<string, unknown>)[field] === segment,\n )\n : undefined\n continue\n }\n if (!isPlainObject(cursor)) return undefined\n cursor = cursor[segment]\n }\n return cursor\n}\n"],"names":["stableStringify","OVERRIDE_DELETE","ARTIFACT_OVERRIDE_FIELD","KEY_FIELDS","isPlainObject","value","Array","isArray","Object","getPrototypeOf","prototype","same","a","b","isDeleteSentinel","isKeyedArrayPatch","arrayKeyField","length","every","field","keys","map","entry","key","Set","size","keyedArrayPatch","keyedBy","entries","order","undefined","resolveOverride","base","patch","resolve","resolveKeyedArray","merged","resolved","source","keyOf","push","present","has","added","rank","Map","index","get","sort","diffOverride","edited","child","keyed","diffKeyedArray","String","baseByKey","editedKeys","includes","natural","filter","reordered","overridePaths","prefix","flatMap","isEmptyOverride","values","isStorablePatch","canOverrideArtifact","provenance","state","ok","reason","updatable","overrideWriteValue","baseSha256","meta","updatedAt","updatedBy","readArtifactOverride","doc","stored","sha","resolveArtifactContent","content","override","overrideConflicts","incoming","overridden","path","baseValue","readPath","incomingValue","cursor","segment","match","find"],"mappings":"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"}
|
|
@@ -15,15 +15,17 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
18
|
+
* The kinds of content a workspace can INSTALL rather than author (AGL-654).
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* Installed content is a platform concept with more readers than writers:
|
|
21
|
+
* whatever installs a component, a theme or an email template stamps where it
|
|
22
|
+
* came from (below), and the theme editor, a template's page, the site's theme
|
|
23
|
+
* resolution and the AI plugin all read that stamp without knowing who wrote
|
|
24
|
+
* it. So the contract is core's and names no installer (AGL-3080). The
|
|
25
|
+
* marketplace, which is today's only installer, re-exports the union under the
|
|
26
|
+
* name its publish routes use, `MarketplaceArtifactType`.
|
|
25
27
|
*/
|
|
26
|
-
export type
|
|
28
|
+
export type InstallableArtifactType = 'component' | 'template' | 'plugin' | 'layout' | 'datasetSchema' | 'emailTemplate'
|
|
27
29
|
/**
|
|
28
30
|
* A campaign email design, installed as a new `kind: 'email'` screen.
|
|
29
31
|
*
|
|
@@ -67,7 +69,7 @@ export interface InstalledFrom {
|
|
|
67
69
|
version: string | null;
|
|
68
70
|
/** Content hash of the base snapshot — the key into {@link ARTIFACT_BASE_COLLECTION}. */
|
|
69
71
|
sha256: string | null;
|
|
70
|
-
artifactType:
|
|
72
|
+
artifactType: InstallableArtifactType;
|
|
71
73
|
installedAt: unknown;
|
|
72
74
|
/** The publishing org (`listing.profileId`, org-owned since AGL-652). */
|
|
73
75
|
publisherOrgId: string | null;
|
|
@@ -100,7 +102,7 @@ export interface ResolvedProvenance {
|
|
|
100
102
|
listingId: string | null;
|
|
101
103
|
version: string | null;
|
|
102
104
|
sha256: string | null;
|
|
103
|
-
artifactType:
|
|
105
|
+
artifactType: InstallableArtifactType | null;
|
|
104
106
|
publisherOrgId: string | null;
|
|
105
107
|
/**
|
|
106
108
|
* A base snapshot exists, so this artifact can be diffed and safely updated.
|
|
@@ -138,7 +140,7 @@ export declare function resolveProvenance(doc: {
|
|
|
138
140
|
profileId?: string;
|
|
139
141
|
version?: unknown;
|
|
140
142
|
sha256?: string;
|
|
141
|
-
} | null | undefined, fallbackArtifactType?:
|
|
143
|
+
} | null | undefined, fallbackArtifactType?: InstallableArtifactType): ResolvedProvenance;
|
|
142
144
|
/**
|
|
143
145
|
* Deterministic JSON with object keys in sorted order, so the same content
|
|
144
146
|
* hashes to the same id no matter what order Firestore handed the fields back.
|
|
@@ -14,13 +14,15 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ /**
|
|
17
|
-
*
|
|
17
|
+
* The kinds of content a workspace can INSTALL rather than author (AGL-654).
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
19
|
+
* Installed content is a platform concept with more readers than writers:
|
|
20
|
+
* whatever installs a component, a theme or an email template stamps where it
|
|
21
|
+
* came from (below), and the theme editor, a template's page, the site's theme
|
|
22
|
+
* resolution and the AI plugin all read that stamp without knowing who wrote
|
|
23
|
+
* it. So the contract is core's and names no installer (AGL-3080). The
|
|
24
|
+
* marketplace, which is today's only installer, re-exports the union under the
|
|
25
|
+
* name its publish routes use, `MarketplaceArtifactType`.
|
|
24
26
|
*/ /**
|
|
25
27
|
* Install provenance (AGL-1015): where an installed artifact came from, at
|
|
26
28
|
* what version, and what it looked like when it arrived.
|
|
@@ -133,4 +135,4 @@ function asVersion(value) {
|
|
|
133
135
|
return `{${entries.join(',')}}`;
|
|
134
136
|
}
|
|
135
137
|
|
|
136
|
-
//# sourceMappingURL=
|
|
138
|
+
//# sourceMappingURL=artifact-provenance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/artifact-provenance.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The kinds of content a workspace can INSTALL rather than author (AGL-654).\n *\n * Installed content is a platform concept with more readers than writers:\n * whatever installs a component, a theme or an email template stamps where it\n * came from (below), and the theme editor, a template's page, the site's theme\n * resolution and the AI plugin all read that stamp without knowing who wrote\n * it. So the contract is core's and names no installer (AGL-3080). The\n * marketplace, which is today's only installer, re-exports the union under the\n * name its publish routes use, `MarketplaceArtifactType`.\n */\nexport type InstallableArtifactType =\n | 'component'\n | 'template'\n | 'plugin'\n | 'layout'\n | 'datasetSchema'\n | 'emailTemplate'\n /**\n * A campaign email design, installed as a new `kind: 'email'` screen.\n *\n * Distinct from `emailTemplate`, which is a design FOR one entry in the\n * fixed transactional catalog and can only ever land on that same key. A\n * starter belongs to no key: it is a blank-page replacement for the composer,\n * so it installs as a new document and the tenant may install ten of them.\n * Folding the two together would give one install button two behaviors and\n * would make the catalog key an absent-field discriminator.\n */\n | 'emailStarter'\n | 'theme'\n\n/**\n * Install provenance (AGL-1015): where an installed artifact came from, at\n * what version, and what it looked like when it arrived.\n *\n * Every install route used to stamp its own shape — plugins wrote a pin,\n * dataset schemas a `source`, email templates an `installedFrom`, components a\n * `marketplace` — and none of them kept the content as installed. Without the\n * original there is no way to tell the publisher's change from the user's, so\n * an update can only overwrite. This module defines the one shape all install\n * routes write and the one reader everything else asks.\n */\n\n/**\n * Immutable, content-addressed base snapshots — the content exactly as\n * installed, keyed by its own hash and stored outside the user's edit path.\n *\n * Top-level rather than per-host on purpose: the same listing version installed\n * onto twenty sites is one document, and re-installing it rewrites nothing.\n */\nexport const ARTIFACT_BASE_COLLECTION = 'marketplaceArtifactBases'\n\n/**\n * Firestore's per-document ceiling is 1 MiB. Snapshots are stored whole, so\n * anything near it is refused rather than truncated — a partial base is worse\n * than no base, because a diff would report the missing half as user edits.\n */\nexport const ARTIFACT_BASE_MAX_BYTES = 800 * 1024\n\n/** The provenance stamp written on every installed artifact (AGL-1015). */\nexport interface InstalledFrom {\n listingId: string\n /** The listing version installed, as a string; null when unversioned. */\n version: string | null\n /** Content hash of the base snapshot — the key into {@link ARTIFACT_BASE_COLLECTION}. */\n sha256: string | null\n artifactType: InstallableArtifactType\n installedAt: unknown\n /** The publishing org (`listing.profileId`, org-owned since AGL-652). */\n publisherOrgId: string | null\n /**\n * What review said about THE VERSION INSTALLED, recorded at install time.\n *\n * Optional because six of the seven artifact types are auto-listed and have\n * no verdict to record. Where it is written it is a fact about one version\n * and is never recomputed from the listing afterwards: a listing that earns\n * `reviewStatus: 'verified'` next month did not retroactively review the\n * bytes installed today, and a surface that resolved assurance live would\n * say it did. Stamping it is what keeps \"unreviewed\" from decaying into a\n * silence a reader mistakes for approval.\n */\n assurance?: 'approved' | 'unreviewed' | 'rejected'\n}\n\n/**\n * How much is actually known about where an artifact came from.\n *\n * `inferred` exists because the marketplace shipped before this stamp did:\n * pre-AGL-1015 installs carry a listing id somewhere but no hash and no base.\n * Calling those `recorded` would let an update think it has an origin to merge\n * against; calling them `unknown` would throw away a listing id we really do\n * have. They are their own state, and the UI says \"installed before update\n * tracking\" rather than implying either.\n */\nexport type ProvenanceState = 'recorded' | 'inferred' | 'unknown'\n\nexport interface ResolvedProvenance {\n state: ProvenanceState\n listingId: string | null\n version: string | null\n sha256: string | null\n artifactType: InstallableArtifactType | null\n publisherOrgId: string | null\n /**\n * A base snapshot exists, so this artifact can be diffed and safely updated.\n * False for everything installed before the stamp — and saying so plainly is\n * the point: a merge built on a fabricated origin destroys work silently.\n */\n updatable: boolean\n}\n\nconst UNKNOWN_PROVENANCE: ResolvedProvenance = {\n state: 'unknown',\n listingId: null,\n version: null,\n sha256: null,\n artifactType: null,\n publisherOrgId: null,\n updatable: false,\n}\n\nfunction asVersion(value: unknown): string | null {\n return value == null || value === '' ? null : String(value)\n}\n\n/**\n * Reads provenance off an installed document, tolerating every shape the\n * install routes wrote before AGL-1015.\n *\n * The legacy fields are read rather than migrated: a batch backfill could only\n * ever recover the listing id and version, which is exactly what this recovers\n * lazily, and it cannot invent a base snapshot. Installs re-stamp themselves\n * properly the next time they are updated.\n */\nexport function resolveProvenance(\n doc:\n | {\n installedFrom?: Partial<InstalledFrom> | null\n /** Component installs (`hosts/{h}/components`). */\n marketplace?: { listingId?: string; profileId?: string; version?: unknown } | null\n /** Template, layout and dataset-schema installs. */\n source?: { type?: string; listingId?: string; version?: unknown } | null\n /** Plugin pins are their own provenance: the pin IS the version. */\n pluginId?: string\n listingId?: string\n profileId?: string\n version?: unknown\n sha256?: string\n }\n | null\n | undefined,\n fallbackArtifactType?: InstallableArtifactType,\n): ResolvedProvenance {\n if (!doc) return UNKNOWN_PROVENANCE\n\n const stamped = doc.installedFrom\n // A full stamp is the only thing that carries a base snapshot. Email\n // templates have written a partial `installedFrom` since AGL-789, so the\n // discriminator is the hash and the artifact type, not the field's presence.\n if (stamped?.listingId && stamped.sha256 && stamped.artifactType) {\n return {\n state: 'recorded',\n listingId: stamped.listingId,\n version: asVersion(stamped.version),\n sha256: stamped.sha256,\n artifactType: stamped.artifactType as InstallableArtifactType,\n publisherOrgId: stamped.publisherOrgId ?? null,\n updatable: true,\n }\n }\n\n const legacy = stamped?.listingId\n ? { listingId: stamped.listingId, version: stamped.version, profileId: null }\n : doc.marketplace?.listingId\n ? {\n listingId: doc.marketplace.listingId,\n version: doc.marketplace.version,\n profileId: doc.marketplace.profileId ?? null,\n }\n : doc.source?.listingId\n ? {\n listingId: doc.source.listingId,\n version: doc.source.version,\n profileId: null,\n }\n : // A plugin pin is flat: `{listingId, version, sha256}` on the install\n // doc itself. It is the one artifact that never needed a snapshot —\n // the bytes are immutable and content-addressed already — so a pin\n // resolves as fully recorded.\n doc.listingId\n ? {\n listingId: doc.listingId,\n version: doc.version,\n profileId: doc.profileId ?? null,\n }\n : null\n if (!legacy) return UNKNOWN_PROVENANCE\n\n const isPin = Boolean(doc.pluginId && doc.sha256)\n return {\n state: isPin ? 'recorded' : 'inferred',\n listingId: legacy.listingId,\n version: asVersion(legacy.version),\n sha256: doc.sha256 ?? null,\n artifactType: isPin ? 'plugin' : (fallbackArtifactType ?? null),\n publisherOrgId: legacy.profileId,\n updatable: isPin,\n }\n}\n\n/**\n * Deterministic JSON with object keys in sorted order, so the same content\n * hashes to the same id no matter what order Firestore handed the fields back.\n *\n * `undefined` members are dropped exactly as `JSON.stringify` drops them, which\n * keeps `{a: 1}` and `{a: 1, b: undefined}` — the same document after a\n * Firestore round-trip — hashing alike.\n */\nexport function stableStringify(value: unknown): string {\n if (value === null || typeof value !== 'object') {\n return JSON.stringify(value ?? null)\n }\n if (Array.isArray(value)) {\n return `[${value.map((entry) => stableStringify(entry)).join(',')}]`\n }\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, entryValue]) => entryValue !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`)\n return `{${entries.join(',')}}`\n}\n"],"names":["ARTIFACT_BASE_COLLECTION","ARTIFACT_BASE_MAX_BYTES","UNKNOWN_PROVENANCE","state","listingId","version","sha256","artifactType","publisherOrgId","updatable","asVersion","value","String","resolveProvenance","doc","fallbackArtifactType","stamped","installedFrom","legacy","profileId","marketplace","source","isPin","Boolean","pluginId","stableStringify","JSON","stringify","Array","isArray","map","entry","join","entries","Object","filter","entryValue","undefined","sort","a","b","key"],"mappings":"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"}
|
|
@@ -15,6 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { isUncappedPlanComp, resolveOrgEntitlements, resolvePlanPricing } from "./plan-entitlements.js";
|
|
18
|
+
import { planQuotaOf } from "../plugin-manager/plugin-plan-entitlements.js";
|
|
18
19
|
/**
|
|
19
20
|
* Aglyn Assist credits: the unit assist is sold, metered and refused in.
|
|
20
21
|
*
|
|
@@ -193,7 +194,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
193
194
|
* reservation's ceiling (`assistMonthlyCeilingUsd` in the AI plugin) drops
|
|
194
195
|
* the default and keeps only an operator's explicit figure.
|
|
195
196
|
*/ export function resolveAssistCreditBudget(org) {
|
|
196
|
-
const credits =
|
|
197
|
+
const credits = planQuotaOf(resolveOrgEntitlements(org), 'assistCreditsPerMonth');
|
|
197
198
|
if (!Number.isFinite(credits) || credits <= 0) return null;
|
|
198
199
|
return Math.floor(credits);
|
|
199
200
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/assist-credits.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n isUncappedPlanComp,\n resolveOrgEntitlements,\n resolvePlanPricing,\n} from './plan-entitlements'\nimport type { AglynOrgBilling } from '../foundation/definitions/org-billing.types'\n\n/**\n * Aglyn Assist credits: the unit assist is sold, metered and refused in.\n *\n * ## Why cost, and not messages\n *\n * Assist is measured per exchange as `estCostUsd` — the exchange priced at\n * the serving model's billed rates, computed where the tokens were counted. A\n * message allowance would throw that measurement away and price every action\n * the same. It cannot: a question is a few thousand tokens, while generating\n * a screen carries the node tree, the component catalog and the theme tokens\n * in, emits structured markup out, and iterates. The two differ by up to two\n * orders of magnitude, so one workspace's ten screen builds can outspend\n * another's thousand questions while both read as \"within allowance\".\n *\n * Metering in cost is the only unit under which those two workspaces draw\n * down their bands differently, which is the entire point.\n *\n * ## Why a credit rather than the dollar figure\n *\n * `estCostUsd` is a dollar figure off our own cost model, not a price.\n * Publishing it would put our model choice, our per-model rates and our\n * margin on a billing page, and it would move under customers every time a\n * model is swapped. A credit is a fixed quantity of that spend with a stable\n * public meaning, so the band on the plan card stays the same number when\n * the model behind it changes.\n *\n * Everything customer-facing counts credits. This module is the ONLY place\n * the two units meet.\n *\n * ## `estCostUsd` is the BILLED figure, not the bill we pay (AGL-3015)\n *\n * It is every exchange priced at the catalog's billed rates, which a model\n * may carry above what its provider charges. Credits are drawn from it, so\n * it is the right figure for a band, a wall, a cap and an invoice line. It\n * is the WRONG figure for a margin or a cost meter, which read the rollup's\n * provider figure through `assistProviderCostUsd` instead.\n */\n\n/**\n * What one credit is BILLED at, in USD.\n *\n * A tenth of a cent, chosen for two properties rather than for roundness:\n *\n * - **1,000 credits cost exactly $1.00**, so the per-1,000 retail rates on\n * `PLAN_PRICING.extraAssistCreditsUsdPer1k` read directly as multiples of\n * cost. A rate of $2.00 per 1,000 is cost x2 with no arithmetic in between,\n * which is what makes the 50% margin floor checkable by eye.\n * - **A single question is tens of credits, a generated screen is hundreds.**\n * At Sonnet list rates a short grounded answer runs around $0.013, so ~13\n * credits; a screen build runs into the hundreds. Both are whole numbers\n * with room to differ, which a cent-sized credit would have flattened to\n * \"1 vs 20\" and a dollar-sized one to \"0 vs 0\".\n *\n * ⚠️ This is a COST-MODEL constant, not a price and not a rate we publish.\n * It never appears on a customer surface; `assistCreditsFromUsd` is how a\n * measured dollar figure becomes something a customer may be shown.\n *\n * ⚠️ AND IT IS NOT WHAT A CREDIT COSTS US (AGL-3015). The dollar figure this\n * divides is priced at the model catalog's BILLED rates, and a model may be\n * billed above what the provider charges. A credit is therefore a tenth of a\n * cent of BILLED spend, and at most a tenth of a cent of provider spend. Read\n * against a retail rate it yields the floor of the line margin, never the\n * middle of it — which is the safe direction, and the direction every margin\n * assertion in this repo depends on. What a period actually cost is measured,\n * not derived: `assistProviderCostUsd` reads it off the rollup.\n */\nexport const ASSIST_CREDIT_COST_USD = 0.001\n\n/**\n * The field a monthly assist rollup, a signal row and a kind bucket keep\n * PROVIDER spend under, beside the `estCostUsd` a customer's credits are\n * drawn from (AGL-3015).\n *\n * Named here rather than in the meter that writes it because the readers are\n * spread across the plugin, the billing routes and the staff surfaces, and a\n * field name spelled out at six call sites is a field name that will be\n * spelled differently at the seventh.\n */\nexport const ASSIST_PROVIDER_COST_FIELD = 'providerCostUsd'\n\n/**\n * What a metered period ACTUALLY COST US, from the two dollar figures a\n * rollup carries.\n *\n * `billedUsd` is `estCostUsd`: every exchange priced at its model's billed\n * rates, and the figure credits are drawn from. `providerUsd` is\n * `ASSIST_PROVIDER_COST_FIELD`: the same exchanges priced at what the\n * provider charged. The second is the answer wherever it exists.\n *\n * ## Why a rollup may not carry one, and which way that errs\n *\n * The provider figure is written from the commit that split the two rates\n * onward. A period closed before it answers with the BILLED figure, which is\n * at or above what we paid — so a margin taken on history reads low and a\n * cost reads high, the direction a cost figure is allowed to be wrong in.\n * The one period that under-reads is the month in flight when the split\n * shipped, whose provider figure covers only the exchanges after it; that\n * month's error is bounded by the markup on a few days of a meter whose\n * largest measured organization is a fraction of a cent.\n */\nexport function assistProviderCostUsd(\n billedUsd: unknown,\n providerUsd: unknown,\n): number {\n const provider = Number(providerUsd)\n if (Number.isFinite(provider) && provider > 0) return provider\n const billed = Number(billedUsd)\n return Number.isFinite(billed) && billed > 0 ? billed : 0\n}\n\n/**\n * The minimum line margin every retail assist rate must clear.\n *\n * The same floor now applied to every retail rate in this codebase, and the\n * reason the assist ladder stops descending at cost x2 on Agency instead of\n * taking one more step the way the contacts ladder once did.\n */\nexport const ASSIST_CREDIT_MIN_MARGIN_PCT = 0.5\n\n/**\n * Credits -> our cost in USD. Non-finite or negative input is 0 credits, so a\n * corrupt band cannot become a budget of `NaN` that every comparison passes.\n */\nexport function assistUsdFromCredits(credits: number): number {\n const value = Number(credits)\n if (!Number.isFinite(value) || value <= 0) return 0\n return Math.round(value * ASSIST_CREDIT_COST_USD * 1_000_000) / 1_000_000\n}\n\n/**\n * Measured USD -> credits, rounded UP.\n *\n * Up, because this converts spend the platform has ALREADY made into the\n * figure a customer is shown against their band. Rounding down would let a\n * long tail of sub-credit exchanges cost real money and draw nothing, which\n * is the one direction a meter must not err in.\n */\nexport function assistCreditsFromUsd(usd: number): number {\n const value = Number(usd)\n if (!Number.isFinite(value) || value <= 0) return 0\n return Math.ceil(value / ASSIST_CREDIT_COST_USD)\n}\n\n/**\n * The org's monthly assist band in credits, or `null` when its plan sells no\n * assist band at all.\n *\n * ## `null` is not zero, and the difference is the whole safety property\n *\n * A band of 0 resolves to `null` — \"this org sells no band\" — and never to\n * a budget of `$0`. An org with no band still reaches the console\n * assistant's docs-grounded rung, which is bounded by the free daily message\n * cap and by the operator's spend backstop. Resolving zero as a budget would\n * refuse that rung outright: a whole assistant turned off by a pricing field\n * that was never about it.\n *\n * Since AGL-3203 no PLAN ROW bands at zero. Free carries the AGL-2925 taste\n * of `FREE_AI_TASTE_CREDITS_PER_MONTH` with no rate beside it, so it\n * resolves to a budget here and `assistBandRefuses` makes that budget a\n * wall; Starter carries 750 WITH a rate, so its budget is a line it meters\n * past rather than a wall. Both draw their docs-grounded chat on the same\n * band, which is fine — each is sized so a month of it costs well under a\n * dollar.\n *\n * So the zero case is now reached only by a per-org override that writes\n * one, and the reading above is what keeps that org's assistant running.\n *\n * The Aglyn AI add-on (AGL-2896) needs no knowledge here either way: the\n * resolver has already added `AI_ADDON_CREDITS_PER_MONTH[plan]` to the\n * band, because the add-on widens the one pool rather than opening a\n * second.\n *\n * The same reading is what makes the guard survive a stubbed entitlements\n * module. A test double that answers 0 for every quota produces `null` here,\n * which falls through to the operator backstop, rather than a budget of zero\n * that refuses everything while the assertions read green. A clamp that goes\n * green having refused every request has proved nothing, and 0 is exactly the\n * value a stub returns.\n *\n * ## Non-finite is also `null`, deliberately\n *\n * `Infinity` must never resolve to a budget. It is unrepresentable off-process\n * — `JSON.stringify(Infinity)` is `null` and reads back as 0 — so a band that\n * reached here as `Infinity` is a band that would read as zero somewhere\n * else. Falling through to the backstop is bounded and consistent; honouring\n * it would be an unbounded budget on one process and a refusal on the next.\n *\n * An UNCAPPED staff comp (AGL-3049) is where that `Infinity` comes from on\n * purpose, and `null` is right for it here too: there is no band to measure\n * against, be over, price or alert on. What `null` must NOT bring with it is\n * the backstop, whose repo default would cap the one workspace staff said\n * has no cap — so `assistBandRefuses` answers false for it, and the\n * reservation's ceiling (`assistMonthlyCeilingUsd` in the AI plugin) drops\n * the default and keeps only an operator's explicit figure.\n */\nexport function resolveAssistCreditBudget(\n org: Partial<AglynOrgBilling> | null | undefined,\n): number | null {\n const credits = Number(resolveOrgEntitlements(org).assistCreditsPerMonth)\n if (!Number.isFinite(credits) || credits <= 0) return null\n return Math.floor(credits)\n}\n\n/** The org's monthly assist band in USD of provider spend, or `null`. */\nexport function resolveAssistBudgetUsd(\n org: Partial<AglynOrgBilling> | null | undefined,\n): number | null {\n const credits = resolveAssistCreditBudget(org)\n return credits === null ? null : assistUsdFromCredits(credits)\n}\n\n/**\n * Credits spent past the band. A `null` band yields 0 — an org with no band\n * has nothing to be over, and reporting the whole month's spend as overage\n * there would invoice the free tier for its docs answers.\n */\nexport function assistCreditOverage(\n usedCredits: number,\n bandCredits: number | null,\n): number {\n const used = Number(usedCredits)\n if (!Number.isFinite(used) || used <= 0) return 0\n if (bandCredits === null) return 0\n const band = Number(bandCredits)\n if (!Number.isFinite(band) || band <= 0) return 0\n return Math.max(0, used - band)\n}\n\n/**\n * The per-1,000 rate the org's assist credits are sold at past its band, or\n * `null` when nothing is sold past it — the ONE reading of the rate\n * (AGL-2896). `priceAssistCreditOverage`, `assistBandRefuses`,\n * `assistRefusedByHardCap` and `assistHardCapRefusalText` all go through\n * it, so the gate, the invoice and the refusal sentence cannot quote three\n * different rates for one org.\n *\n * The plan's `extraAssistCreditsUsdPer1k` IS the answer — the whole answer,\n * since AGL-3203. Starter used to be the one exception: it banded at 0, so\n * the rate could not sit on its plan row without advertising a fee on a\n * quantity the plan never sold, and the add-on's band was priced from a\n * constant read here instead. Starter now includes 750 credits\n * unconditionally and carries $3.00 on its own row, so the exception and its\n * constant are gone and there is exactly one place the rate is written.\n *\n * Free and Enterprise are null with or without the add-on: Free sells no\n * add-on and its taste band is a wall by decision (AGL-2925), and\n * Enterprise's usage is in the contract.\n *\n * A STAFF COMP is null on every plan and with every add-on (AGL-3034),\n * because `resolvePlanPricing` — never `PLAN_PRICING` indexed directly —\n * sells nothing on a comp. That is what makes a comp's band a wall at the\n * gate and prices its overage to zero at the charge: the AI overage path\n * (AGL-3011) claims only what `assistMonthOverage` prices, so a comp never\n * becomes a Stripe invoice. The function is a single read for that reason;\n * a second source for the rate is how a comp gets quoted one.\n */\nexport function resolveAssistOverageRateUsdPer1k(\n org: Partial<AglynOrgBilling> | null | undefined,\n): number | null {\n return resolvePlanPricing(org).extraAssistCreditsUsdPer1k\n}\n\nexport interface AssistCreditOveragePrice {\n /** Credits past the plan's band, as handed in. */\n overageCredits: number\n /** Estimated charge at the plan's per-1,000 rate. */\n overageMonthlyUsd: number\n /** Per-1,000 rate; null when the plan prices no assist overage. */\n overageRateUsd: number | null\n}\n\n/**\n * Prices assist credits past a plan's band.\n *\n * ## No `allowed`, deliberately — and for the opposite reason to email\n *\n * `priceEmailSendOverage` omits an `allowed` field because email has two\n * gates and a transactional sender consulting the wrong one drops a password\n * reset. Assist omits it because it has exactly ONE gate and this is not it:\n * whether the band refuses is `assistBandRefuses`, applied inside\n * `reserveAssistMessage` before a token is spent. A second answer to \"may\n * this proceed\", computed after the fact from a rounded credit figure, would\n * be a second gate to drift from the first.\n *\n * ## Takes the overage, does not recompute it\n *\n * `assistCreditOverage` derives the excess once, from the measured meter and\n * the resolved band. Re-deriving it here would be a second overage model.\n *\n * A null rate yields zero structurally rather than by a check, which is what\n * keeps Free and Starter at zero on this axis.\n */\nexport function priceAssistCreditOverage(\n org: Partial<AglynOrgBilling> | null | undefined,\n overageCredits: number,\n): AssistCreditOveragePrice {\n const overageRateUsd = resolveAssistOverageRateUsdPer1k(org)\n const credits = Number(overageCredits)\n const over = Number.isFinite(credits) && credits > 0 ? credits : 0\n return {\n overageCredits: over,\n overageMonthlyUsd:\n overageRateUsd === null\n ? 0\n : Math.round((over / 1000) * overageRateUsd * 100) / 100,\n overageRateUsd,\n }\n}\n\n/**\n * The line margin a per-1,000-credit retail rate earns AT LEAST, as a\n * fraction.\n *\n * One expression, so the ladder and its floor cannot be checked against two\n * different definitions of margin. A null or non-positive rate has no margin\n * to report and answers `null` rather than 1 or 0 — \"this plan sells no\n * overage\" is not \"this plan sells overage at 100% margin\".\n *\n * A LOWER BOUND, not the realized figure (AGL-3015). The cost side is\n * `ASSIST_CREDIT_COST_USD`, which is what a credit BILLS at; a model billed\n * above its provider rate costs us less than that per credit, so the real\n * line margin is this or better and never worse. The floor stays checkable\n * by eye — a $2.00 rate is still cost x2 — and a retail rate that clears the\n * floor here clears it on every model mix. What a period realized is the\n * staff margin card's business, and it reads measured provider spend.\n */\nexport function assistCreditRateMarginPct(\n rateUsdPer1k: number | null,\n): number | null {\n if (rateUsdPer1k === null) return null\n const rate = Number(rateUsdPer1k)\n if (!Number.isFinite(rate) || rate <= 0) return null\n const costPer1k = ASSIST_CREDIT_COST_USD * 1000\n return (rate - costPer1k) / rate\n}\n\n/**\n * What a refused or in-progress workspace may be TOLD about its assist band.\n *\n * The reservation carries `costUsd` and `costLimitUsd` because that is what\n * it measured and what it compared against, and both are our provider bill.\n * Shipping the reservation itself to a browser publishes them. This is the\n * projection that crosses that boundary: credits only, no dollars, no model,\n * no rates.\n */\nexport interface PublicAssistCredits {\n /** Credits drawn this month, rounded up. */\n used: number\n /** The band, or `null` when no band applied to this request. */\n limit: number | null\n /** Credits left in the band, or `null` when there is no band. */\n remaining: number | null\n}\n\n/**\n * Projects a measured spend figure and a ceiling into the credit view.\n *\n * Both inputs are nullable because the reservation's are: `costUsd` is `null`\n * when nothing consulted the monthly document, and a ceiling is `null` when\n * none applied. Neither collapses to 0 here — \"nobody looked\" and \"spent\n * nothing\" have to stay distinguishable at the surface, exactly as they do on\n * the reservation.\n */\nexport function publicAssistCredits(\n costUsd: number | null,\n ceilingUsd: number | null,\n): PublicAssistCredits {\n const used = costUsd === null ? 0 : assistCreditsFromUsd(costUsd)\n if (ceilingUsd === null) return { used, limit: null, remaining: null }\n const limit = assistCreditsFromUsd(ceilingUsd)\n return { used, limit, remaining: Math.max(0, limit - used) }\n}\n\n/**\n * The console's name for the org's hard-cap switch (AGL-2653), quoted by the\n * refusal so the words on the 402 and the words beside the switch are one\n * string. Change it here and both move.\n */\nexport const ASSIST_HARD_CAP_CONTROL_LABEL = 'Stop AI assist at the included band'\n\n/** Where the switch lives, in the words the refusal uses. */\nexport const ASSIST_HARD_CAP_CONTROL_LOCATION = 'Billing → Usage'\n\n/**\n * Whether the org asked to be stopped at its assist band (AGL-2653).\n *\n * Strictly `=== true`: the map is absent on every org that has not touched\n * the switch, and absent is the selling default. A truthy string or a `1`\n * written by hand must not switch an assistant off at the band, so nothing\n * short of the boolean counts.\n */\nexport function resolveAssistHardCap(\n org: Partial<AglynOrgBilling> | null | undefined,\n): boolean {\n return org?.assistOverage?.hardCap === true\n}\n\n/**\n * Is the org's assist band a WALL, or a line past which credits are sold?\n *\n * Two things make it a wall, and only two:\n *\n * - **The org asked.** `assistOverage.hardCap` is the customer's own ceiling,\n * the `storageOverage.capUsd` of this meter.\n * - **There is nothing to sell past it at.** An org whose\n * `resolveAssistOverageRateUsdPer1k` is `null` has no rate to bill, so\n * credits past its band would be provider spend with no invoice line — the\n * silent free overage `plan-entitlements.spec.ts` forbids. Enterprise is\n * one case, its band being contractual; FREE is the other, its taste band\n * a wall by the AGL-2925 decision so that nothing about a Free workspace\n * can produce a charge.\n *\n * Everything else sells past the band by default, which is the 2026-09-07\n * decision this function encodes — and since AGL-3203 that includes Starter,\n * which carries 750 credits and a $3.00 rate like every paid tier above it.\n *\n * An org with NO band at all answers the same as Enterprise, harmlessly:\n * `resolveAssistBudgetUsd` is `null` for it and the reservation never\n * measures against a band. No plan ROW is in that state any more — Free and\n * Starter both were, at different times — so it is reached by a per-org\n * override that writes a zero.\n *\n * An UNCAPPED staff comp (AGL-3049) is neither: it has no band to be a wall,\n * and nothing is sold past a band it does not have. It answers false, ahead\n * of the switch — a workspace staff uncapped is not stopped by a band, and\n * the reservation reads that answer as \"no wall, no backstop default\".\n */\nexport function assistBandRefuses(\n org: Partial<AglynOrgBilling> | null | undefined,\n): boolean {\n if (isUncappedPlanComp(org)) return false\n if (resolveAssistHardCap(org)) return true\n return resolveAssistOverageRateUsdPer1k(org) === null\n}\n\n/**\n * Which ceiling refused a reservation, or `null` when it was admitted.\n *\n * - `messages`: the message cap (daily on the free rung, monthly entitled).\n * - `budget`: the operator's spend backstop.\n * - `band`: the plan's own band, as a wall — the switch, or a plan with\n * no rate to sell past it at.\n * - `cap`: the org's own dollar ceiling on overage (AGL-2898).\n *\n * The four that exist only on the Free taste (AGL-2925), each a precaution\n * against the one band that has no invoice behind it:\n *\n * - `account`: the ACCOUNT's 300 credits are spent across every free\n * workspace it owns — the org band alone would multiply by\n * the workspaces one person may hold.\n * - `requests`: the account's daily free request cap.\n * - `refusals`: the account's free generation is paused for the day after\n * too many `refusal` stops — a brief the model declined is a\n * brief that should not be retried thirty times.\n * - `platform`: the platform-wide daily ceiling on free spend; nobody's\n * fault, and \"try again tomorrow\" is the whole answer.\n *\n * Declared here, beside the helpers that read it, so the reservation, its\n * public projection and both doors name one union.\n */\nexport type AssistRefusedBy =\n | 'messages'\n | 'budget'\n | 'band'\n | 'cap'\n | 'account'\n | 'requests'\n | 'refusals'\n | 'platform'\n | null\n\n/**\n * The sentence a Free workspace is told when one of the taste's own\n * precautions refused it, or `null` when the refusal was something else.\n *\n * One string per rung, for both doors and the gate ladder, so a customer\n * reading the console panel and one reading the besigner get the same\n * words. Every sentence is customer-safe: it names what to do (upgrade,\n * wait) and never a figure from our cost model, a counter document, or\n * another workspace.\n */\nexport function assistFreeTasteRefusalText(\n refusedBy: AssistRefusedBy,\n): string | null {\n switch (refusedBy) {\n case 'account':\n return (\n 'Your free AI credits for this month are used across your ' +\n 'workspaces — upgrade any workspace to keep going.'\n )\n case 'requests':\n return (\n 'Free workspaces get a limited number of AI requests a day — ' +\n 'try again tomorrow, or upgrade to keep going.'\n )\n case 'refusals':\n return (\n 'The AI declined several requests today, so free AI generation is ' +\n 'paused until tomorrow. Try a different kind of brief then.'\n )\n case 'platform':\n return (\n 'Free AI generation is paused for the rest of today — try again ' +\n 'tomorrow. Paid workspaces are not affected.'\n )\n default:\n return null\n }\n}\n\n/**\n * Whether a refusal at the band was the org's OWN doing — the switch is on\n * AND turning it off would have let the exchange through. The refusal that\n * names the switch has to be the one the switch caused: on a plan with no\n * rate the band refuses whatever the switch says, and telling that customer\n * to turn it off would send them to a control that does nothing.\n */\nexport function assistRefusedByHardCap(\n org: Partial<AglynOrgBilling> | null | undefined,\n refusedBy: AssistRefusedBy,\n): boolean {\n return (\n refusedBy === 'band' &&\n resolveAssistHardCap(org) &&\n resolveAssistOverageRateUsdPer1k(org) !== null\n )\n}\n\n/**\n * The sentence a workspace is told when its own hard cap refused it. ONE\n * string for both assist entrypoints, so the console panel and the besigner\n * cannot describe the same control in two ways. It names the switch by its\n * label, says where it lives, and quotes the plan's rate so the reader knows\n * what turning it off costs.\n */\nexport function assistHardCapRefusalText(\n org: Partial<AglynOrgBilling> | null | undefined,\n): string {\n const rate = resolveAssistOverageRateUsdPer1k(org)\n const rateClause =\n rate === null ? '' : ` at $${rate.toFixed(2)} per 1,000 credits`\n return (\n `This workspace used its assistant credits for the month, and ` +\n `\"${ASSIST_HARD_CAP_CONTROL_LABEL}\" is on. Turn it off under ` +\n `${ASSIST_HARD_CAP_CONTROL_LOCATION} to keep going${rateClause}.`\n )\n}\n\nexport interface AssistMonthOverage extends AssistCreditOveragePrice {\n /** Credits drawn this month, rounded up from the measured spend. */\n usedCredits: number\n /** The band those credits were measured against, or `null` for no band. */\n bandCredits: number | null\n}\n\n/**\n * A month's assist overage from its measured spend — the ONE derivation the\n * invoice and every meter read (AGL-2653).\n *\n * Takes `estCostUsd` as the rollup reads it and composes the three steps\n * that already existed — credits from spend, overage past the band, price at\n * the plan's rate — so `report-usage` cannot assemble them in a different\n * order from a console readout and arrive at a different figure. The\n * rounding is the other overage terms' rounding: `priceAssistCreditOverage`\n * rounds to the cent, and the rollup turns dollars into cents with the same\n * `Math.round(usd * 100)` it applies to contacts, API and dataset storage.\n *\n * A non-finite or negative spend is zero credits, for the reason\n * `assistCreditsFromUsd` gives; a plan with no band or no rate prices zero\n * structurally, so Free, Starter and Enterprise bill nothing here without a\n * check that could be forgotten.\n */\nexport function assistMonthOverage(\n org: Partial<AglynOrgBilling> | null | undefined,\n estCostUsd: number,\n): AssistMonthOverage {\n const usedCredits = assistCreditsFromUsd(estCostUsd)\n const bandCredits = resolveAssistCreditBudget(org)\n return {\n usedCredits,\n bandCredits,\n ...priceAssistCreditOverage(\n org,\n assistCreditOverage(usedCredits, bandCredits),\n ),\n }\n}\n\n/** The console's name for the org's overage ceiling (AGL-2898). */\nexport const ASSIST_OVERAGE_CAP_CONTROL_LABEL =\n 'Stop AI when this month’s overage reaches'\n\n/**\n * Bounds on the ceiling a self-serve org may set. The floor is a dollar\n * because a ceiling of cents would refuse on the first exchange past the\n * band while reading as a limit; the roof is the storage cap's, so no\n * self-serve org writes itself an enterprise-sized commitment.\n */\nexport const ASSIST_OVERAGE_CAP_MIN_USD = 1\nexport const ASSIST_OVERAGE_CAP_MAX_USD = 100_000\n\n/**\n * The org's monthly ceiling on AI overage dollars, or `null` for none.\n *\n * Strictly a finite positive number. `null`, absent, a string, `NaN`,\n * `Infinity` and zero all read as no ceiling — the failure mode of the\n * alternative is a wall of `NaN` that every comparison passes, or a ceiling\n * of zero that refuses the first exchange past the band while the card\n * says nothing is set.\n */\nexport function resolveAssistOverageCapUsd(\n org: Partial<AglynOrgBilling> | null | undefined,\n): number | null {\n const raw = org?.assistOverage?.capUsd\n if (typeof raw !== 'number' || !Number.isFinite(raw) || raw <= 0) return null\n return raw\n}\n\n/**\n * Has this month's overage reached the org's own ceiling (AGL-2898)?\n *\n * Composed from `assistMonthOverage`, the ONE derivation the invoice reads,\n * so the figure the ceiling stops at is the figure the invoice would have\n * billed. That composition is also what confines the ceiling to plans that\n * sell past their band: a plan with no rate prices its overage to zero, and\n * zero never reaches a positive ceiling — Free and Enterprise cannot get here\n * without a check that could be forgotten. `>=` rather than `>`, because an\n * org that asked to stop AT a figure is stopped when the figure is met.\n */\nexport function assistOverageCapReached(\n org: Partial<AglynOrgBilling> | null | undefined,\n estCostUsd: number,\n): boolean {\n const cap = resolveAssistOverageCapUsd(org)\n if (cap === null) return false\n return assistMonthOverage(org, estCostUsd).overageMonthlyUsd >= cap\n}\n\n/**\n * Whether a refusal was the org's own ceiling. The reservation answers\n * `'cap'` only when `assistOverageCapReached` did, and that helper only\n * reaches a ceiling on a plan with a rate, so the two conditions restated\n * here are the same guarantee `assistRefusedByHardCap` gives the switch: the\n * control the sentence names is the control that refused.\n */\nexport function assistRefusedByOverageCap(\n org: Partial<AglynOrgBilling> | null | undefined,\n refusedBy: AssistRefusedBy,\n): boolean {\n return (\n refusedBy === 'cap' &&\n resolveAssistOverageCapUsd(org) !== null &&\n resolveAssistOverageRateUsdPer1k(org) !== null\n )\n}\n\n/**\n * The sentence a workspace is told when its own overage ceiling refused it.\n * ONE string for both assist entrypoints, as `assistHardCapRefusalText` is\n * for the switch. It names the control by its label, quotes the figure the\n * org chose, and says where to raise or clear it.\n */\nexport function assistOverageCapRefusalText(\n org: Partial<AglynOrgBilling> | null | undefined,\n): string {\n const cap = resolveAssistOverageCapUsd(org)\n const figure = cap === null ? '' : ` $${cap.toFixed(2)}`\n return (\n `This month’s AI assist overage reached${figure}, the figure you set as ` +\n `\"${ASSIST_OVERAGE_CAP_CONTROL_LABEL}\". Raise or remove it under ` +\n `${ASSIST_HARD_CAP_CONTROL_LOCATION} to keep going.`\n )\n}\n\n/**\n * The refusal sentence for whichever of the org's OWN controls refused, or\n * `null` when the refusal was nobody's control — the message cap, the\n * operator's backstop, or a plan that sells no overage. Both doors answer a\n * 402 with this text when it is a string and their usual 429 otherwise, so\n * the status and the sentence cannot disagree about whose decision it was.\n */\nexport function assistOwnControlRefusalText(\n org: Partial<AglynOrgBilling> | null | undefined,\n refusedBy: AssistRefusedBy,\n): string | null {\n if (assistRefusedByHardCap(org, refusedBy)) return assistHardCapRefusalText(org)\n if (assistRefusedByOverageCap(org, refusedBy)) {\n return assistOverageCapRefusalText(org)\n }\n return null\n}\n"],"names":["isUncappedPlanComp","resolveOrgEntitlements","resolvePlanPricing","ASSIST_CREDIT_COST_USD","ASSIST_PROVIDER_COST_FIELD","assistProviderCostUsd","billedUsd","providerUsd","provider","Number","isFinite","billed","ASSIST_CREDIT_MIN_MARGIN_PCT","assistUsdFromCredits","credits","value","Math","round","assistCreditsFromUsd","usd","ceil","resolveAssistCreditBudget","org","assistCreditsPerMonth","floor","resolveAssistBudgetUsd","assistCreditOverage","usedCredits","bandCredits","used","band","max","resolveAssistOverageRateUsdPer1k","extraAssistCreditsUsdPer1k","priceAssistCreditOverage","overageCredits","overageRateUsd","over","overageMonthlyUsd","assistCreditRateMarginPct","rateUsdPer1k","rate","costPer1k","publicAssistCredits","costUsd","ceilingUsd","limit","remaining","ASSIST_HARD_CAP_CONTROL_LABEL","ASSIST_HARD_CAP_CONTROL_LOCATION","resolveAssistHardCap","assistOverage","hardCap","assistBandRefuses","assistFreeTasteRefusalText","refusedBy","assistRefusedByHardCap","assistHardCapRefusalText","rateClause","toFixed","assistMonthOverage","estCostUsd","ASSIST_OVERAGE_CAP_CONTROL_LABEL","ASSIST_OVERAGE_CAP_MIN_USD","ASSIST_OVERAGE_CAP_MAX_USD","resolveAssistOverageCapUsd","raw","capUsd","assistOverageCapReached","cap","assistRefusedByOverageCap","assistOverageCapRefusalText","figure","assistOwnControlRefusalText"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/assist-credits.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n isUncappedPlanComp,\n resolveOrgEntitlements,\n resolvePlanPricing,\n} from './plan-entitlements'\nimport { planQuotaOf } from '../plugin-manager/plugin-plan-entitlements'\nimport type { AglynOrgBilling } from '../foundation/definitions/org-billing.types'\n\n/**\n * Aglyn Assist credits: the unit assist is sold, metered and refused in.\n *\n * ## Why cost, and not messages\n *\n * Assist is measured per exchange as `estCostUsd` — the exchange priced at\n * the serving model's billed rates, computed where the tokens were counted. A\n * message allowance would throw that measurement away and price every action\n * the same. It cannot: a question is a few thousand tokens, while generating\n * a screen carries the node tree, the component catalog and the theme tokens\n * in, emits structured markup out, and iterates. The two differ by up to two\n * orders of magnitude, so one workspace's ten screen builds can outspend\n * another's thousand questions while both read as \"within allowance\".\n *\n * Metering in cost is the only unit under which those two workspaces draw\n * down their bands differently, which is the entire point.\n *\n * ## Why a credit rather than the dollar figure\n *\n * `estCostUsd` is a dollar figure off our own cost model, not a price.\n * Publishing it would put our model choice, our per-model rates and our\n * margin on a billing page, and it would move under customers every time a\n * model is swapped. A credit is a fixed quantity of that spend with a stable\n * public meaning, so the band on the plan card stays the same number when\n * the model behind it changes.\n *\n * Everything customer-facing counts credits. This module is the ONLY place\n * the two units meet.\n *\n * ## `estCostUsd` is the BILLED figure, not the bill we pay (AGL-3015)\n *\n * It is every exchange priced at the catalog's billed rates, which a model\n * may carry above what its provider charges. Credits are drawn from it, so\n * it is the right figure for a band, a wall, a cap and an invoice line. It\n * is the WRONG figure for a margin or a cost meter, which read the rollup's\n * provider figure through `assistProviderCostUsd` instead.\n */\n\n/**\n * What one credit is BILLED at, in USD.\n *\n * A tenth of a cent, chosen for two properties rather than for roundness:\n *\n * - **1,000 credits cost exactly $1.00**, so the per-1,000 retail rates on\n * `PLAN_PRICING.extraAssistCreditsUsdPer1k` read directly as multiples of\n * cost. A rate of $2.00 per 1,000 is cost x2 with no arithmetic in between,\n * which is what makes the 50% margin floor checkable by eye.\n * - **A single question is tens of credits, a generated screen is hundreds.**\n * At Sonnet list rates a short grounded answer runs around $0.013, so ~13\n * credits; a screen build runs into the hundreds. Both are whole numbers\n * with room to differ, which a cent-sized credit would have flattened to\n * \"1 vs 20\" and a dollar-sized one to \"0 vs 0\".\n *\n * ⚠️ This is a COST-MODEL constant, not a price and not a rate we publish.\n * It never appears on a customer surface; `assistCreditsFromUsd` is how a\n * measured dollar figure becomes something a customer may be shown.\n *\n * ⚠️ AND IT IS NOT WHAT A CREDIT COSTS US (AGL-3015). The dollar figure this\n * divides is priced at the model catalog's BILLED rates, and a model may be\n * billed above what the provider charges. A credit is therefore a tenth of a\n * cent of BILLED spend, and at most a tenth of a cent of provider spend. Read\n * against a retail rate it yields the floor of the line margin, never the\n * middle of it — which is the safe direction, and the direction every margin\n * assertion in this repo depends on. What a period actually cost is measured,\n * not derived: `assistProviderCostUsd` reads it off the rollup.\n */\nexport const ASSIST_CREDIT_COST_USD = 0.001\n\n/**\n * The field a monthly assist rollup, a signal row and a kind bucket keep\n * PROVIDER spend under, beside the `estCostUsd` a customer's credits are\n * drawn from (AGL-3015).\n *\n * Named here rather than in the meter that writes it because the readers are\n * spread across the plugin, the billing routes and the staff surfaces, and a\n * field name spelled out at six call sites is a field name that will be\n * spelled differently at the seventh.\n */\nexport const ASSIST_PROVIDER_COST_FIELD = 'providerCostUsd'\n\n/**\n * What a metered period ACTUALLY COST US, from the two dollar figures a\n * rollup carries.\n *\n * `billedUsd` is `estCostUsd`: every exchange priced at its model's billed\n * rates, and the figure credits are drawn from. `providerUsd` is\n * `ASSIST_PROVIDER_COST_FIELD`: the same exchanges priced at what the\n * provider charged. The second is the answer wherever it exists.\n *\n * ## Why a rollup may not carry one, and which way that errs\n *\n * The provider figure is written from the commit that split the two rates\n * onward. A period closed before it answers with the BILLED figure, which is\n * at or above what we paid — so a margin taken on history reads low and a\n * cost reads high, the direction a cost figure is allowed to be wrong in.\n * The one period that under-reads is the month in flight when the split\n * shipped, whose provider figure covers only the exchanges after it; that\n * month's error is bounded by the markup on a few days of a meter whose\n * largest measured organization is a fraction of a cent.\n */\nexport function assistProviderCostUsd(\n billedUsd: unknown,\n providerUsd: unknown,\n): number {\n const provider = Number(providerUsd)\n if (Number.isFinite(provider) && provider > 0) return provider\n const billed = Number(billedUsd)\n return Number.isFinite(billed) && billed > 0 ? billed : 0\n}\n\n/**\n * The minimum line margin every retail assist rate must clear.\n *\n * The same floor now applied to every retail rate in this codebase, and the\n * reason the assist ladder stops descending at cost x2 on Agency instead of\n * taking one more step the way the contacts ladder once did.\n */\nexport const ASSIST_CREDIT_MIN_MARGIN_PCT = 0.5\n\n/**\n * Credits -> our cost in USD. Non-finite or negative input is 0 credits, so a\n * corrupt band cannot become a budget of `NaN` that every comparison passes.\n */\nexport function assistUsdFromCredits(credits: number): number {\n const value = Number(credits)\n if (!Number.isFinite(value) || value <= 0) return 0\n return Math.round(value * ASSIST_CREDIT_COST_USD * 1_000_000) / 1_000_000\n}\n\n/**\n * Measured USD -> credits, rounded UP.\n *\n * Up, because this converts spend the platform has ALREADY made into the\n * figure a customer is shown against their band. Rounding down would let a\n * long tail of sub-credit exchanges cost real money and draw nothing, which\n * is the one direction a meter must not err in.\n */\nexport function assistCreditsFromUsd(usd: number): number {\n const value = Number(usd)\n if (!Number.isFinite(value) || value <= 0) return 0\n return Math.ceil(value / ASSIST_CREDIT_COST_USD)\n}\n\n/**\n * The org's monthly assist band in credits, or `null` when its plan sells no\n * assist band at all.\n *\n * ## `null` is not zero, and the difference is the whole safety property\n *\n * A band of 0 resolves to `null` — \"this org sells no band\" — and never to\n * a budget of `$0`. An org with no band still reaches the console\n * assistant's docs-grounded rung, which is bounded by the free daily message\n * cap and by the operator's spend backstop. Resolving zero as a budget would\n * refuse that rung outright: a whole assistant turned off by a pricing field\n * that was never about it.\n *\n * Since AGL-3203 no PLAN ROW bands at zero. Free carries the AGL-2925 taste\n * of `FREE_AI_TASTE_CREDITS_PER_MONTH` with no rate beside it, so it\n * resolves to a budget here and `assistBandRefuses` makes that budget a\n * wall; Starter carries 750 WITH a rate, so its budget is a line it meters\n * past rather than a wall. Both draw their docs-grounded chat on the same\n * band, which is fine — each is sized so a month of it costs well under a\n * dollar.\n *\n * So the zero case is now reached only by a per-org override that writes\n * one, and the reading above is what keeps that org's assistant running.\n *\n * The Aglyn AI add-on (AGL-2896) needs no knowledge here either way: the\n * resolver has already added `AI_ADDON_CREDITS_PER_MONTH[plan]` to the\n * band, because the add-on widens the one pool rather than opening a\n * second.\n *\n * The same reading is what makes the guard survive a stubbed entitlements\n * module. A test double that answers 0 for every quota produces `null` here,\n * which falls through to the operator backstop, rather than a budget of zero\n * that refuses everything while the assertions read green. A clamp that goes\n * green having refused every request has proved nothing, and 0 is exactly the\n * value a stub returns.\n *\n * ## Non-finite is also `null`, deliberately\n *\n * `Infinity` must never resolve to a budget. It is unrepresentable off-process\n * — `JSON.stringify(Infinity)` is `null` and reads back as 0 — so a band that\n * reached here as `Infinity` is a band that would read as zero somewhere\n * else. Falling through to the backstop is bounded and consistent; honouring\n * it would be an unbounded budget on one process and a refusal on the next.\n *\n * An UNCAPPED staff comp (AGL-3049) is where that `Infinity` comes from on\n * purpose, and `null` is right for it here too: there is no band to measure\n * against, be over, price or alert on. What `null` must NOT bring with it is\n * the backstop, whose repo default would cap the one workspace staff said\n * has no cap — so `assistBandRefuses` answers false for it, and the\n * reservation's ceiling (`assistMonthlyCeilingUsd` in the AI plugin) drops\n * the default and keeps only an operator's explicit figure.\n */\nexport function resolveAssistCreditBudget(\n org: Partial<AglynOrgBilling> | null | undefined,\n): number | null {\n const credits = planQuotaOf(resolveOrgEntitlements(org), 'assistCreditsPerMonth')\n if (!Number.isFinite(credits) || credits <= 0) return null\n return Math.floor(credits)\n}\n\n/** The org's monthly assist band in USD of provider spend, or `null`. */\nexport function resolveAssistBudgetUsd(\n org: Partial<AglynOrgBilling> | null | undefined,\n): number | null {\n const credits = resolveAssistCreditBudget(org)\n return credits === null ? null : assistUsdFromCredits(credits)\n}\n\n/**\n * Credits spent past the band. A `null` band yields 0 — an org with no band\n * has nothing to be over, and reporting the whole month's spend as overage\n * there would invoice the free tier for its docs answers.\n */\nexport function assistCreditOverage(\n usedCredits: number,\n bandCredits: number | null,\n): number {\n const used = Number(usedCredits)\n if (!Number.isFinite(used) || used <= 0) return 0\n if (bandCredits === null) return 0\n const band = Number(bandCredits)\n if (!Number.isFinite(band) || band <= 0) return 0\n return Math.max(0, used - band)\n}\n\n/**\n * The per-1,000 rate the org's assist credits are sold at past its band, or\n * `null` when nothing is sold past it — the ONE reading of the rate\n * (AGL-2896). `priceAssistCreditOverage`, `assistBandRefuses`,\n * `assistRefusedByHardCap` and `assistHardCapRefusalText` all go through\n * it, so the gate, the invoice and the refusal sentence cannot quote three\n * different rates for one org.\n *\n * The plan's `extraAssistCreditsUsdPer1k` IS the answer — the whole answer,\n * since AGL-3203. Starter used to be the one exception: it banded at 0, so\n * the rate could not sit on its plan row without advertising a fee on a\n * quantity the plan never sold, and the add-on's band was priced from a\n * constant read here instead. Starter now includes 750 credits\n * unconditionally and carries $3.00 on its own row, so the exception and its\n * constant are gone and there is exactly one place the rate is written.\n *\n * Free and Enterprise are null with or without the add-on: Free sells no\n * add-on and its taste band is a wall by decision (AGL-2925), and\n * Enterprise's usage is in the contract.\n *\n * A STAFF COMP is null on every plan and with every add-on (AGL-3034),\n * because `resolvePlanPricing` — never `PLAN_PRICING` indexed directly —\n * sells nothing on a comp. That is what makes a comp's band a wall at the\n * gate and prices its overage to zero at the charge: the AI overage path\n * (AGL-3011) claims only what `assistMonthOverage` prices, so a comp never\n * becomes a Stripe invoice. The function is a single read for that reason;\n * a second source for the rate is how a comp gets quoted one.\n */\nexport function resolveAssistOverageRateUsdPer1k(\n org: Partial<AglynOrgBilling> | null | undefined,\n): number | null {\n return resolvePlanPricing(org).extraAssistCreditsUsdPer1k\n}\n\nexport interface AssistCreditOveragePrice {\n /** Credits past the plan's band, as handed in. */\n overageCredits: number\n /** Estimated charge at the plan's per-1,000 rate. */\n overageMonthlyUsd: number\n /** Per-1,000 rate; null when the plan prices no assist overage. */\n overageRateUsd: number | null\n}\n\n/**\n * Prices assist credits past a plan's band.\n *\n * ## No `allowed`, deliberately — and for the opposite reason to email\n *\n * `priceEmailSendOverage` omits an `allowed` field because email has two\n * gates and a transactional sender consulting the wrong one drops a password\n * reset. Assist omits it because it has exactly ONE gate and this is not it:\n * whether the band refuses is `assistBandRefuses`, applied inside\n * `reserveAssistMessage` before a token is spent. A second answer to \"may\n * this proceed\", computed after the fact from a rounded credit figure, would\n * be a second gate to drift from the first.\n *\n * ## Takes the overage, does not recompute it\n *\n * `assistCreditOverage` derives the excess once, from the measured meter and\n * the resolved band. Re-deriving it here would be a second overage model.\n *\n * A null rate yields zero structurally rather than by a check, which is what\n * keeps Free and Starter at zero on this axis.\n */\nexport function priceAssistCreditOverage(\n org: Partial<AglynOrgBilling> | null | undefined,\n overageCredits: number,\n): AssistCreditOveragePrice {\n const overageRateUsd = resolveAssistOverageRateUsdPer1k(org)\n const credits = Number(overageCredits)\n const over = Number.isFinite(credits) && credits > 0 ? credits : 0\n return {\n overageCredits: over,\n overageMonthlyUsd:\n overageRateUsd === null\n ? 0\n : Math.round((over / 1000) * overageRateUsd * 100) / 100,\n overageRateUsd,\n }\n}\n\n/**\n * The line margin a per-1,000-credit retail rate earns AT LEAST, as a\n * fraction.\n *\n * One expression, so the ladder and its floor cannot be checked against two\n * different definitions of margin. A null or non-positive rate has no margin\n * to report and answers `null` rather than 1 or 0 — \"this plan sells no\n * overage\" is not \"this plan sells overage at 100% margin\".\n *\n * A LOWER BOUND, not the realized figure (AGL-3015). The cost side is\n * `ASSIST_CREDIT_COST_USD`, which is what a credit BILLS at; a model billed\n * above its provider rate costs us less than that per credit, so the real\n * line margin is this or better and never worse. The floor stays checkable\n * by eye — a $2.00 rate is still cost x2 — and a retail rate that clears the\n * floor here clears it on every model mix. What a period realized is the\n * staff margin card's business, and it reads measured provider spend.\n */\nexport function assistCreditRateMarginPct(\n rateUsdPer1k: number | null,\n): number | null {\n if (rateUsdPer1k === null) return null\n const rate = Number(rateUsdPer1k)\n if (!Number.isFinite(rate) || rate <= 0) return null\n const costPer1k = ASSIST_CREDIT_COST_USD * 1000\n return (rate - costPer1k) / rate\n}\n\n/**\n * What a refused or in-progress workspace may be TOLD about its assist band.\n *\n * The reservation carries `costUsd` and `costLimitUsd` because that is what\n * it measured and what it compared against, and both are our provider bill.\n * Shipping the reservation itself to a browser publishes them. This is the\n * projection that crosses that boundary: credits only, no dollars, no model,\n * no rates.\n */\nexport interface PublicAssistCredits {\n /** Credits drawn this month, rounded up. */\n used: number\n /** The band, or `null` when no band applied to this request. */\n limit: number | null\n /** Credits left in the band, or `null` when there is no band. */\n remaining: number | null\n}\n\n/**\n * Projects a measured spend figure and a ceiling into the credit view.\n *\n * Both inputs are nullable because the reservation's are: `costUsd` is `null`\n * when nothing consulted the monthly document, and a ceiling is `null` when\n * none applied. Neither collapses to 0 here — \"nobody looked\" and \"spent\n * nothing\" have to stay distinguishable at the surface, exactly as they do on\n * the reservation.\n */\nexport function publicAssistCredits(\n costUsd: number | null,\n ceilingUsd: number | null,\n): PublicAssistCredits {\n const used = costUsd === null ? 0 : assistCreditsFromUsd(costUsd)\n if (ceilingUsd === null) return { used, limit: null, remaining: null }\n const limit = assistCreditsFromUsd(ceilingUsd)\n return { used, limit, remaining: Math.max(0, limit - used) }\n}\n\n/**\n * The console's name for the org's hard-cap switch (AGL-2653), quoted by the\n * refusal so the words on the 402 and the words beside the switch are one\n * string. Change it here and both move.\n */\nexport const ASSIST_HARD_CAP_CONTROL_LABEL = 'Stop AI assist at the included band'\n\n/** Where the switch lives, in the words the refusal uses. */\nexport const ASSIST_HARD_CAP_CONTROL_LOCATION = 'Billing → Usage'\n\n/**\n * Whether the org asked to be stopped at its assist band (AGL-2653).\n *\n * Strictly `=== true`: the map is absent on every org that has not touched\n * the switch, and absent is the selling default. A truthy string or a `1`\n * written by hand must not switch an assistant off at the band, so nothing\n * short of the boolean counts.\n */\nexport function resolveAssistHardCap(\n org: Partial<AglynOrgBilling> | null | undefined,\n): boolean {\n return org?.assistOverage?.hardCap === true\n}\n\n/**\n * Is the org's assist band a WALL, or a line past which credits are sold?\n *\n * Two things make it a wall, and only two:\n *\n * - **The org asked.** `assistOverage.hardCap` is the customer's own ceiling,\n * the `storageOverage.capUsd` of this meter.\n * - **There is nothing to sell past it at.** An org whose\n * `resolveAssistOverageRateUsdPer1k` is `null` has no rate to bill, so\n * credits past its band would be provider spend with no invoice line — the\n * silent free overage `plan-entitlements.spec.ts` forbids. Enterprise is\n * one case, its band being contractual; FREE is the other, its taste band\n * a wall by the AGL-2925 decision so that nothing about a Free workspace\n * can produce a charge.\n *\n * Everything else sells past the band by default, which is the 2026-09-07\n * decision this function encodes — and since AGL-3203 that includes Starter,\n * which carries 750 credits and a $3.00 rate like every paid tier above it.\n *\n * An org with NO band at all answers the same as Enterprise, harmlessly:\n * `resolveAssistBudgetUsd` is `null` for it and the reservation never\n * measures against a band. No plan ROW is in that state any more — Free and\n * Starter both were, at different times — so it is reached by a per-org\n * override that writes a zero.\n *\n * An UNCAPPED staff comp (AGL-3049) is neither: it has no band to be a wall,\n * and nothing is sold past a band it does not have. It answers false, ahead\n * of the switch — a workspace staff uncapped is not stopped by a band, and\n * the reservation reads that answer as \"no wall, no backstop default\".\n */\nexport function assistBandRefuses(\n org: Partial<AglynOrgBilling> | null | undefined,\n): boolean {\n if (isUncappedPlanComp(org)) return false\n if (resolveAssistHardCap(org)) return true\n return resolveAssistOverageRateUsdPer1k(org) === null\n}\n\n/**\n * Which ceiling refused a reservation, or `null` when it was admitted.\n *\n * - `messages`: the message cap (daily on the free rung, monthly entitled).\n * - `budget`: the operator's spend backstop.\n * - `band`: the plan's own band, as a wall — the switch, or a plan with\n * no rate to sell past it at.\n * - `cap`: the org's own dollar ceiling on overage (AGL-2898).\n *\n * The four that exist only on the Free taste (AGL-2925), each a precaution\n * against the one band that has no invoice behind it:\n *\n * - `account`: the ACCOUNT's 300 credits are spent across every free\n * workspace it owns — the org band alone would multiply by\n * the workspaces one person may hold.\n * - `requests`: the account's daily free request cap.\n * - `refusals`: the account's free generation is paused for the day after\n * too many `refusal` stops — a brief the model declined is a\n * brief that should not be retried thirty times.\n * - `platform`: the platform-wide daily ceiling on free spend; nobody's\n * fault, and \"try again tomorrow\" is the whole answer.\n *\n * Declared here, beside the helpers that read it, so the reservation, its\n * public projection and both doors name one union.\n */\nexport type AssistRefusedBy =\n | 'messages'\n | 'budget'\n | 'band'\n | 'cap'\n | 'account'\n | 'requests'\n | 'refusals'\n | 'platform'\n | null\n\n/**\n * The sentence a Free workspace is told when one of the taste's own\n * precautions refused it, or `null` when the refusal was something else.\n *\n * One string per rung, for both doors and the gate ladder, so a customer\n * reading the console panel and one reading the besigner get the same\n * words. Every sentence is customer-safe: it names what to do (upgrade,\n * wait) and never a figure from our cost model, a counter document, or\n * another workspace.\n */\nexport function assistFreeTasteRefusalText(\n refusedBy: AssistRefusedBy,\n): string | null {\n switch (refusedBy) {\n case 'account':\n return (\n 'Your free AI credits for this month are used across your ' +\n 'workspaces — upgrade any workspace to keep going.'\n )\n case 'requests':\n return (\n 'Free workspaces get a limited number of AI requests a day — ' +\n 'try again tomorrow, or upgrade to keep going.'\n )\n case 'refusals':\n return (\n 'The AI declined several requests today, so free AI generation is ' +\n 'paused until tomorrow. Try a different kind of brief then.'\n )\n case 'platform':\n return (\n 'Free AI generation is paused for the rest of today — try again ' +\n 'tomorrow. Paid workspaces are not affected.'\n )\n default:\n return null\n }\n}\n\n/**\n * Whether a refusal at the band was the org's OWN doing — the switch is on\n * AND turning it off would have let the exchange through. The refusal that\n * names the switch has to be the one the switch caused: on a plan with no\n * rate the band refuses whatever the switch says, and telling that customer\n * to turn it off would send them to a control that does nothing.\n */\nexport function assistRefusedByHardCap(\n org: Partial<AglynOrgBilling> | null | undefined,\n refusedBy: AssistRefusedBy,\n): boolean {\n return (\n refusedBy === 'band' &&\n resolveAssistHardCap(org) &&\n resolveAssistOverageRateUsdPer1k(org) !== null\n )\n}\n\n/**\n * The sentence a workspace is told when its own hard cap refused it. ONE\n * string for both assist entrypoints, so the console panel and the besigner\n * cannot describe the same control in two ways. It names the switch by its\n * label, says where it lives, and quotes the plan's rate so the reader knows\n * what turning it off costs.\n */\nexport function assistHardCapRefusalText(\n org: Partial<AglynOrgBilling> | null | undefined,\n): string {\n const rate = resolveAssistOverageRateUsdPer1k(org)\n const rateClause =\n rate === null ? '' : ` at $${rate.toFixed(2)} per 1,000 credits`\n return (\n `This workspace used its assistant credits for the month, and ` +\n `\"${ASSIST_HARD_CAP_CONTROL_LABEL}\" is on. Turn it off under ` +\n `${ASSIST_HARD_CAP_CONTROL_LOCATION} to keep going${rateClause}.`\n )\n}\n\nexport interface AssistMonthOverage extends AssistCreditOveragePrice {\n /** Credits drawn this month, rounded up from the measured spend. */\n usedCredits: number\n /** The band those credits were measured against, or `null` for no band. */\n bandCredits: number | null\n}\n\n/**\n * A month's assist overage from its measured spend — the ONE derivation the\n * invoice and every meter read (AGL-2653).\n *\n * Takes `estCostUsd` as the rollup reads it and composes the three steps\n * that already existed — credits from spend, overage past the band, price at\n * the plan's rate — so `report-usage` cannot assemble them in a different\n * order from a console readout and arrive at a different figure. The\n * rounding is the other overage terms' rounding: `priceAssistCreditOverage`\n * rounds to the cent, and the rollup turns dollars into cents with the same\n * `Math.round(usd * 100)` it applies to contacts, API and dataset storage.\n *\n * A non-finite or negative spend is zero credits, for the reason\n * `assistCreditsFromUsd` gives; a plan with no band or no rate prices zero\n * structurally, so Free, Starter and Enterprise bill nothing here without a\n * check that could be forgotten.\n */\nexport function assistMonthOverage(\n org: Partial<AglynOrgBilling> | null | undefined,\n estCostUsd: number,\n): AssistMonthOverage {\n const usedCredits = assistCreditsFromUsd(estCostUsd)\n const bandCredits = resolveAssistCreditBudget(org)\n return {\n usedCredits,\n bandCredits,\n ...priceAssistCreditOverage(\n org,\n assistCreditOverage(usedCredits, bandCredits),\n ),\n }\n}\n\n/** The console's name for the org's overage ceiling (AGL-2898). */\nexport const ASSIST_OVERAGE_CAP_CONTROL_LABEL =\n 'Stop AI when this month’s overage reaches'\n\n/**\n * Bounds on the ceiling a self-serve org may set. The floor is a dollar\n * because a ceiling of cents would refuse on the first exchange past the\n * band while reading as a limit; the roof is the storage cap's, so no\n * self-serve org writes itself an enterprise-sized commitment.\n */\nexport const ASSIST_OVERAGE_CAP_MIN_USD = 1\nexport const ASSIST_OVERAGE_CAP_MAX_USD = 100_000\n\n/**\n * The org's monthly ceiling on AI overage dollars, or `null` for none.\n *\n * Strictly a finite positive number. `null`, absent, a string, `NaN`,\n * `Infinity` and zero all read as no ceiling — the failure mode of the\n * alternative is a wall of `NaN` that every comparison passes, or a ceiling\n * of zero that refuses the first exchange past the band while the card\n * says nothing is set.\n */\nexport function resolveAssistOverageCapUsd(\n org: Partial<AglynOrgBilling> | null | undefined,\n): number | null {\n const raw = org?.assistOverage?.capUsd\n if (typeof raw !== 'number' || !Number.isFinite(raw) || raw <= 0) return null\n return raw\n}\n\n/**\n * Has this month's overage reached the org's own ceiling (AGL-2898)?\n *\n * Composed from `assistMonthOverage`, the ONE derivation the invoice reads,\n * so the figure the ceiling stops at is the figure the invoice would have\n * billed. That composition is also what confines the ceiling to plans that\n * sell past their band: a plan with no rate prices its overage to zero, and\n * zero never reaches a positive ceiling — Free and Enterprise cannot get here\n * without a check that could be forgotten. `>=` rather than `>`, because an\n * org that asked to stop AT a figure is stopped when the figure is met.\n */\nexport function assistOverageCapReached(\n org: Partial<AglynOrgBilling> | null | undefined,\n estCostUsd: number,\n): boolean {\n const cap = resolveAssistOverageCapUsd(org)\n if (cap === null) return false\n return assistMonthOverage(org, estCostUsd).overageMonthlyUsd >= cap\n}\n\n/**\n * Whether a refusal was the org's own ceiling. The reservation answers\n * `'cap'` only when `assistOverageCapReached` did, and that helper only\n * reaches a ceiling on a plan with a rate, so the two conditions restated\n * here are the same guarantee `assistRefusedByHardCap` gives the switch: the\n * control the sentence names is the control that refused.\n */\nexport function assistRefusedByOverageCap(\n org: Partial<AglynOrgBilling> | null | undefined,\n refusedBy: AssistRefusedBy,\n): boolean {\n return (\n refusedBy === 'cap' &&\n resolveAssistOverageCapUsd(org) !== null &&\n resolveAssistOverageRateUsdPer1k(org) !== null\n )\n}\n\n/**\n * The sentence a workspace is told when its own overage ceiling refused it.\n * ONE string for both assist entrypoints, as `assistHardCapRefusalText` is\n * for the switch. It names the control by its label, quotes the figure the\n * org chose, and says where to raise or clear it.\n */\nexport function assistOverageCapRefusalText(\n org: Partial<AglynOrgBilling> | null | undefined,\n): string {\n const cap = resolveAssistOverageCapUsd(org)\n const figure = cap === null ? '' : ` $${cap.toFixed(2)}`\n return (\n `This month’s AI assist overage reached${figure}, the figure you set as ` +\n `\"${ASSIST_OVERAGE_CAP_CONTROL_LABEL}\". Raise or remove it under ` +\n `${ASSIST_HARD_CAP_CONTROL_LOCATION} to keep going.`\n )\n}\n\n/**\n * The refusal sentence for whichever of the org's OWN controls refused, or\n * `null` when the refusal was nobody's control — the message cap, the\n * operator's backstop, or a plan that sells no overage. Both doors answer a\n * 402 with this text when it is a string and their usual 429 otherwise, so\n * the status and the sentence cannot disagree about whose decision it was.\n */\nexport function assistOwnControlRefusalText(\n org: Partial<AglynOrgBilling> | null | undefined,\n refusedBy: AssistRefusedBy,\n): string | null {\n if (assistRefusedByHardCap(org, refusedBy)) return assistHardCapRefusalText(org)\n if (assistRefusedByOverageCap(org, refusedBy)) {\n return assistOverageCapRefusalText(org)\n }\n return null\n}\n"],"names":["isUncappedPlanComp","resolveOrgEntitlements","resolvePlanPricing","planQuotaOf","ASSIST_CREDIT_COST_USD","ASSIST_PROVIDER_COST_FIELD","assistProviderCostUsd","billedUsd","providerUsd","provider","Number","isFinite","billed","ASSIST_CREDIT_MIN_MARGIN_PCT","assistUsdFromCredits","credits","value","Math","round","assistCreditsFromUsd","usd","ceil","resolveAssistCreditBudget","org","floor","resolveAssistBudgetUsd","assistCreditOverage","usedCredits","bandCredits","used","band","max","resolveAssistOverageRateUsdPer1k","extraAssistCreditsUsdPer1k","priceAssistCreditOverage","overageCredits","overageRateUsd","over","overageMonthlyUsd","assistCreditRateMarginPct","rateUsdPer1k","rate","costPer1k","publicAssistCredits","costUsd","ceilingUsd","limit","remaining","ASSIST_HARD_CAP_CONTROL_LABEL","ASSIST_HARD_CAP_CONTROL_LOCATION","resolveAssistHardCap","assistOverage","hardCap","assistBandRefuses","assistFreeTasteRefusalText","refusedBy","assistRefusedByHardCap","assistHardCapRefusalText","rateClause","toFixed","assistMonthOverage","estCostUsd","ASSIST_OVERAGE_CAP_CONTROL_LABEL","ASSIST_OVERAGE_CAP_MIN_USD","ASSIST_OVERAGE_CAP_MAX_USD","resolveAssistOverageCapUsd","raw","capUsd","assistOverageCapReached","cap","assistRefusedByOverageCap","assistOverageCapRefusalText","figure","assistOwnControlRefusalText"],"mappings":"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"}
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type DatasetModel } from './dataset-models';
|
|
18
17
|
/**
|
|
19
18
|
* Browsable data-placeholder catalogs for the designer's insert picker
|
|
20
19
|
* (AGL-583). Hand-typing `{{entry.title}}` stays the advanced path; these
|
|
@@ -67,16 +66,9 @@ export declare const COLLECTION_TOKEN_CATALOG: readonly BindingTokenCatalogEntry
|
|
|
67
66
|
*/
|
|
68
67
|
export declare const AUTHOR_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[];
|
|
69
68
|
/**
|
|
70
|
-
*
|
|
71
|
-
* one reference to a field of the referenced record
|
|
72
|
-
* takes the stable
|
|
69
|
+
* The `{{item.*}}` token for one field of the record a repeat is rendering,
|
|
70
|
+
* optionally hopping one reference to a field of the referenced record
|
|
71
|
+
* (AGL-180). Always takes the stable field id — display names are labels
|
|
72
|
+
* only, so renaming a field never breaks a binding (AGL-578).
|
|
73
73
|
*/
|
|
74
|
-
export declare function
|
|
75
|
-
/**
|
|
76
|
-
* Dataset-item token entries for a repeatable container's model: one per
|
|
77
|
-
* field in model order, labeled with the CURRENT display name while the
|
|
78
|
-
* token carries the stable reference id (AGL-578 — renaming a label never
|
|
79
|
-
* breaks bindings). Reference fields with a configured display field also
|
|
80
|
-
* get their one-hop token (`{{item.author.name}}`).
|
|
81
|
-
*/
|
|
82
|
-
export declare function datasetItemTokens(model: DatasetModel): BindingTokenCatalogEntry[];
|
|
74
|
+
export declare function repeatItemToken(fieldId: string, targetFieldId?: string): string;
|
|
@@ -13,9 +13,17 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
*/ /**
|
|
17
|
+
* Browsable data-placeholder catalogs for the designer's insert picker
|
|
18
|
+
* (AGL-583). Hand-typing `{{entry.title}}` stays the advanced path; these
|
|
19
|
+
* catalogs give every token a friendly label + description so editors can
|
|
20
|
+
* browse and insert instead of memorizing the grammar.
|
|
21
|
+
*
|
|
22
|
+
* The token STRINGS are owned elsewhere — `collectionEntryTokens`
|
|
23
|
+
* (collection-entries.ts) and the tenant compose pipeline resolve them —
|
|
24
|
+
* this module only names them for pickers. The spec cross-checks the entry
|
|
25
|
+
* catalog against the resolver so the two can never drift.
|
|
26
|
+
*/ /**
|
|
19
27
|
* `{{entry.*}}` tokens (AGL-105/551/582) — resolve per entry inside a
|
|
20
28
|
* Collection entries block and page-wide on entry-template screens.
|
|
21
29
|
* Mirrors {@link collectionEntryTokens}; the spec enforces the mirror.
|
|
@@ -115,7 +123,7 @@ import { humanizeDatasetFieldId } from "./datasets.js";
|
|
|
115
123
|
{
|
|
116
124
|
token: '{{entry.coverVideo}}',
|
|
117
125
|
label: 'Featured video',
|
|
118
|
-
description: 'The featured video’s source — a library film, a video link or
|
|
126
|
+
description: 'The featured video’s source — a library film, a video file link or ' + 'a video host’s link — for a Video element.'
|
|
119
127
|
},
|
|
120
128
|
{
|
|
121
129
|
token: '{{entry.category}}',
|
|
@@ -257,44 +265,12 @@ import { humanizeDatasetFieldId } from "./datasets.js";
|
|
|
257
265
|
}
|
|
258
266
|
];
|
|
259
267
|
/**
|
|
260
|
-
*
|
|
261
|
-
* one reference to a field of the referenced record
|
|
262
|
-
* takes the stable
|
|
263
|
-
|
|
268
|
+
* The `{{item.*}}` token for one field of the record a repeat is rendering,
|
|
269
|
+
* optionally hopping one reference to a field of the referenced record
|
|
270
|
+
* (AGL-180). Always takes the stable field id — display names are labels
|
|
271
|
+
* only, so renaming a field never breaks a binding (AGL-578).
|
|
272
|
+
*/ export function repeatItemToken(fieldId, targetFieldId) {
|
|
264
273
|
return targetFieldId ? `{{item.${fieldId}.${targetFieldId}}}` : `{{item.${fieldId}}}`;
|
|
265
274
|
}
|
|
266
|
-
/**
|
|
267
|
-
* Dataset-item token entries for a repeatable container's model: one per
|
|
268
|
-
* field in model order, labeled with the CURRENT display name while the
|
|
269
|
-
* token carries the stable reference id (AGL-578 — renaming a label never
|
|
270
|
-
* breaks bindings). Reference fields with a configured display field also
|
|
271
|
-
* get their one-hop token (`{{item.author.name}}`).
|
|
272
|
-
*/ export function datasetItemTokens(model) {
|
|
273
|
-
var _model_order;
|
|
274
|
-
const entries = [];
|
|
275
|
-
for (const fieldId of (_model_order = model.order) != null ? _model_order : []){
|
|
276
|
-
var _DATASET_FIELD_TYPE_LABELS_field_type;
|
|
277
|
-
var _model_fields, _field_name, _field_reference;
|
|
278
|
-
const field = (_model_fields = model.fields) == null ? void 0 : _model_fields[fieldId];
|
|
279
|
-
if (!field) continue;
|
|
280
|
-
const label = ((_field_name = field.name) == null ? void 0 : _field_name.trim()) || humanizeDatasetFieldId(fieldId);
|
|
281
|
-
entries.push({
|
|
282
|
-
token: datasetItemToken(fieldId),
|
|
283
|
-
label,
|
|
284
|
-
description: (_DATASET_FIELD_TYPE_LABELS_field_type = DATASET_FIELD_TYPE_LABELS[field.type]) != null ? _DATASET_FIELD_TYPE_LABELS_field_type : field.type
|
|
285
|
-
});
|
|
286
|
-
// One reference hop (AGL-180): surface the configured display field of
|
|
287
|
-
// the referenced record — the hop editors reach for most.
|
|
288
|
-
const hopId = field.type === 'reference' ? (_field_reference = field.reference) == null ? void 0 : _field_reference.displayFieldId : undefined;
|
|
289
|
-
if (hopId) {
|
|
290
|
-
entries.push({
|
|
291
|
-
token: datasetItemToken(fieldId, hopId),
|
|
292
|
-
label: `${label} → ${humanizeDatasetFieldId(hopId)}`,
|
|
293
|
-
description: 'Field from the referenced record.'
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
return entries;
|
|
298
|
-
}
|
|
299
275
|
|
|
300
276
|
//# sourceMappingURL=binding-token-catalog.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/binding-token-catalog.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n DATASET_FIELD_TYPE_LABELS,\n type DatasetModel,\n} from './dataset-models'\nimport { humanizeDatasetFieldId } from './datasets'\n\n/**\n * Browsable data-placeholder catalogs for the designer's insert picker\n * (AGL-583). Hand-typing `{{entry.title}}` stays the advanced path; these\n * catalogs give every token a friendly label + description so editors can\n * browse and insert instead of memorizing the grammar.\n *\n * The token STRINGS are owned elsewhere — `collectionEntryTokens`\n * (collection-entries.ts) and the tenant compose pipeline resolve them —\n * this module only names them for pickers. The spec cross-checks the entry\n * catalog against the resolver so the two can never drift.\n */\nexport interface BindingTokenCatalogEntry {\n /** The literal token inserted into the prop, e.g. `{{entry.title}}`. */\n token: string\n /** Friendly picker label, e.g. `Title`. */\n label: string\n /** One-line description shown as the option's secondary text. */\n description?: string\n}\n\n/**\n * `{{entry.*}}` tokens (AGL-105/551/582) — resolve per entry inside a\n * Collection entries block and page-wide on entry-template screens.\n * Mirrors {@link collectionEntryTokens}; the spec enforces the mirror.\n */\nexport const ENTRY_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n { token: '{{entry.title}}', label: 'Title', description: 'The entry headline.' },\n {\n token: '{{entry.excerpt}}',\n label: 'Excerpt',\n description: 'Short summary text.',\n },\n {\n token: '{{entry.body}}',\n label: 'Body',\n description: 'The full entry body.',\n },\n {\n token: '{{entry.url}}',\n label: 'Link URL',\n description: 'Auto-route to the entry page.',\n },\n {\n token: '{{entry.date}}',\n label: 'Published date',\n description: 'Formatted publish date.',\n },\n /*\n Beside the readable date on purpose: a designer reaching for \"the date\" in\n a Video element's Publication date field has to see that the readable one\n is the wrong pick there, and why.\n */\n {\n token: '{{entry.publishedAt}}',\n label: 'Publish timestamp',\n description:\n 'The publish date and time in ISO 8601, for a field that needs a ' +\n 'machine-readable date, such as a Video element’s publication date.',\n },\n {\n token: '{{entry.author}}',\n label: 'Author',\n description: 'The byline set on the entry.',\n },\n {\n token: '{{entry.authorBio}}',\n label: 'Author bio',\n description: 'Blurb from the author’s record.',\n },\n {\n token: '{{entry.authorImage}}',\n label: 'Author portrait',\n description: 'Portrait or logo from the author’s record.',\n },\n {\n token: '{{entry.authorUrl}}',\n label: 'Author link',\n description: 'The author’s own page.',\n },\n {\n token: '{{entry.authorPageUrl}}',\n label: 'Author page',\n description:\n 'This author’s page on this site — everything they wrote, across every ' +\n 'collection. Separate from Author link, which is their own site.',\n },\n {\n token: '{{entry.slug}}',\n label: 'Slug',\n description: 'URL-safe entry identifier.',\n },\n /*\n Named \"Its collection…\" rather than \"Collection…\", which is what these\n describe and also exactly what the `{{collection.*}}` entries below are\n called. The picker groups options by heading, so the two sets sit apart —\n but a designer scanning it reads the LABEL, and two options reading\n \"Collection name\" three lines apart is a choice made by guessing. The\n pronoun is doing real work: on the one page where both resolve, they mean\n different collections.\n */\n {\n token: '{{entry.collection}}',\n label: 'Its collection',\n description:\n 'Which section this entry belongs to. Worth binding on a listing that ' +\n 'MIXES collections — an author page — where a card otherwise cannot ' +\n 'tell a release note from an essay.',\n },\n {\n token: '{{entry.collectionSlug}}',\n label: 'Its collection slug',\n description: 'That collection’s URL segment.',\n },\n {\n token: '{{entry.collectionUrl}}',\n label: 'Its collection link',\n description: 'The listing this entry belongs to.',\n },\n {\n token: '{{entry.coverImage}}',\n label: 'Cover image',\n description: 'Cover image URL.',\n },\n {\n token: '{{entry.coverVideo}}',\n label: 'Featured video',\n description:\n 'The featured video’s source — a library film, a video link or a ' +\n 'Wistia link — for a Video element.',\n },\n {\n token: '{{entry.category}}',\n label: 'Category',\n description: 'The entry category.',\n },\n {\n token: '{{entry.tags}}',\n label: 'Tags',\n description: 'Tags, comma separated.',\n },\n {\n token: '{{entry.seoTitle}}',\n label: 'SEO title',\n description: 'Search title; falls back to Title.',\n },\n {\n token: '{{entry.seoDescription}}',\n label: 'SEO description',\n description: 'Meta description; falls back to Excerpt.',\n },\n]\n\n/**\n * `{{collection.*}}` and `{{pagination.*}}` tokens (AGL-551/1321/1386) —\n * resolve on collection list/entry template screens (see the tenant compose\n * pipeline's collection tokens).\n *\n * The category and pagination tokens all resolve to the empty string where\n * they do not apply, so they are safe to bind unconditionally: ONE list\n * screen serves the bare listing, every `/page/{n}` and every\n * `/category/{slug}`, and a template has no runtime conditional to vary\n * itself with.\n */\nexport const COLLECTION_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n {\n token: '{{collection.name}}',\n label: 'Collection name',\n description: 'Display name of the routed collection.',\n },\n {\n token: '{{collection.slug}}',\n label: 'Collection slug',\n description: 'URL slug of the routed collection.',\n },\n {\n token: '{{collection.category}}',\n label: 'Filtered category',\n description: 'Category the URL filtered on; empty when unfiltered.',\n },\n {\n token: '{{collection.categorySlug}}',\n label: 'Filtered category slug',\n description: 'That category’s URL segment; empty when unfiltered.',\n },\n {\n token: '{{pagination.page}}',\n label: 'Current page',\n description: 'Page number this URL is showing.',\n },\n {\n token: '{{pagination.totalPages}}',\n label: 'Total pages',\n // A listing bigger than one read cannot be counted honestly (AGL-3219):\n // the count and the entries were read at two different moments, and the\n // pager built from them disagreed with itself at the page boundary. It\n // still resolves, so a template binding it keeps rendering.\n description:\n 'Pages in the listing, after any category filter. Empty on a listing too large to count — bind the next link instead.',\n },\n {\n token: '{{pagination.prevUrl}}',\n label: 'Previous page link',\n description: 'Keeps the category; empty on the first page.',\n },\n {\n token: '{{pagination.nextUrl}}',\n label: 'Next page link',\n // The reliable \"is there more\" (AGL-3219): it is set from a read that\n // asked for one entry more than the page needed, so empty means there was\n // no such entry rather than that a total said so.\n description:\n 'Keeps the category; empty when there is nothing older to show.',\n },\n]\n\n/**\n * `{{author.*}}` tokens (AGL-2518) — resolve on an author's own page,\n * `/author/{slug}`.\n *\n * Empty everywhere else, like the category tokens above and for the same\n * reason: a template has no runtime conditional, so the tokens have to be the\n * thing that varies. A heading bound to `{{author.name}}` prints a name on an\n * author page and nothing anywhere else.\n *\n * The `{{pagination.*}}` tokens resolve HERE TOO, over this author's own\n * archive — deliberately the same four names a collection listing uses, so a\n * pager built once works on both.\n */\nexport const AUTHOR_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n {\n token: '{{author.name}}',\n label: 'Name',\n description: 'The byline this page collects.',\n },\n {\n token: '{{author.bio}}',\n label: 'Bio',\n description: 'Their blurb, from the author record.',\n },\n {\n token: '{{author.image}}',\n label: 'Portrait',\n description: 'Their portrait or logo, from the author record.',\n },\n {\n token: '{{author.jobTitle}}',\n label: 'Role',\n description: 'Their job title; empty for an Organization author.',\n },\n {\n token: '{{author.worksFor}}',\n label: 'Organization',\n description: 'Who they write for; empty for an Organization author.',\n },\n {\n token: '{{author.url}}',\n label: 'Their own site',\n description:\n 'The url on their record — a personal site, not this page. Empty when ' +\n 'they have none.',\n },\n {\n token: '{{author.pageUrl}}',\n label: 'This page',\n description: 'The canonical address of the page you are designing.',\n },\n {\n token: '{{author.entryCount}}',\n label: 'Post count',\n description: 'How many entries they have published, across every collection.',\n },\n {\n token: '{{author.entryCountLabel}}',\n label: 'Post count, worded',\n description:\n '\"1 post\" or \"12 posts\" — pluralized for you, because a template has no ' +\n 'conditional to do it with.',\n },\n]\n\n/**\n * Renders an `{{item.*}}` token for a dataset field, optionally hopping\n * one reference to a field of the referenced record (AGL-180). Always\n * takes the stable model field ID — display names are labels only.\n */\nexport function datasetItemToken(\n fieldId: string,\n targetFieldId?: string,\n): string {\n return targetFieldId\n ? `{{item.${fieldId}.${targetFieldId}}}`\n : `{{item.${fieldId}}}`\n}\n\n/**\n * Dataset-item token entries for a repeatable container's model: one per\n * field in model order, labeled with the CURRENT display name while the\n * token carries the stable reference id (AGL-578 — renaming a label never\n * breaks bindings). Reference fields with a configured display field also\n * get their one-hop token (`{{item.author.name}}`).\n */\nexport function datasetItemTokens(\n model: DatasetModel,\n): BindingTokenCatalogEntry[] {\n const entries: BindingTokenCatalogEntry[] = []\n for (const fieldId of model.order ?? []) {\n const field = model.fields?.[fieldId]\n if (!field) continue\n const label = field.name?.trim() || humanizeDatasetFieldId(fieldId)\n entries.push({\n token: datasetItemToken(fieldId),\n label,\n description: DATASET_FIELD_TYPE_LABELS[field.type] ?? field.type,\n })\n // One reference hop (AGL-180): surface the configured display field of\n // the referenced record — the hop editors reach for most.\n const hopId =\n field.type === 'reference' ? field.reference?.displayFieldId : undefined\n if (hopId) {\n entries.push({\n token: datasetItemToken(fieldId, hopId),\n label: `${label} → ${humanizeDatasetFieldId(hopId)}`,\n description: 'Field from the referenced record.',\n })\n }\n }\n return entries\n}\n"],"names":["DATASET_FIELD_TYPE_LABELS","humanizeDatasetFieldId","ENTRY_TOKEN_CATALOG","token","label","description","COLLECTION_TOKEN_CATALOG","AUTHOR_TOKEN_CATALOG","datasetItemToken","fieldId","targetFieldId","datasetItemTokens","model","entries","order","field","fields","name","trim","push","type","hopId","reference","displayFieldId","undefined"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,yBAAyB,QAEpB,sBAAkB;AACzB,SAASC,sBAAsB,QAAQ,gBAAY;AAsBnD;;;;CAIC,GACD,OAAO,MAAMC,sBAA2D;IACtE;QAAEC,OAAO;QAAmBC,OAAO;QAASC,aAAa;IAAsB;IAC/E;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;;;;EAIA,GACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,qEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,2EACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;;;;;;;;EAQA,GACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,0EACA,wEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,qEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;CACD,CAAA;AAED;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,2BAAgE;IAC3E;QACEH,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACP,wEAAwE;QACxE,wEAAwE;QACxE,uEAAuE;QACvE,4DAA4D;QAC5DC,aACE;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACP,sEAAsE;QACtE,0EAA0E;QAC1E,kDAAkD;QAClDC,aACE;IACJ;CACD,CAAA;AAED;;;;;;;;;;;;CAYC,GACD,OAAO,MAAME,uBAA4D;IACvE;QACEJ,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,0EACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,4EACA;IACJ;CACD,CAAA;AAED;;;;CAIC,GACD,OAAO,SAASG,iBACdC,OAAe,EACfC,aAAsB;IAEtB,OAAOA,gBACH,CAAC,OAAO,EAAED,QAAQ,CAAC,EAAEC,cAAc,EAAE,CAAC,GACtC,CAAC,OAAO,EAAED,QAAQ,EAAE,CAAC;AAC3B;AAEA;;;;;;CAMC,GACD,OAAO,SAASE,kBACdC,KAAmB;QAGGA;IADtB,MAAMC,UAAsC,EAAE;IAC9C,KAAK,MAAMJ,YAAWG,eAAAA,MAAME,KAAK,YAAXF,eAAe,EAAE,CAAE;YAOxBZ;YANDY,eAEAG,aASiBA;QAX/B,MAAMA,SAAQH,gBAAAA,MAAMI,MAAM,qBAAZJ,aAAc,CAACH,QAAQ;QACrC,IAAI,CAACM,OAAO;QACZ,MAAMX,QAAQW,EAAAA,cAAAA,MAAME,IAAI,qBAAVF,YAAYG,IAAI,OAAMjB,uBAAuBQ;QAC3DI,QAAQM,IAAI,CAAC;YACXhB,OAAOK,iBAAiBC;YACxBL;YACAC,WAAW,GAAEL,wCAAAA,yBAAyB,CAACe,MAAMK,IAAI,CAAC,YAArCpB,wCAAyCe,MAAMK,IAAI;QAClE;QACA,uEAAuE;QACvE,0DAA0D;QAC1D,MAAMC,QACJN,MAAMK,IAAI,KAAK,eAAcL,mBAAAA,MAAMO,SAAS,qBAAfP,iBAAiBQ,cAAc,GAAGC;QACjE,IAAIH,OAAO;YACTR,QAAQM,IAAI,CAAC;gBACXhB,OAAOK,iBAAiBC,SAASY;gBACjCjB,OAAO,GAAGA,MAAM,GAAG,EAAEH,uBAAuBoB,QAAQ;gBACpDhB,aAAa;YACf;QACF;IACF;IACA,OAAOQ;AACT"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/binding-token-catalog.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Browsable data-placeholder catalogs for the designer's insert picker\n * (AGL-583). Hand-typing `{{entry.title}}` stays the advanced path; these\n * catalogs give every token a friendly label + description so editors can\n * browse and insert instead of memorizing the grammar.\n *\n * The token STRINGS are owned elsewhere — `collectionEntryTokens`\n * (collection-entries.ts) and the tenant compose pipeline resolve them —\n * this module only names them for pickers. The spec cross-checks the entry\n * catalog against the resolver so the two can never drift.\n */\nexport interface BindingTokenCatalogEntry {\n /** The literal token inserted into the prop, e.g. `{{entry.title}}`. */\n token: string\n /** Friendly picker label, e.g. `Title`. */\n label: string\n /** One-line description shown as the option's secondary text. */\n description?: string\n}\n\n/**\n * `{{entry.*}}` tokens (AGL-105/551/582) — resolve per entry inside a\n * Collection entries block and page-wide on entry-template screens.\n * Mirrors {@link collectionEntryTokens}; the spec enforces the mirror.\n */\nexport const ENTRY_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n { token: '{{entry.title}}', label: 'Title', description: 'The entry headline.' },\n {\n token: '{{entry.excerpt}}',\n label: 'Excerpt',\n description: 'Short summary text.',\n },\n {\n token: '{{entry.body}}',\n label: 'Body',\n description: 'The full entry body.',\n },\n {\n token: '{{entry.url}}',\n label: 'Link URL',\n description: 'Auto-route to the entry page.',\n },\n {\n token: '{{entry.date}}',\n label: 'Published date',\n description: 'Formatted publish date.',\n },\n /*\n Beside the readable date on purpose: a designer reaching for \"the date\" in\n a Video element's Publication date field has to see that the readable one\n is the wrong pick there, and why.\n */\n {\n token: '{{entry.publishedAt}}',\n label: 'Publish timestamp',\n description:\n 'The publish date and time in ISO 8601, for a field that needs a ' +\n 'machine-readable date, such as a Video element’s publication date.',\n },\n {\n token: '{{entry.author}}',\n label: 'Author',\n description: 'The byline set on the entry.',\n },\n {\n token: '{{entry.authorBio}}',\n label: 'Author bio',\n description: 'Blurb from the author’s record.',\n },\n {\n token: '{{entry.authorImage}}',\n label: 'Author portrait',\n description: 'Portrait or logo from the author’s record.',\n },\n {\n token: '{{entry.authorUrl}}',\n label: 'Author link',\n description: 'The author’s own page.',\n },\n {\n token: '{{entry.authorPageUrl}}',\n label: 'Author page',\n description:\n 'This author’s page on this site — everything they wrote, across every ' +\n 'collection. Separate from Author link, which is their own site.',\n },\n {\n token: '{{entry.slug}}',\n label: 'Slug',\n description: 'URL-safe entry identifier.',\n },\n /*\n Named \"Its collection…\" rather than \"Collection…\", which is what these\n describe and also exactly what the `{{collection.*}}` entries below are\n called. The picker groups options by heading, so the two sets sit apart —\n but a designer scanning it reads the LABEL, and two options reading\n \"Collection name\" three lines apart is a choice made by guessing. The\n pronoun is doing real work: on the one page where both resolve, they mean\n different collections.\n */\n {\n token: '{{entry.collection}}',\n label: 'Its collection',\n description:\n 'Which section this entry belongs to. Worth binding on a listing that ' +\n 'MIXES collections — an author page — where a card otherwise cannot ' +\n 'tell a release note from an essay.',\n },\n {\n token: '{{entry.collectionSlug}}',\n label: 'Its collection slug',\n description: 'That collection’s URL segment.',\n },\n {\n token: '{{entry.collectionUrl}}',\n label: 'Its collection link',\n description: 'The listing this entry belongs to.',\n },\n {\n token: '{{entry.coverImage}}',\n label: 'Cover image',\n description: 'Cover image URL.',\n },\n {\n token: '{{entry.coverVideo}}',\n label: 'Featured video',\n description:\n 'The featured video’s source — a library film, a video file link or ' +\n 'a video host’s link — for a Video element.',\n },\n {\n token: '{{entry.category}}',\n label: 'Category',\n description: 'The entry category.',\n },\n {\n token: '{{entry.tags}}',\n label: 'Tags',\n description: 'Tags, comma separated.',\n },\n {\n token: '{{entry.seoTitle}}',\n label: 'SEO title',\n description: 'Search title; falls back to Title.',\n },\n {\n token: '{{entry.seoDescription}}',\n label: 'SEO description',\n description: 'Meta description; falls back to Excerpt.',\n },\n]\n\n/**\n * `{{collection.*}}` and `{{pagination.*}}` tokens (AGL-551/1321/1386) —\n * resolve on collection list/entry template screens (see the tenant compose\n * pipeline's collection tokens).\n *\n * The category and pagination tokens all resolve to the empty string where\n * they do not apply, so they are safe to bind unconditionally: ONE list\n * screen serves the bare listing, every `/page/{n}` and every\n * `/category/{slug}`, and a template has no runtime conditional to vary\n * itself with.\n */\nexport const COLLECTION_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n {\n token: '{{collection.name}}',\n label: 'Collection name',\n description: 'Display name of the routed collection.',\n },\n {\n token: '{{collection.slug}}',\n label: 'Collection slug',\n description: 'URL slug of the routed collection.',\n },\n {\n token: '{{collection.category}}',\n label: 'Filtered category',\n description: 'Category the URL filtered on; empty when unfiltered.',\n },\n {\n token: '{{collection.categorySlug}}',\n label: 'Filtered category slug',\n description: 'That category’s URL segment; empty when unfiltered.',\n },\n {\n token: '{{pagination.page}}',\n label: 'Current page',\n description: 'Page number this URL is showing.',\n },\n {\n token: '{{pagination.totalPages}}',\n label: 'Total pages',\n // A listing bigger than one read cannot be counted honestly (AGL-3219):\n // the count and the entries were read at two different moments, and the\n // pager built from them disagreed with itself at the page boundary. It\n // still resolves, so a template binding it keeps rendering.\n description:\n 'Pages in the listing, after any category filter. Empty on a listing too large to count — bind the next link instead.',\n },\n {\n token: '{{pagination.prevUrl}}',\n label: 'Previous page link',\n description: 'Keeps the category; empty on the first page.',\n },\n {\n token: '{{pagination.nextUrl}}',\n label: 'Next page link',\n // The reliable \"is there more\" (AGL-3219): it is set from a read that\n // asked for one entry more than the page needed, so empty means there was\n // no such entry rather than that a total said so.\n description:\n 'Keeps the category; empty when there is nothing older to show.',\n },\n]\n\n/**\n * `{{author.*}}` tokens (AGL-2518) — resolve on an author's own page,\n * `/author/{slug}`.\n *\n * Empty everywhere else, like the category tokens above and for the same\n * reason: a template has no runtime conditional, so the tokens have to be the\n * thing that varies. A heading bound to `{{author.name}}` prints a name on an\n * author page and nothing anywhere else.\n *\n * The `{{pagination.*}}` tokens resolve HERE TOO, over this author's own\n * archive — deliberately the same four names a collection listing uses, so a\n * pager built once works on both.\n */\nexport const AUTHOR_TOKEN_CATALOG: readonly BindingTokenCatalogEntry[] = [\n {\n token: '{{author.name}}',\n label: 'Name',\n description: 'The byline this page collects.',\n },\n {\n token: '{{author.bio}}',\n label: 'Bio',\n description: 'Their blurb, from the author record.',\n },\n {\n token: '{{author.image}}',\n label: 'Portrait',\n description: 'Their portrait or logo, from the author record.',\n },\n {\n token: '{{author.jobTitle}}',\n label: 'Role',\n description: 'Their job title; empty for an Organization author.',\n },\n {\n token: '{{author.worksFor}}',\n label: 'Organization',\n description: 'Who they write for; empty for an Organization author.',\n },\n {\n token: '{{author.url}}',\n label: 'Their own site',\n description:\n 'The url on their record — a personal site, not this page. Empty when ' +\n 'they have none.',\n },\n {\n token: '{{author.pageUrl}}',\n label: 'This page',\n description: 'The canonical address of the page you are designing.',\n },\n {\n token: '{{author.entryCount}}',\n label: 'Post count',\n description: 'How many entries they have published, across every collection.',\n },\n {\n token: '{{author.entryCountLabel}}',\n label: 'Post count, worded',\n description:\n '\"1 post\" or \"12 posts\" — pluralized for you, because a template has no ' +\n 'conditional to do it with.',\n },\n]\n\n/**\n * The `{{item.*}}` token for one field of the record a repeat is rendering,\n * optionally hopping one reference to a field of the referenced record\n * (AGL-180). Always takes the stable field id — display names are labels\n * only, so renaming a field never breaks a binding (AGL-578).\n */\nexport function repeatItemToken(\n fieldId: string,\n targetFieldId?: string,\n): string {\n return targetFieldId\n ? `{{item.${fieldId}.${targetFieldId}}}`\n : `{{item.${fieldId}}}`\n}\n"],"names":["ENTRY_TOKEN_CATALOG","token","label","description","COLLECTION_TOKEN_CATALOG","AUTHOR_TOKEN_CATALOG","repeatItemToken","fieldId","targetFieldId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;CAUC,GAUD;;;;CAIC,GACD,OAAO,MAAMA,sBAA2D;IACtE;QAAEC,OAAO;QAAmBC,OAAO;QAASC,aAAa;IAAsB;IAC/E;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;;;;EAIA,GACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,qEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,2EACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;;;;;;;;EAQA,GACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,0EACA,wEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,wEACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;CACD,CAAA;AAED;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,2BAAgE;IAC3E;QACEH,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACP,wEAAwE;QACxE,wEAAwE;QACxE,uEAAuE;QACvE,4DAA4D;QAC5DC,aACE;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACP,sEAAsE;QACtE,0EAA0E;QAC1E,kDAAkD;QAClDC,aACE;IACJ;CACD,CAAA;AAED;;;;;;;;;;;;CAYC,GACD,OAAO,MAAME,uBAA4D;IACvE;QACEJ,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,0EACA;IACJ;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aAAa;IACf;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,aACE,4EACA;IACJ;CACD,CAAA;AAED;;;;;CAKC,GACD,OAAO,SAASG,gBACdC,OAAe,EACfC,aAAsB;IAEtB,OAAOA,gBACH,CAAC,OAAO,EAAED,QAAQ,CAAC,EAAEC,cAAc,EAAE,CAAC,GACtC,CAAC,OAAO,EAAED,QAAQ,EAAE,CAAC;AAC3B"}
|
|
@@ -49,7 +49,7 @@ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_wi
|
|
|
49
49
|
*/ export function keyById(docs) {
|
|
50
50
|
const map = {};
|
|
51
51
|
for (const _ref of docs){
|
|
52
|
-
|
|
52
|
+
const { id } = _ref, doc = _object_without_properties_loose(_ref, [
|
|
53
53
|
"id"
|
|
54
54
|
]);
|
|
55
55
|
map[id] = doc;
|