primitive-admin 1.1.0-alpha.82 → 1.1.0-alpha.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/src/commands/documents.js +333 -42
  2. package/dist/src/commands/documents.js.map +1 -1
  3. package/dist/src/commands/functions.js +302 -120
  4. package/dist/src/commands/functions.js.map +1 -1
  5. package/dist/src/commands/locks.js +15 -1
  6. package/dist/src/commands/locks.js.map +1 -1
  7. package/dist/src/commands/sync.d.ts +50 -0
  8. package/dist/src/commands/sync.js +560 -312
  9. package/dist/src/commands/sync.js.map +1 -1
  10. package/dist/src/lib/api-client.d.ts +75 -2
  11. package/dist/src/lib/api-client.js +122 -7
  12. package/dist/src/lib/api-client.js.map +1 -1
  13. package/dist/src/lib/config-object-descriptor.js +5 -5
  14. package/dist/src/lib/config-object-descriptor.js.map +1 -1
  15. package/dist/src/lib/document-ingest-artifact.d.ts +120 -0
  16. package/dist/src/lib/document-ingest-artifact.js +505 -0
  17. package/dist/src/lib/document-ingest-artifact.js.map +1 -0
  18. package/dist/src/lib/document-ingest-input.d.ts +51 -0
  19. package/dist/src/lib/document-ingest-input.js +132 -0
  20. package/dist/src/lib/document-ingest-input.js.map +1 -0
  21. package/dist/src/lib/document-ingest.d.ts +78 -0
  22. package/dist/src/lib/document-ingest.js +231 -0
  23. package/dist/src/lib/document-ingest.js.map +1 -0
  24. package/dist/src/lib/function-bundle.d.ts +6 -0
  25. package/dist/src/lib/function-bundle.js +8 -0
  26. package/dist/src/lib/function-bundle.js.map +1 -1
  27. package/dist/src/lib/function-collect.d.ts +1 -1
  28. package/dist/src/lib/function-collect.js +16 -0
  29. package/dist/src/lib/function-collect.js.map +1 -1
  30. package/dist/src/lib/function-db-types.d.ts +4 -0
  31. package/dist/src/lib/function-db-types.js +31 -2
  32. package/dist/src/lib/function-db-types.js.map +1 -1
  33. package/dist/src/lib/function-document-types.d.ts +83 -0
  34. package/dist/src/lib/function-document-types.js +133 -0
  35. package/dist/src/lib/function-document-types.js.map +1 -1
  36. package/dist/src/lib/function-grants-preflight.js +5 -0
  37. package/dist/src/lib/function-grants-preflight.js.map +1 -1
  38. package/dist/src/lib/function-log-lines.d.ts +76 -0
  39. package/dist/src/lib/function-log-lines.js +160 -0
  40. package/dist/src/lib/function-log-lines.js.map +1 -0
  41. package/dist/src/lib/function-log-row.d.ts +29 -0
  42. package/dist/src/lib/function-log-row.js +73 -0
  43. package/dist/src/lib/function-log-row.js.map +1 -0
  44. package/dist/src/lib/function-run.d.ts +53 -35
  45. package/dist/src/lib/function-run.js +55 -44
  46. package/dist/src/lib/function-run.js.map +1 -1
  47. package/dist/src/lib/function-schema-codegen.d.ts +6 -9
  48. package/dist/src/lib/function-schema-codegen.js +22 -35
  49. package/dist/src/lib/function-schema-codegen.js.map +1 -1
  50. package/dist/src/lib/function-sync.d.ts +226 -35
  51. package/dist/src/lib/function-sync.js +731 -99
  52. package/dist/src/lib/function-sync.js.map +1 -1
  53. package/dist/src/lib/function-versions.d.ts +122 -0
  54. package/dist/src/lib/function-versions.js +182 -0
  55. package/dist/src/lib/function-versions.js.map +1 -0
  56. package/dist/src/lib/generated-config-surfaces.d.ts +390 -146
  57. package/dist/src/lib/generated-config-surfaces.js +1207 -401
  58. package/dist/src/lib/generated-config-surfaces.js.map +1 -1
  59. package/dist/src/lib/generated-sdk-types.d.ts +1 -1
  60. package/dist/src/lib/generated-sdk-types.js +1 -1
  61. package/dist/src/lib/generated-sdk-types.js.map +1 -1
  62. package/dist/src/lib/log-inspection.d.ts +2 -0
  63. package/dist/src/lib/log-inspection.js +4 -0
  64. package/dist/src/lib/log-inspection.js.map +1 -1
  65. package/dist/src/lib/output.d.ts +15 -0
  66. package/dist/src/lib/output.js +28 -0
  67. package/dist/src/lib/output.js.map +1 -1
  68. package/dist/src/lib/snapshot-audit-source.d.ts +45 -0
  69. package/dist/src/lib/snapshot-audit-source.js +58 -0
  70. package/dist/src/lib/snapshot-audit-source.js.map +1 -0
  71. package/dist/src/lib/snapshot-audit-store.d.ts +52 -0
  72. package/dist/src/lib/snapshot-audit-store.js +196 -0
  73. package/dist/src/lib/snapshot-audit-store.js.map +1 -0
  74. package/dist/src/lib/snapshot-audit.d.ts +207 -0
  75. package/dist/src/lib/snapshot-audit.js +431 -0
  76. package/dist/src/lib/snapshot-audit.js.map +1 -0
  77. package/dist/src/lib/snapshot-build-rows.d.ts +60 -0
  78. package/dist/src/lib/snapshot-build-rows.js +87 -0
  79. package/dist/src/lib/snapshot-build-rows.js.map +1 -0
  80. package/dist/src/lib/swift-codegen/functionGenerator.d.ts +23 -16
  81. package/dist/src/lib/swift-codegen/functionGenerator.js +34 -33
  82. package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -1
  83. package/dist/src/lib/sync-selectors.d.ts +45 -2
  84. package/dist/src/lib/sync-selectors.js +66 -5
  85. package/dist/src/lib/sync-selectors.js.map +1 -1
  86. package/package.json +2 -2
@@ -110,9 +110,19 @@ export interface ConfigField {
110
110
  * `"src/workflows/config/workflow-field-handlers.ts#normalizeWorkflowStatus"`.
111
111
  * `cli/tests/unit/config-surface-drift-guard.test.ts` fails when the module or
112
112
  * the export is missing (#2644 behavior 2b).
113
+ *
114
+ * `stage` says WHEN the handler runs, with exactly the meaning `RuleStage`
115
+ * gives a cross-field rule (#3375). Until this issue the slot named an owner
116
+ * and never said when, so intent criterion 5 — "a rule declared `preflight`
117
+ * is provably reached before the first mutating call" — could not cover a
118
+ * single-field rule at all. It is REQUIRED, so a new field handler cannot be
119
+ * declared without answering the question, and
120
+ * `findMalformedFieldValidationStages` closes the key set at
121
+ * `handler`/`stage`: the slot stays declarative like the field entry itself.
113
122
  */
114
123
  validation: "passthrough" | {
115
124
  handler: string;
125
+ stage: RuleStage;
116
126
  };
117
127
  /**
118
128
  * This field's declared type genuinely admits more than one TOML spelling
@@ -207,7 +217,18 @@ export interface ConfigTable {
207
217
  * CLI call exists would make the registry promise a refusal that in fact lands
208
218
  * mid-apply.
209
219
  */
210
- export type CrossFieldRuleStage = "preflight" | "apply";
220
+ export type RuleStage = "preflight" | "apply";
221
+ /**
222
+ * The cross-field spelling of `RuleStage`, kept as an exported alias.
223
+ *
224
+ * #3373 named the type for the only slot that had a stage. #3375 gave
225
+ * `ConfigField.validation` one too, with the same meaning, so the canonical
226
+ * name is the general one — and this alias stays because it is part of the
227
+ * registry's published surface (`CrossFieldRule.stage`, the guard, every
228
+ * `crossFieldRulesForStage` caller) and renaming it would be a breaking change
229
+ * to buy a shorter name.
230
+ */
231
+ export type CrossFieldRuleStage = RuleStage;
211
232
  /**
212
233
  * One rule of a configuration object that spans more than one field — `durable
213
234
  * = true` beside a webhook trigger, `runAs = "system"` beside a non-empty
@@ -337,8 +358,15 @@ export declare const UPDATE_ONLY: readonly ("create" | "update")[];
337
358
  * the definition never dispatches through it (#1976 decision of record); the
338
359
  * guard asserts the export exists so a renamed handler fails the CLI unit suite
339
360
  * instead of leaving a dangling reference (#2644 behavior 2b).
361
+ *
362
+ * `stage` is REQUIRED (#3375): a field handler that does not say when it runs
363
+ * makes the same empty promise the cross-field slot made before #3373 — it
364
+ * names an owner, and criterion 5 has nothing to check. A `preflight` handler
365
+ * is reached from `cli/src/commands/sync.ts#runConfigPushPreflight` before the
366
+ * first mutating call, which `findUnreachedPreflightRules` proves statically;
367
+ * an `apply` handler is everything else.
340
368
  */
341
- export declare function handledBy(modulePath: string, exportName: string): ConfigField["validation"];
369
+ export declare function handledBy(modulePath: string, exportName: string, stage: RuleStage): ConfigField["validation"];
342
370
  /**
343
371
  * Name the ONE export that enforces a cross-field rule, as
344
372
  * `"<repo-relative module>#<export>"` — the sibling of `handledBy` for a rule
@@ -739,6 +767,115 @@ export declare function hasDeclaredCrossFieldRules(surface: ConfigObjectSurface)
739
767
  * `[]`: "not declared yet" must never be read as "has no rules" (principle 6).
740
768
  */
741
769
  export declare function crossFieldRulesForStage(surface: ConfigObjectSurface, stage: CrossFieldRuleStage): readonly CrossFieldRule[];
770
+ /**
771
+ * Is a rule declared `preflight` actually reached before `config push` mutates
772
+ * anything? (issue #3375, project phase 3, intent §Success criteria 5.)
773
+ *
774
+ * #3373 gave a rule a `stage`; #3374 declared three surfaces' rules. Both left
775
+ * the string a PROMISE. "This runs before the first mutating call" was
776
+ * satisfied by typing `preflight` beside a rule that is not on the preflight
777
+ * path at all, because the push preflight was an inline block inside the
778
+ * command's `.action()` callback with no exported symbol — so "reached from the
779
+ * preflight" had nothing to resolve against.
780
+ *
781
+ * #3375 extracts that block into `cli/src/commands/sync.ts#runConfigPushPreflight`
782
+ * and this module answers the question statically, from the declared
783
+ * `<module>#<export>`:
784
+ *
785
+ * - `reachableExports` walks the call graph from one entry symbol over module
786
+ * SOURCE TEXT and returns what it reached — plus what it could NOT resolve,
787
+ * so silence is never a pass.
788
+ * - `findUnreachedPreflightRules` reports every `preflight` declaration,
789
+ * cross-field or single-field, whose handler is missing from that set.
790
+ * - `findMalformedFieldValidationStages` is the field-level twin of #3373's
791
+ * `findMalformedRuleDeclarations`: `ConfigField["validation"]` gained a
792
+ * `stage` here, and it must be well-formed to mean anything.
793
+ *
794
+ * Everything is a PURE function over values and over source text the caller
795
+ * passes in: `cli/tests/unit/config-surface-preflight-staging-3375.test.ts`
796
+ * does the filesystem walk, so this module stays vendorable (`types.ts`
797
+ * §Purity), exactly the split `rules.ts` already uses.
798
+ *
799
+ * ── Two things this check is NOT ──────────────────────────────────────────
800
+ *
801
+ * It is NECESSARY, not sufficient, and there is deliberately no converse. A
802
+ * handler reached on one surface's leg satisfies a `preflight` declaration on
803
+ * another (the residual the intent accepts and #3379 records), and asking "is
804
+ * this `apply` rule reached from the preflight?" would be wrong by
805
+ * construction: `validateSigningSecretDeclaration` IS reached on the function
806
+ * leg and IS correctly `apply` on the webhook surface, where no CLI call
807
+ * exists.
808
+ *
809
+ * The walk is identifier-based over masked source, so it over-approximates in
810
+ * the PASSING direction: a local variable shadowing an imported handler's name
811
+ * reads as a call. That is the safe direction for a guard whose failure mode
812
+ * must be "a promise you did not keep", not "a promise you did keep, reported
813
+ * as broken".
814
+ */
815
+ /** A field's `validation` object may carry these keys and no others (#3375). */
816
+ export declare const FIELD_VALIDATION_KEYS: readonly string[];
817
+ /**
818
+ * The modules a walk may read, and how a specifier resolves between them.
819
+ *
820
+ * The caller owns resolution because it owns the filesystem: the CLI test maps
821
+ * both spellings of the vendored artifact onto the `src/config-surface/`
822
+ * modules it is a copy of, which is the whole point of DSO-3375-002 — the
823
+ * generator strips every intra-directory import before concatenating, so the
824
+ * artifact itself carries no edges between its sections.
825
+ */
826
+ export interface StaticModuleGraph {
827
+ /** Repo-relative module path → that module's SOURCE TEXT. */
828
+ modules: ReadonlyMap<string, string>;
829
+ /**
830
+ * Candidate modules a specifier names, seen from `fromModule`.
831
+ *
832
+ * `null` = a bare package specifier: deliberately not followed, and never
833
+ * reported. `[]` = a relative specifier that resolves to no module in the
834
+ * graph, which IS reported — a guard that silently stops looking is worse
835
+ * than no guard.
836
+ */
837
+ resolve(fromModule: string, specifier: string): readonly string[] | null;
838
+ }
839
+ /** What one walk found, and what it could not resolve on the way. */
840
+ export interface ReachabilityWalk {
841
+ /** `<module>#<export>` for every symbol reached, module = where it is DECLARED. */
842
+ reached: ReadonlySet<string>;
843
+ /** Specifiers and entry symbols the graph could not answer for, sorted. */
844
+ unresolved: string[];
845
+ }
846
+ /**
847
+ * Walk the call graph from `entry` and return every symbol it reaches.
848
+ *
849
+ * An identifier inside a reached declaration's span is followed when it names
850
+ * another top-level declaration of the same module, or a value import — which
851
+ * resolves through the graph to whichever candidate module DECLARES the name,
852
+ * following `export { x } from` and `export * from` on the way. A symbol is
853
+ * recorded under the module that declares it, never under a barrel that merely
854
+ * re-exports it: a declaration that names the barrel is reported unreached,
855
+ * because the point of the string is to name the one implementation.
856
+ */
857
+ export declare function reachableExports(entry: string, graph: StaticModuleGraph): ReachabilityWalk;
858
+ /**
859
+ * Every fault in one surface's FIELD validation declarations, each naming the
860
+ * surface and the field. `[]` means every field is well-formed.
861
+ *
862
+ * Runs over every surface in the registry, migrated or not: field declarations
863
+ * exist independently of the cross-field slot, so the
864
+ * `CROSS_FIELD_RULES_UNMIGRATED` ratchet does not — and must not — stand
865
+ * between a field-level `preflight` declaration and this check (DSO-3375-001).
866
+ */
867
+ export declare function findMalformedFieldValidationStages(surface: ConfigObjectSurface): string[];
868
+ /**
869
+ * Every `preflight` declaration on this surface whose handler the walk did not
870
+ * reach, each naming the surface, the rule (or field), the handler and the
871
+ * entry point.
872
+ *
873
+ * The FIELD half runs on every surface. The CROSS-FIELD half runs only where a
874
+ * surface has declared, because `crossFieldRulesForStage` throws for one that
875
+ * has not — "not declared yet" is never "rule-free" (#3373), and asking it
876
+ * here would turn an unmigrated surface into a crash rather than a skip.
877
+ */
878
+ export declare function findUnreachedPreflightRules(surface: ConfigObjectSurface, reached: ReadonlySet<string>, entry: string): string[];
742
879
  /**
743
880
  * Request schemas generated from the configuration-object definitions
744
881
  * (issue #2644, phase 5 / criterion 9).
@@ -1239,115 +1376,107 @@ export interface ParsedManifest {
1239
1376
  */
1240
1377
  export declare function parseFunctionManifest(value: unknown): ParsedManifest;
1241
1378
  /**
1242
- * The function MODE vocabulary, and the one place its spellings meet —
1243
- * #3281, widened by #3454, project `server-functions` phase 4.
1244
- *
1245
- * `mode = "request" | "task" | "any"` is what an author writes (the intent,
1246
- * "Mode vocabulary?", sponsor decision 2026-09-09, AMENDED 2026-09-14): a
1247
- * function is a function, and the runner is chosen at CALL time. `any` — the
1248
- * default when the key is absent — takes either: `invoke` runs it inside the
1249
- * call and returns its result, `start` returns a run id and lets it survive
1250
- * restarts. `request` and `task` are LOCKS to one runner, and the other door
1251
- * is refused with the stable mode-mismatch error.
1252
- *
1253
- * `durable = true | false` is the key the first releases taught, and it stays
1254
- * ACCEPTED as an alias for the two locks — a tree pushed with it keeps
1255
- * pushing, and pulls back byte for byte — while `durable` stays the adjective
1256
- * for the property itself. A boolean names ONE runner, so it cannot say
1257
- * "either": `durable` beside `mode = "any"` is refused naming both keys, the
1258
- * same way the two locks disagreeing always was.
1259
- *
1260
- * ── Why one module ───────────────────────────────────────────────────────
1261
- *
1262
- * Two spellings for one fact is a place two readers can disagree. So the
1263
- * resolution is written once, here, in the dependency-free tree the CLI
1264
- * vendors (`cli/scripts/gen-config-surfaces.mjs`), and every door reads it:
1265
- * the CLI preflight and diff entity, the server's trigger derivation, the
1266
- * admin config-version route (payload `mode` beside payload `durable`), the
1267
- * invoke branch and the trigger fires. A file — or a payload — that carries
1268
- * both keys has to have them AGREE, or it is refused naming both; a silent
1269
- * winner would be the one thing worse than two spellings.
1270
- *
1271
- * The stored fact is `ServerFunctionConfig.mode`, with `durable` beside it as
1272
- * the adjective (`mode === "task"`), which is why `configMode` exists: reading
1273
- * the row is the other half of the same translation. A row written BEFORE
1274
- * #3454 carries no `mode` column at all, and `configMode` reads it the way it
1275
- * always read — the boolean, and the request runner otherwise. Widening such a
1276
- * row to `any` needs its own authored declaration, which lives in its
1277
- * envelope's TOML, so it is `src/server-functions/legacy-mode.ts`'s job and
1278
- * not this module's.
1279
- */
1280
- export type FunctionMode = "request" | "task" | "any";
1281
- /** The two runners a call may name. `any` is a version's mode, not a runner. */
1282
- export type FunctionRunner = "request" | "task";
1283
- export declare const FUNCTION_MODES: readonly FunctionMode[];
1284
- /** The runners, for the doors where a caller states one (#3454, D3454-004). */
1285
- export declare const FUNCTION_RUNNERS: readonly FunctionRunner[];
1286
- /**
1287
- * The rule ids this module owns, declared on the `function` surface (#3374).
1288
- * The union below closes `refuseMode`'s id parameter: a fourth refusal cannot
1289
- * compile without a fourth id, and a fourth id fails `cli-unit-tests` until the
1290
- * surface declares it.
1291
- */
1292
- export declare const FUNCTION_MODE_RULE_IDS: readonly ["mode-value", "durable-value", "mode-durable-agreement"];
1293
- type FunctionModeRuleId = (typeof FUNCTION_MODE_RULE_IDS)[number];
1294
- /** What a declaration resolved to, or why it could not. */
1295
- export type DeclaredModeResolution = {
1296
- ok: true;
1297
- mode: FunctionMode;
1379
+ * The runtime keys a `functions/<key>.toml` no longer decides anything with —
1380
+ * #3482, project `server-functions` phase 4.
1381
+ *
1382
+ * The sponsor settled it on 2026-09-15 (the intent, "Mode vocabulary?",
1383
+ * amended again): the config says NOTHING about how a function runs. A
1384
+ * function is a function; the caller picks the runtime at each call, cron
1385
+ * fires always start a task run, webhook deliveries always run as a request,
1386
+ * and a function that must not run under one runtime tests for it in code with
1387
+ * `assertRuntime`.
1388
+ *
1389
+ * ── Why these keys are accepted, not refused ──────────────────────────────
1390
+ *
1391
+ * Every tree pushed since #3454 that declares a cron trigger carries
1392
+ * `mode = …` ON THE ENTRY, because `cron-trigger-mode-required` demanded it;
1393
+ * plenty carry a top-level `mode` or the older `durable`. Removing the keys
1394
+ * from the accepted set outright would refuse those trees with an unknown-key
1395
+ * error one day after the platform insisted on them, which is the opposite of
1396
+ * principle 5 inside a non-breaking phase.
1397
+ *
1398
+ * So through phase 4 they are ACCEPTED and IGNORED, with one warning per key
1399
+ * naming it and the line to delete; from phase 5 (#3188) they are refused with
1400
+ * the rest of the retirement matrix. The value is not validated — there is
1401
+ * nothing left to validate it against, and refusing a bad value for a key
1402
+ * nobody reads would be a refusal about spelling alone.
1403
+ *
1404
+ * Pure: no models, no tenant context, no `env` (see `types.ts` §Purity). Both
1405
+ * the CLI preflight (vendored by `cli/scripts/gen-config-surfaces.mjs`) and the
1406
+ * admin config-version route read this one scanner, so the warning an author
1407
+ * sees from `config push` is the sentence the API answers with.
1408
+ */
1409
+ /** The two `[function]` keys that no longer say anything (#3482). */
1410
+ export declare const RETIRED_FUNCTION_RUNTIME_KEYS: readonly ["mode", "durable"];
1411
+ export type RetiredFunctionRuntimeKey = (typeof RETIRED_FUNCTION_RUNTIME_KEYS)[number];
1412
+ /** One ignored key met in a `[function]` table. */
1413
+ export interface IgnoredFunctionKey {
1298
1414
  /**
1299
- * Whether the declaration SAID anything — false when neither key was
1300
- * present and the default applied. The admin route's divergence check
1301
- * reads this: an omitted mode is no claim, so it cannot diverge.
1415
+ * Where it was: `mode`, `durable`, or `triggers.cron.<name>.mode`. Never
1416
+ * rendered on its own — a reader that wants to branch reads this, and every
1417
+ * printer reads `message`.
1302
1418
  */
1303
- declared: boolean;
1304
- } | {
1305
- ok: false;
1306
- /** Which rule refused; never rendered, every reader picks `error`. */
1307
- ruleId: FunctionModeRuleId;
1308
- error: string;
1309
- };
1419
+ path: string;
1420
+ /** The sentence the author is owed, with no file prefix (the caller adds it). */
1421
+ message: string;
1422
+ }
1310
1423
  /**
1311
- * Resolve `mode` and its alias `durable` to one mode.
1424
+ * Every retired runtime key a `[function]` table still carries.
1425
+ *
1426
+ * Reads the table the way the author wrote it: the two top-level keys, and a
1427
+ * `mode` on each `[[function.triggers.cron]]` entry. A cron entry's sentence
1428
+ * says the extra thing its author needs to hear — that the schedule no longer
1429
+ * chooses, because every fire starts a task run (#3334's runner branch is
1430
+ * gone).
1312
1431
  *
1313
- * Absent both → `any` (#3454): a file that says nothing about its runner takes
1314
- * either, which is what "a function is a function" means. `mode` must be one
1315
- * of the three words; `durable` must be a boolean. Both present and
1316
- * disagreeing is refused naming both keys, so an author who half-migrated a
1317
- * file hears which line to fix — and `durable` beside `mode = "any"` is one of
1318
- * those disagreements, because a boolean cannot say "either".
1432
+ * Order is the order an author reads their file in: the top-level keys first,
1433
+ * then the cron entries in declaration order.
1319
1434
  */
1320
- export declare function resolveDeclaredMode(input: {
1321
- mode?: unknown;
1322
- durable?: unknown;
1323
- }): DeclaredModeResolution;
1435
+ export declare function ignoredFunctionKeys(functionTable: unknown): IgnoredFunctionKey[];
1324
1436
  /**
1325
- * The mode a stored config-version row runs in.
1437
+ * The database types a `[function]` table's retired trigger block names —
1438
+ * #3483.
1439
+ *
1440
+ * Beside `ignoredFunctionKeys` because it answers the same question about the
1441
+ * same table, for the same reader: `config pull` writes a version's AUTHORED
1442
+ * bytes, and a version pushed before the removal carries the block. Without
1443
+ * this the pulled tree would be refused on its next push — a tree an author
1444
+ * cannot get back into a working state, which is the failure D3482-007 already
1445
+ * had to close for the runtime keys.
1446
+ *
1447
+ * This is NOT a refusal and shares nothing with one. The block is refused on
1448
+ * input by `normalizeTriggerDeclaration`; what a strip needs is the list of
1449
+ * entries it is taking out so it can say so, which is why an entry whose
1450
+ * `type` is unreadable still counts as one entry (the empty string). It is the
1451
+ * block's PRESENCE that goes, not its contents.
1326
1452
  *
1327
- * `mode` is the stored fact and is read first. A row written before #3454
1328
- * carries none, and is read the way it always read: the `durable` boolean, and
1329
- * the request runner otherwise. That reading is CONSERVATIVE on purpose — it
1330
- * is byte for byte what the row did before the change — and widening a
1331
- * pre-change row to `any` needs its own authored declaration, which
1332
- * `src/server-functions/legacy-mode.ts` recovers from the envelope's TOML and
1333
- * records on the row once (D3454-002).
1453
+ * TOML spells the block three ways — an array of tables, a single table, and
1454
+ * an inline array — and a scan that missed one would leave a key behind
1455
+ * (CR3482-002's lesson, applied to a whole table).
1334
1456
  */
1335
- export declare function configMode(row: {
1336
- mode?: unknown;
1337
- durable?: unknown;
1338
- } | null | undefined): FunctionMode;
1339
- /** Is this mode the durable one? The adjective, for the paths that branch on it. */
1340
- export declare function isDurableMode(mode: FunctionMode): boolean;
1457
+ export declare function retiredDatabaseTriggerTypes(functionTable: unknown): string[];
1341
1458
  /**
1342
- * The runner a call gets on a version of this mode, given what it asked for.
1459
+ * Whether the block is THERE at all — the question a strip has to ask, which
1460
+ * the type list above cannot answer (CR3483-001).
1343
1461
  *
1344
- * A LOCK answers itself whatever was requested — the refusal for the other
1345
- * door is a separate decision, made before this one, so that a caller hears a
1346
- * mismatch rather than silently getting the runner it did not ask for. An
1347
- * `any` version answers what the caller named, and the REQUEST runner when it
1348
- * named nothing: that is byte for byte what every `invoke` caller gets today.
1462
+ * `database = []` names no type, so the list is empty and a strip that decided
1463
+ * by its length left the key in the pulled file. That file then fails its own
1464
+ * next push: `normalizeTriggerDeclaration` refuses the key by PRESENCE, of any
1465
+ * shape, an empty array included — so the two halves disagreed about what the
1466
+ * block is, and the tree an author pulled was a tree they could not push.
1467
+ * Presence is the one rule both sides read now, spelled the way
1468
+ * `normalizeTriggerDeclaration` spells it — `!== undefined` — so a value one
1469
+ * of them would call a block and the other would not cannot exist.
1349
1470
  */
1350
- export declare function runnerFor(mode: FunctionMode, requested: FunctionRunner | null | undefined): FunctionRunner;
1471
+ export declare function hasRetiredDatabaseTriggerBlock(functionTable: unknown): boolean;
1472
+ /**
1473
+ * The same scan, rendered for one file — the shape `config push` and
1474
+ * `config diff` print and the admin route answers with.
1475
+ *
1476
+ * `label` is what the reader can act on: `functions/<key>.toml` from the CLI,
1477
+ * and the function's key from the API, which has no file to name.
1478
+ */
1479
+ export declare function ignoredFunctionKeyWarnings(label: string, functionTable: unknown): string[];
1351
1480
  /**
1352
1481
  * The trigger blocks a function declares, derived from its authored TOML
1353
1482
  * (#3181, project `server-functions`, criterion 4).
@@ -1394,29 +1523,20 @@ export declare function runnerFor(mode: FunctionMode, requested: FunctionRunner
1394
1523
  * and the CLI has already parsed its own file by the time it asks.
1395
1524
  */
1396
1525
  /**
1397
- * The resolved mode the declaration is checked against (#3281).
1526
+ * Why a webhook trigger is the request runtime, and a cron fire a task run —
1527
+ * #3482, the intent's "Mode vocabulary?" decision of 2026-09-15.
1398
1528
  *
1399
- * The rule is per KIND, and permanent (the intent, "Sync functions write a run
1400
- * row?": *Webhook and database-change triggers require request mode; HTTP and
1401
- * cron allow both*). A webhook delivery and a database write are both waiting
1402
- * inside a request for the function to answer; a cron fire is waiting for
1403
- * nothing, so it starts a task run when the version is a task (#3334).
1529
+ * Neither is a rule about the FUNCTION any more, so neither is a refusal: a
1530
+ * function is a function, and the DOOR decides which runtime runs it. A
1531
+ * webhook delivery runs inside the receiver because the provider is holding
1532
+ * the connection open waiting for an answer; a cron fire starts a task run
1533
+ * because it is waiting for nothing. A function that must not run under one of
1534
+ * them tests for it in code with `assertRuntime`, which is the only lock left.
1404
1535
  *
1405
- * The caller resolves `mode`/`durable` once, through `resolveDeclaredMode`,
1406
- * and hands the answer here — this module never reads the alias itself.
1536
+ * What survives here is the SHAPE of a declaration — the keys, the names, the
1537
+ * cron expression, the caps. The retired `mode` keys are accepted and reported
1538
+ * as ignored by `ignoredFunctionKeys` in `function-retired-keys.ts`.
1407
1539
  */
1408
- export interface TriggerModeOptions {
1409
- mode?: FunctionMode;
1410
- }
1411
- /**
1412
- * Why a webhook cannot drive a task function, and what to do instead (#3334).
1413
- *
1414
- * The first clause is verbatim what it has always been, so sibling pins on the
1415
- * request-mode prefix (#3281's) keep holding; everything after the dash is the
1416
- * half that was missing — an author who meets this at the push door could not
1417
- * previously tell whether it was a limitation or a design, nor what to write.
1418
- */
1419
- export declare const WEBHOOK_ON_TASK_REFUSAL: string;
1420
1540
  /** A function's single webhook trigger, normalized. */
1421
1541
  export interface WebhookTriggerDeclaration {
1422
1542
  verificationScheme: string;
@@ -1433,29 +1553,13 @@ export interface WebhookTriggerDeclaration {
1433
1553
  export interface CronTriggerDeclaration {
1434
1554
  name: string;
1435
1555
  cron: string;
1436
- /**
1437
- * Which runner THIS schedule's fires use (#3454, D3454-006).
1438
- *
1439
- * Required on an `any` function — nothing else in the system says whether a
1440
- * given schedule means "run it inline" or "start a background run" — and
1441
- * optional on a locked one, where the lock already answers. Absent on every
1442
- * declaration stored before the key existed, which `cronEntryRunner` reads
1443
- * as the request runner those fires already used.
1444
- */
1445
- mode?: FunctionRunner;
1446
1556
  timezone?: string;
1447
1557
  overlapPolicy?: string;
1448
1558
  rootInput?: unknown;
1449
1559
  }
1450
- /** One `[[function.triggers.database]]` entry, normalized (#3184). */
1451
- export interface DatabaseTriggerDeclaration {
1452
- /** The database type key whose writes fire this function. */
1453
- type: string;
1454
- }
1455
1560
  export interface TriggerDeclaration {
1456
1561
  webhook: WebhookTriggerDeclaration | null;
1457
1562
  cron: CronTriggerDeclaration[];
1458
- databases: DatabaseTriggerDeclaration[];
1459
1563
  }
1460
1564
  export declare const EMPTY_TRIGGER_DECLARATION: TriggerDeclaration;
1461
1565
  /**
@@ -1467,21 +1571,13 @@ export declare const EMPTY_TRIGGER_DECLARATION: TriggerDeclaration;
1467
1571
  * remaining slots for the other 40+ objects the cap is sized for.
1468
1572
  */
1469
1573
  export declare const MAX_CRON_TRIGGERS_PER_FUNCTION = 10;
1470
- /**
1471
- * The most database types one function may watch (#3184, D-07).
1472
- *
1473
- * Per-OBJECT, like the cron cap above: a function watching more than a handful
1474
- * of types is describing a fan-in the platform cannot bound per write, and
1475
- * every extra type is another discovery read on somebody's write path.
1476
- */
1477
- export declare const MAX_DATABASE_TRIGGERS_PER_FUNCTION = 5;
1478
1574
  /**
1479
1575
  * The rule ids this module owns — one per refusal, declared on the `function`
1480
1576
  * surface (#3374). The union below closes `refuseTrigger`'s id parameter, so a
1481
1577
  * twenty-seventh refusal cannot compile without a twenty-seventh id, and that
1482
1578
  * id fails `cli-unit-tests` until the surface declares it (#3373's anchor).
1483
1579
  */
1484
- export declare const TRIGGER_RULE_IDS: readonly ["triggers-table", "trigger-kind", "database-trigger-mode", "database-trigger-cap", "database-trigger-entry", "database-trigger-keys", "database-trigger-type-required", "database-trigger-type-charset", "database-trigger-duplicate", "webhook-trigger-single", "webhook-trigger-table", "webhook-trigger-keys", "webhook-trigger-mode", "webhook-trigger-scheme-required", "webhook-trigger-scheme-none", "webhook-trigger-number", "webhook-trigger-dedup-boolean", "webhook-trigger-verification-table", "cron-trigger-cap", "cron-trigger-entry", "cron-trigger-keys", "cron-trigger-name-required", "cron-trigger-name-charset", "cron-trigger-duplicate", "cron-trigger-expression-required", "cron-trigger-overlap-policy", "cron-trigger-mode-value", "cron-trigger-mode-required", "cron-trigger-mode-locked"];
1580
+ export declare const TRIGGER_RULE_IDS: readonly ["triggers-table", "trigger-kind", "database-trigger-removed", "webhook-trigger-single", "webhook-trigger-table", "webhook-trigger-keys", "webhook-trigger-scheme-required", "webhook-trigger-scheme-none", "webhook-trigger-number", "webhook-trigger-dedup-boolean", "webhook-trigger-verification-table", "cron-trigger-cap", "cron-trigger-entry", "cron-trigger-keys", "cron-trigger-name-required", "cron-trigger-name-charset", "cron-trigger-duplicate", "cron-trigger-expression-required", "cron-trigger-overlap-policy"];
1485
1581
  export type TriggerRuleId = (typeof TRIGGER_RULE_IDS)[number];
1486
1582
  /**
1487
1583
  * A broken trigger rule. `ruleId` names which one, for a reader that wants to
@@ -1504,11 +1600,20 @@ export declare function hasDeclaredTriggers(declaration: TriggerDeclaration): bo
1504
1600
  * push boundary can normalize a declaration it read from a stored row without
1505
1601
  * re-parsing bytes.
1506
1602
  */
1507
- export declare function normalizeTriggerDeclaration(functionTable: any, options?: TriggerModeOptions): TriggerDeclaration;
1603
+ export declare function normalizeTriggerDeclaration(functionTable: any): TriggerDeclaration;
1508
1604
  /** The stored JSON for `ServerFunctionConfig.triggers`, or null when empty. */
1509
1605
  export declare function serializeTriggerDeclaration(declaration: TriggerDeclaration): string | null;
1510
1606
  /** Read a stored `ServerFunctionConfig.triggers` value back. */
1511
1607
  export declare function parseStoredTriggerDeclaration(stored: unknown): TriggerDeclaration;
1608
+ /**
1609
+ * The database types a STORED declaration used to watch, for a reader that
1610
+ * wants to name them — #3483.
1611
+ *
1612
+ * Pure, and separate from the parse on purpose: the parse answers what the
1613
+ * version means NOW (nothing), and this answers what it said, which is only
1614
+ * ever used to write one log line. Nothing branches on it.
1615
+ */
1616
+ export declare function storedDatabaseTriggerTypes(stored: unknown): string[];
1512
1617
  /** The `CronTrigger.triggerKey` an entry owns. */
1513
1618
  export declare function cronTriggerKeyFor(functionKey: string, name: string): string;
1514
1619
  /** The entry name a `<functionKey>:<name>` trigger key carries. */
@@ -1762,6 +1867,145 @@ export declare function checkWorkflowSystemAccessRule(input: {
1762
1867
  accessRule?: unknown;
1763
1868
  }): WorkflowRuleViolation | null;
1764
1869
  export declare const WORKFLOW_SURFACE: ConfigObjectSurface;
1870
+ /**
1871
+ * Cron-expression and timezone VALIDITY — the half of `src/cron-parser.ts`
1872
+ * both runners need (#3376, project `cli-server-rule-parity` phase 4).
1873
+ *
1874
+ * ── Why this module exists ───────────────────────────────────────────────
1875
+ *
1876
+ * A malformed cron expression and an unknown timezone are both decidable from
1877
+ * the authored file, so the intent lands both rules `preflight` — which under
1878
+ * this project's vocabulary means the implementation lives here, under
1879
+ * `src/config-surface/`, and is vendored into the CLI artifact.
1880
+ *
1881
+ * It got here by a SPLIT, not a move. `src/cron-parser.ts` is 415 lines, and
1882
+ * roughly 290 of them are timezone-aware scheduling arithmetic — "when does
1883
+ * this expression next fire?" — that only the cron Durable Object runs.
1884
+ * Vendoring is wholesale: the generator concatenates this whole directory, so
1885
+ * moving the file would have shipped that scheduler into the published CLI
1886
+ * package. The validator moved; the scheduler stayed; `src/cron-parser.ts`
1887
+ * re-exports what moved, so no server import changed.
1888
+ *
1889
+ * The scheduler's two entry points are deliberately not NAMED here: a CLI
1890
+ * unit test asserts the artifact does not contain either identifier, which is
1891
+ * how "the artifact carries the validator only" is kept true mechanically
1892
+ * rather than by review.
1893
+ *
1894
+ * ── Not a rules module ───────────────────────────────────────────────────
1895
+ *
1896
+ * `parseCron` THROWS, eight different sentences, and `tests/unit/
1897
+ * cron-parser.test.ts` pins that it throws. A module exporting a `*_RULE_IDS`
1898
+ * list may not contain a `throw` outside its one refusal constructor
1899
+ * (`findUnanchoredRefusals`, #3373), so the rule that renders these answers as
1900
+ * refusals is a separate module, `cron-rules.ts`, and this one is a plain
1901
+ * shared-predicate module — the `webhook-signing-secret.ts` / `function-mode.ts`
1902
+ * precedent.
1903
+ *
1904
+ * Pure: no models, no tenant context, no `env` (see `types.ts` §Purity).
1905
+ * `Intl.DateTimeFormat` is the one platform call, and it is the timezone
1906
+ * question itself.
1907
+ *
1908
+ * Supported fields (standard POSIX order):
1909
+ * minute (0-59)
1910
+ * hour (0-23)
1911
+ * day-of-month (1-31)
1912
+ * month (1-12)
1913
+ * day-of-week (0-6, where 0 = Sunday; 7 also accepted as Sunday)
1914
+ *
1915
+ * Supported syntax per field:
1916
+ * - "*" any value
1917
+ * - "5" exact value
1918
+ * - "5-10" range (inclusive)
1919
+ * - "star/step" every N values within the full range (e.g. "* /5" => 0,5,10,...)
1920
+ * - "a-b/step" every N values within a range
1921
+ * - "1,2,3" comma-separated list of values or ranges
1922
+ *
1923
+ * Not supported: month/day names, "?", "L", "W", "#", last-day modifiers.
1924
+ */
1925
+ export interface ParsedCron {
1926
+ minutes: Set<number>;
1927
+ hours: Set<number>;
1928
+ daysOfMonth: Set<number>;
1929
+ months: Set<number>;
1930
+ daysOfWeek: Set<number>;
1931
+ dayOfMonthWild: boolean;
1932
+ dayOfWeekWild: boolean;
1933
+ }
1934
+ export declare function parseCron(expr: string): ParsedCron;
1935
+ /**
1936
+ * The message `parseCron` would throw for `expr`, or null when it parses.
1937
+ *
1938
+ * The same grammar, asked as a question instead of as an exception, because
1939
+ * both callers want the sentence rather than the control flow: the CLI's push
1940
+ * preflight collects one message per broken entry, and the standalone cron
1941
+ * routes render theirs into a 400 body. `expr` is typed `unknown` because it
1942
+ * arrives as an authored TOML value, where it can be anything at all.
1943
+ */
1944
+ export declare function cronExpressionError(expr: unknown): string | null;
1945
+ /**
1946
+ * Whether `Intl.DateTimeFormat` accepts `name` as a timezone.
1947
+ *
1948
+ * This is THE timezone check — before #3376 the identical probe was written
1949
+ * out three times (the function push boundary and the standalone create and
1950
+ * update). Each of those sites keeps its own sentence and its own defaulting;
1951
+ * what they now share is this answer. A non-string is false rather than a
1952
+ * throw, for the same reason as above.
1953
+ */
1954
+ export declare function isValidTimezone(name: unknown): boolean;
1955
+ /**
1956
+ * The cron schedule rules two runners share (#3376, project
1957
+ * `cli-server-rule-parity` phase 4; intent §Success criteria 6).
1958
+ *
1959
+ * ── One implementation, two runners ──────────────────────────────────────
1960
+ *
1961
+ * A cron expression the grammar refuses, and a timezone `Intl` does not know,
1962
+ * are both decidable from the authored file. Before this issue only the SERVER
1963
+ * decided them, inside the apply loop: a `functions/<key>.toml` carrying
1964
+ * either passed `config push`'s preflight, sibling entities were written, and
1965
+ * the function alone was refused — while the command promises that a refusal
1966
+ * means nothing was applied. Now `cli/src/lib/function-sync.ts` and
1967
+ * `src/services/function-trigger-validation.ts` both call the rule below, and
1968
+ * the sentence an author reads is the same on either side of the wire because
1969
+ * it is built in one place.
1970
+ *
1971
+ * ── Why this is a `*-rules.ts` module and the parser is not ──────────────
1972
+ *
1973
+ * The registry's unclaimed-export check (`findUnclaimedRuleModuleExports`)
1974
+ * keys on this name under `src/config-surface/`: every exported function here
1975
+ * is a rule, so one no surface declares fails `cli-unit-tests`. The anchor
1976
+ * also forbids a `throw` outside the one refusal constructor — and `parseCron`
1977
+ * throws eight different sentences, which `tests/unit/cron-parser.test.ts`
1978
+ * pins. So the grammar and the `Intl` probe live next door in
1979
+ * `cron-expression.ts`, which is a shared-predicate module rather than a rules
1980
+ * module, and this one only decides and renders.
1981
+ *
1982
+ * ── What is NOT shared ───────────────────────────────────────────────────
1983
+ *
1984
+ * The standalone cron routes ask the same two predicates and render their own
1985
+ * `Invalid cron expression: …` / `Invalid timezone: …`, with their own
1986
+ * defaulting (create reads an absent timezone as UTC, update does not). That
1987
+ * is intent assumption 2 in the form its own fallback describes, and the shape
1988
+ * `workflow-rules.ts` set in phase 2: one predicate, each site's message and
1989
+ * defaulting preserved. No refusal message changed anywhere.
1990
+ */
1991
+ /** The rule ids this module owns — the union type below anchors them (#3373). */
1992
+ export declare const CRON_TRIGGER_RULE_IDS: readonly ["cron-trigger-expression", "cron-trigger-timezone"];
1993
+ type CronTriggerRuleId = (typeof CRON_TRIGGER_RULE_IDS)[number];
1994
+ /** A refused cron entry: the rule and the sentence the site renders. */
1995
+ export type CronTriggerRuleRefusal = RuleRefusal<CronTriggerRuleId>;
1996
+ /**
1997
+ * One `[[function.triggers.cron]]` entry's schedule, or null.
1998
+ *
1999
+ * Expression first, then timezone, which is the order the push boundary ran
2000
+ * them in — so an entry broken both ways still answers with the expression's
2001
+ * refusal, exactly as before. An unset or empty `timezone` reads as UTC, again
2002
+ * exactly as before: the boundary's `entry.timezone || "UTC"`.
2003
+ */
2004
+ export declare function checkCronTriggerEntry(entry: {
2005
+ name: string;
2006
+ cron: string;
2007
+ timezone?: string;
2008
+ }): CronTriggerRuleRefusal | null;
1765
2009
  /**
1766
2010
  * The prompt config's reasoning controls, resolved onto each provider's own
1767
2011
  * spelling — issue #3358.