@vxil/feature-configs 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,18 +1,49 @@
1
1
  import { type Static, type TSchema } from '@sinclair/typebox';
2
2
  export * from './hooks.js';
3
3
  export * from './readmodels.js';
4
+ export declare const RESERVED_CREDIT_TYPES: ReadonlySet<string>;
5
+ /** True when a credit_type is a vxil-COGS reserved type (grant/consume/config
6
+ * of which is restricted to internal platform machinery). */
7
+ export declare function isReservedCreditType(creditType: string): boolean;
8
+ export declare const NOTIFICATION_TEMPLATE_PLACEHOLDERS: Readonly<Record<string, readonly string[]>>;
9
+ /** Per-override caps (D4). Bodies are emails, not documents. */
10
+ export declare const NOTIF_OVERRIDE_SUBJECT_MAX = 500;
11
+ export declare const NOTIF_OVERRIDE_BODY_MAX = 20000;
12
+ /** Whole-map caps: keeps one manifest (and the KV row every send reads) small. */
13
+ export declare const NOTIF_OVERRIDE_MAX_LOCALES_PER_TEMPLATE = 20;
14
+ export declare const NOTIF_OVERRIDE_MAX_BYTES: number;
15
+ /**
16
+ * D4 — validate `templates.overrides` at CONFIG-WRITE time. Returns [] when the
17
+ * bag is absent or clean. Every rule fails LOUD rather than shipping something
18
+ * that silently renders wrong in a customer's inbox:
19
+ * • overrides present with `allowOverride:false` → rejected (never inert);
20
+ * • an unknown template id / locale tag → rejected;
21
+ * • an unknown `{{placeholder}}` in subject/html/text → rejected (the class of
22
+ * typo that renders as an empty string in a live email);
23
+ * • size caps (per-locale count, whole-map bytes) → rejected.
24
+ * Pure, so it is unit-tested without a control plane.
25
+ */
26
+ export declare function validateNotificationOverrides(templates: {
27
+ allowOverride?: boolean;
28
+ overrides?: Record<string, Record<string, {
29
+ subject?: string;
30
+ html?: string;
31
+ text?: string;
32
+ }>>;
33
+ } | undefined): string[];
4
34
  export declare const NotificationsConfigSchema: import("@sinclair/typebox").TObject<{
5
35
  enabled: import("@sinclair/typebox").TBoolean;
6
36
  fromEmail: import("@sinclair/typebox").TString;
7
37
  fromName: import("@sinclair/typebox").TString;
8
38
  replyTo: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
9
39
  resendApiKeyRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
40
+ webhookSecretRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
10
41
  provider: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"resend">, import("@sinclair/typebox").TLiteral<"mock">]>;
11
42
  defaultLocale: import("@sinclair/typebox").TString;
12
- retry: import("@sinclair/typebox").TObject<{
43
+ retry: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
13
44
  maxAttempts: import("@sinclair/typebox").TInteger;
14
45
  backoff: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"exponential">, import("@sinclair/typebox").TLiteral<"linear">]>;
15
- }>;
46
+ }>>;
16
47
  suppression: import("@sinclair/typebox").TObject<{
17
48
  softBounceThreshold: import("@sinclair/typebox").TInteger;
18
49
  }>;
@@ -20,13 +51,41 @@ export declare const NotificationsConfigSchema: import("@sinclair/typebox").TObj
20
51
  perDay: import("@sinclair/typebox").TInteger;
21
52
  perTenantSec: import("@sinclair/typebox").TInteger;
22
53
  }>;
23
- templates: import("@sinclair/typebox").TObject<{
54
+ templates: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
24
55
  allowOverride: import("@sinclair/typebox").TBoolean;
25
- }>;
56
+ overrides: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TObject<{
57
+ subject: import("@sinclair/typebox").TString;
58
+ html: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
59
+ text: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
60
+ }>>>>;
61
+ }>>;
26
62
  /** in-app inbox channel (send with channel: 'inbox' | 'both') */
27
63
  inboxEnabled: import("@sinclair/typebox").TBoolean;
64
+ /** §11b.5 broadcast campaigns channel. Optional bag (= 1 leaf): absent means
65
+ * disabled; per-campaign quiet_hours / freq_cap overrides live on the
66
+ * notifications.campaigns ROW (tenant data), not here. Folded into the
67
+ * canonical schema 2026-07-18 (M21/#4 — Value.Clean previously STRIPPED the
68
+ * worker-local extension, so campaigns 403'd via the real config path). */
69
+ broadcast: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
70
+ enabled: import("@sinclair/typebox").TBoolean;
71
+ /** tenant-default per-recipient quiet window (defer-not-drop); a
72
+ * per-campaign quiet_hours wins over it. Mirrors the campaigns-row shape
73
+ * (workers/notifications-v1 CampaignBody.quiet_hours). start/end are
74
+ * pattern-pinned to what the worker's parseHhMm actually parses — a
75
+ * looser string ('10pm') would validate but FAIL OPEN at runtime
76
+ * (quiet window silently ignored). */
77
+ defaultQuietHours: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
78
+ tz: import("@sinclair/typebox").TString;
79
+ start: import("@sinclair/typebox").TString;
80
+ end: import("@sinclair/typebox").TString;
81
+ }>>;
82
+ /** default rolling per-user-per-day campaign-send cap across campaigns */
83
+ freqCapPerUserPerDay: import("@sinclair/typebox").TInteger;
84
+ }>>;
28
85
  }>;
29
86
  export type NotificationsConfig = Static<typeof NotificationsConfigSchema>;
87
+ /** The §11b.5 broadcast bag as persisted (present ⇒ leaf defaults applied). */
88
+ export type BroadcastConfig = NonNullable<NotificationsConfig['broadcast']>;
30
89
  export declare const JobsConfigSchema: import("@sinclair/typebox").TObject<{
31
90
  enabled: import("@sinclair/typebox").TBoolean;
32
91
  retry: import("@sinclair/typebox").TObject<{
@@ -72,16 +131,29 @@ export declare const JobsConfigSchema: import("@sinclair/typebox").TObject<{
72
131
  }>;
73
132
  }>;
74
133
  export type JobsConfig = Static<typeof JobsConfigSchema>;
134
+ declare const OidcProviderSchema: import("@sinclair/typebox").TObject<{
135
+ issuer: import("@sinclair/typebox").TString;
136
+ clientId: import("@sinclair/typebox").TString;
137
+ clientSecretRef: import("@sinclair/typebox").TString;
138
+ scopes: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
139
+ claims: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
140
+ email: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
141
+ name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
142
+ roles: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
143
+ }>>;
144
+ allowedDomains: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
145
+ autoLink: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
146
+ }>;
75
147
  export declare const AuthConfigSchema: import("@sinclair/typebox").TObject<{
76
148
  enabled: import("@sinclair/typebox").TBoolean;
77
- methods: import("@sinclair/typebox").TObject<{
149
+ methods: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
78
150
  emailPassword: import("@sinclair/typebox").TBoolean;
79
151
  magicLink: import("@sinclair/typebox").TBoolean;
80
152
  google: import("@sinclair/typebox").TBoolean;
81
153
  github: import("@sinclair/typebox").TBoolean;
82
154
  apple: import("@sinclair/typebox").TBoolean;
83
155
  facebook: import("@sinclair/typebox").TBoolean;
84
- }>;
156
+ }>>;
85
157
  providers: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
86
158
  google: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
87
159
  clientIdRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
@@ -105,10 +177,24 @@ export declare const AuthConfigSchema: import("@sinclair/typebox").TObject<{
105
177
  clientSecretRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
106
178
  audRefs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
107
179
  }>>;
180
+ oidc: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
181
+ issuer: import("@sinclair/typebox").TString;
182
+ clientId: import("@sinclair/typebox").TString;
183
+ clientSecretRef: import("@sinclair/typebox").TString;
184
+ scopes: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
185
+ claims: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
186
+ email: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
187
+ name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
188
+ roles: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
189
+ }>>;
190
+ allowedDomains: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
191
+ autoLink: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
192
+ }>>;
108
193
  }>>;
109
194
  session: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
110
195
  ttlMinutes: import("@sinclair/typebox").TInteger;
111
196
  refreshTtlDays: import("@sinclair/typebox").TInteger;
197
+ maxConcurrent: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
112
198
  }>>;
113
199
  password: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
114
200
  minLength: import("@sinclair/typebox").TInteger;
@@ -125,6 +211,7 @@ export declare const AuthConfigSchema: import("@sinclair/typebox").TObject<{
125
211
  codeTtlMinutes: import("@sinclair/typebox").TInteger;
126
212
  maxAttempts: import("@sinclair/typebox").TInteger;
127
213
  resendCooldownSec: import("@sinclair/typebox").TInteger;
214
+ testRecipients: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
128
215
  }>>;
129
216
  anonymous: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
130
217
  enabled: import("@sinclair/typebox").TBoolean;
@@ -132,7 +219,21 @@ export declare const AuthConfigSchema: import("@sinclair/typebox").TObject<{
132
219
  orgClaims: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
133
220
  enabled: import("@sinclair/typebox").TBoolean;
134
221
  }>>;
222
+ security: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
223
+ lockout: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
224
+ maxFailures: import("@sinclair/typebox").TInteger;
225
+ windowMinutes: import("@sinclair/typebox").TInteger;
226
+ lockMinutes: import("@sinclair/typebox").TInteger;
227
+ }>>;
228
+ breachedPasswords: import("@sinclair/typebox").TBoolean;
229
+ captchaSecretRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
230
+ allowedRedirectOrigins: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
231
+ }>>;
135
232
  }>;
233
+ /** The security bag as persisted (present ⇒ leaf defaults applied). */
234
+ export type AuthSecurityConfig = NonNullable<Static<typeof AuthConfigSchema>['security']>;
235
+ /** The RB-2 generic OIDC / SSO issuer block (`providers.oidc`). */
236
+ export type AuthOidcConfig = Static<typeof OidcProviderSchema>;
136
237
  export type AuthConfig = Static<typeof AuthConfigSchema>;
137
238
  export declare const RateLimitsConfigSchema: import("@sinclair/typebox").TObject<{
138
239
  enabled: import("@sinclair/typebox").TBoolean;
@@ -144,7 +245,6 @@ export declare const RateLimitsConfigSchema: import("@sinclair/typebox").TObject
144
245
  export type RateLimitsConfig = Static<typeof RateLimitsConfigSchema>;
145
246
  export declare const FilesConfigSchema: import("@sinclair/typebox").TObject<{
146
247
  enabled: import("@sinclair/typebox").TBoolean;
147
- bucketRef: import("@sinclair/typebox").TString;
148
248
  uploadUrlTtl: import("@sinclair/typebox").TInteger;
149
249
  downloadUrlTtl: import("@sinclair/typebox").TInteger;
150
250
  quotas: import("@sinclair/typebox").TObject<{
@@ -153,10 +253,6 @@ export declare const FilesConfigSchema: import("@sinclair/typebox").TObject<{
153
253
  maxObjectCount: import("@sinclair/typebox").TInteger;
154
254
  }>;
155
255
  allowedContentTypes: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
156
- contentScan: import("@sinclair/typebox").TObject<{
157
- enabled: import("@sinclair/typebox").TBoolean;
158
- quarantineOnFail: import("@sinclair/typebox").TBoolean;
159
- }>;
160
256
  sharedLinks: import("@sinclair/typebox").TObject<{
161
257
  enabled: import("@sinclair/typebox").TBoolean;
162
258
  maxTtl: import("@sinclair/typebox").TInteger;
@@ -240,9 +336,6 @@ export declare const McpConfigSchema: import("@sinclair/typebox").TObject<{
240
336
  enabled: import("@sinclair/typebox").TBoolean;
241
337
  exposureLevel: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"all">, import("@sinclair/typebox").TLiteral<"read-only">, import("@sinclair/typebox").TLiteral<"custom">]>;
242
338
  allowToolList: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
243
- scopedKey: import("@sinclair/typebox").TObject<{
244
- perAgentKeys: import("@sinclair/typebox").TBoolean;
245
- }>;
246
339
  rateLimits: import("@sinclair/typebox").TObject<{
247
340
  toolCallsPerMin: import("@sinclair/typebox").TInteger;
248
341
  }>;
@@ -300,9 +393,6 @@ export declare const ActivityFeedConfigSchema: import("@sinclair/typebox").TObje
300
393
  copyLimit: import("@sinclair/typebox").TInteger;
301
394
  maxFollowing: import("@sinclair/typebox").TInteger;
302
395
  }>;
303
- aggregation: import("@sinclair/typebox").TObject<{
304
- maxGroupActivities: import("@sinclair/typebox").TInteger;
305
- }>;
306
396
  realtime: import("@sinclair/typebox").TObject<{
307
397
  enabled: import("@sinclair/typebox").TBoolean;
308
398
  }>;
@@ -310,9 +400,6 @@ export declare const ActivityFeedConfigSchema: import("@sinclair/typebox").TObje
310
400
  enabled: import("@sinclair/typebox").TBoolean;
311
401
  digestCadence: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"off">, import("@sinclair/typebox").TLiteral<"hourly">, import("@sinclair/typebox").TLiteral<"daily">]>;
312
402
  }>;
313
- rateLimit: import("@sinclair/typebox").TObject<{
314
- addPerSec: import("@sinclair/typebox").TInteger;
315
- }>;
316
403
  }>;
317
404
  export type ActivityFeedConfig = Static<typeof ActivityFeedConfigSchema>;
318
405
  export declare const VectorSearchConfigSchema: import("@sinclair/typebox").TObject<{
@@ -430,6 +517,7 @@ export declare const PaymentsConfigSchema: import("@sinclair/typebox").TObject<{
430
517
  publicSdkKey: import("@sinclair/typebox").TString;
431
518
  secretApiKeyRef: import("@sinclair/typebox").TString;
432
519
  webhookSecretRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
520
+ acceptSandbox: import("@sinclair/typebox").TBoolean;
433
521
  }>>;
434
522
  paypal: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
435
523
  clientIdRef: import("@sinclair/typebox").TString;
@@ -438,9 +526,6 @@ export declare const PaymentsConfigSchema: import("@sinclair/typebox").TObject<{
438
526
  sandbox: import("@sinclair/typebox").TBoolean;
439
527
  }>>;
440
528
  returnUrl: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
441
- prices: import("@sinclair/typebox").TObject<{
442
- catalogRef: import("@sinclair/typebox").TString;
443
- }>;
444
529
  defaults: import("@sinclair/typebox").TObject<{
445
530
  currency: import("@sinclair/typebox").TString;
446
531
  trialDays: import("@sinclair/typebox").TInteger;
@@ -463,19 +548,19 @@ export declare const PaymentsConfigSchema: import("@sinclair/typebox").TObject<{
463
548
  }>>;
464
549
  priceMap: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>>;
465
550
  autoRefundOnJobFailure: import("@sinclair/typebox").TBoolean;
551
+ grace: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
552
+ pastDueDays: import("@sinclair/typebox").TInteger;
553
+ }>>;
554
+ enforcePeriodEnd: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
555
+ slackHours: import("@sinclair/typebox").TInteger;
556
+ }>>;
466
557
  }>>;
467
- webhooks: import("@sinclair/typebox").TObject<{
468
- forwardToTenantUrl: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
469
- }>;
470
558
  }>;
471
559
  export type PaymentsConfig = Static<typeof PaymentsConfigSchema>;
472
560
  export declare const FunctionsConfigSchema: import("@sinclair/typebox").TObject<{
473
561
  enabled: import("@sinclair/typebox").TBoolean;
474
- runtime: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"isolate">, import("@sinclair/typebox").TLiteral<"container">]>;
475
562
  defaultLimits: import("@sinclair/typebox").TObject<{
476
563
  cpuMs: import("@sinclair/typebox").TInteger;
477
- timeoutMs: import("@sinclair/typebox").TInteger;
478
- memoryMb: import("@sinclair/typebox").TInteger;
479
564
  }>;
480
565
  metering: import("@sinclair/typebox").TObject<{
481
566
  enabled: import("@sinclair/typebox").TBoolean;
@@ -539,14 +624,8 @@ export declare const CopilotConfigSchema: import("@sinclair/typebox").TObject<{
539
624
  guestToolAllow: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
540
625
  }>>;
541
626
  }>>;
542
- escalation: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
543
- enabled: import("@sinclair/typebox").TBoolean;
544
- handler: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"comments">, import("@sinclair/typebox").TLiteral<"notifications">]>;
545
- notifyTemplate: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
546
- }>>;
547
627
  limits: import("@sinclair/typebox").TObject<{
548
628
  consumeCredits: import("@sinclair/typebox").TBoolean;
549
- tokensPerUserPerDay: import("@sinclair/typebox").TInteger;
550
629
  }>;
551
630
  widget: import("@sinclair/typebox").TObject<{
552
631
  enabled: import("@sinclair/typebox").TBoolean;
@@ -562,6 +641,21 @@ export declare const CONFIG_FLAG_CAP = 15;
562
641
  * Per features/auth.md §4: an OPTIONAL object (e.g. a provider credential
563
642
  * block) counts as ONE flag — the tenant's decision is "configure it or
564
643
  * not", not each inner ref. */
644
+ /** The auth lifecycle events an `authHook` binding may name (F4-30) — the
645
+ * closed union `packages/config` types as AuthHookEvent; the control-plane
646
+ * reconciler maps each to its `auth.<event>` audit-event prefix. */
647
+ export declare const AUTH_HOOK_EVENTS: readonly ["user.created", "session.created", "session.revoked", "signin.failure"];
648
+ export type AuthHookEvent = (typeof AUTH_HOOK_EVENTS)[number];
649
+ /** F4-29 test-recipient entry grammar (shared by the validator and auth-v1's
650
+ * matcher): an exact email, a `*@domain` glob, or a +E.164 phone number. */
651
+ export declare const TEST_RECIPIENT_EMAIL_RE: RegExp;
652
+ export declare const TEST_RECIPIENT_GLOB_RE: RegExp;
653
+ export declare const TEST_RECIPIENT_E164_RE: RegExp;
654
+ export declare function isTestRecipientPattern(entry: string): boolean;
655
+ /** True iff `identifier` (an email today) matches one configured entry:
656
+ * exact (case-insensitive) or the `*@domain` glob. +E.164 entries never match
657
+ * an email. */
658
+ export declare function matchesTestRecipient(identifier: string, entries: readonly string[] | undefined): boolean;
565
659
  export declare function countLeaves(schema: TSchema): number;
566
660
  export interface ConfigValidation {
567
661
  ok: boolean;
@@ -585,3 +679,19 @@ export declare const MCP_MAX_PROMPTS = 32;
585
679
  * validation below and the control-plane deploy clamp. */
586
680
  export declare const DENY_FUNCTION_SCOPES: Set<string>;
587
681
  export declare function validateFeatureConfig(feature: string, raw: unknown): ConfigValidation;
682
+ export declare const FEATURE_KEYS: readonly ["notifications", "jobs", "auth", "rate-limits", "files", "webhooks", "comments", "cms", "mcp", "realtime", "presence", "orgs", "activity-feed", "vector-search", "ai", "rag", "payments", "functions", "copilot"];
683
+ export type FeatureKey = (typeof FEATURE_KEYS)[number];
684
+ /** The hand-authored "when to use" intent a machine schema cannot convey.
685
+ * REQUIRED for every feature (Record key-closure): a feature added to
686
+ * FEATURE_KEYS without a hint is a compile error naming the missing key. */
687
+ export interface PlannerHint {
688
+ /** One or two sentences: when an app needs this feature. */
689
+ whenToUse: string;
690
+ /** Soft priors — words/needs that suggest the feature (NOT a matcher). */
691
+ signals: string[];
692
+ /** When NOT to pick it (disambiguation vs neighbors). */
693
+ notFor: string;
694
+ /** Features that must be enabled alongside (runtime substrate deps). */
695
+ dependsOn: FeatureKey[];
696
+ }
697
+ export declare const FEATURE_HINTS: Record<FeatureKey, PlannerHint>;