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

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 (121) hide show
  1. package/README.md +43 -1
  2. package/assets/skill/skills/primitive-platform/SKILL.md +105 -675
  3. package/dist/src/commands/admins.js +29 -32
  4. package/dist/src/commands/admins.js.map +1 -1
  5. package/dist/src/commands/analytics.js +2 -1
  6. package/dist/src/commands/analytics.js.map +1 -1
  7. package/dist/src/commands/apps.js +12 -5
  8. package/dist/src/commands/apps.js.map +1 -1
  9. package/dist/src/commands/blob-buckets.js +2 -1
  10. package/dist/src/commands/blob-buckets.js.map +1 -1
  11. package/dist/src/commands/catalog.js +26 -10
  12. package/dist/src/commands/catalog.js.map +1 -1
  13. package/dist/src/commands/collection-type-configs.js +2 -1
  14. package/dist/src/commands/collection-type-configs.js.map +1 -1
  15. package/dist/src/commands/collections.js +88 -18
  16. package/dist/src/commands/collections.js.map +1 -1
  17. package/dist/src/commands/connections.js +8 -9
  18. package/dist/src/commands/connections.js.map +1 -1
  19. package/dist/src/commands/cron-triggers.js +2 -1
  20. package/dist/src/commands/cron-triggers.js.map +1 -1
  21. package/dist/src/commands/database-type-configs.js +3 -2
  22. package/dist/src/commands/database-type-configs.js.map +1 -1
  23. package/dist/src/commands/databases.js +26 -10
  24. package/dist/src/commands/databases.js.map +1 -1
  25. package/dist/src/commands/documents.d.ts +38 -0
  26. package/dist/src/commands/documents.js +258 -64
  27. package/dist/src/commands/documents.js.map +1 -1
  28. package/dist/src/commands/email-templates.js +2 -1
  29. package/dist/src/commands/email-templates.js.map +1 -1
  30. package/dist/src/commands/env.js +11 -1
  31. package/dist/src/commands/env.js.map +1 -1
  32. package/dist/src/commands/feature-flags.js +2 -1
  33. package/dist/src/commands/feature-flags.js.map +1 -1
  34. package/dist/src/commands/functions.js +3 -2
  35. package/dist/src/commands/functions.js.map +1 -1
  36. package/dist/src/commands/group-type-configs.js +2 -1
  37. package/dist/src/commands/group-type-configs.js.map +1 -1
  38. package/dist/src/commands/groups.js +29 -8
  39. package/dist/src/commands/groups.js.map +1 -1
  40. package/dist/src/commands/guides.js +17 -11
  41. package/dist/src/commands/guides.js.map +1 -1
  42. package/dist/src/commands/integrations.js +26 -9
  43. package/dist/src/commands/integrations.js.map +1 -1
  44. package/dist/src/commands/locks.js +2 -1
  45. package/dist/src/commands/locks.js.map +1 -1
  46. package/dist/src/commands/metadata-category-configs.js +2 -1
  47. package/dist/src/commands/metadata-category-configs.js.map +1 -1
  48. package/dist/src/commands/metadata.js +8 -1
  49. package/dist/src/commands/metadata.js.map +1 -1
  50. package/dist/src/commands/prompts.js +28 -11
  51. package/dist/src/commands/prompts.js.map +1 -1
  52. package/dist/src/commands/rule-sets.js +2 -1
  53. package/dist/src/commands/rule-sets.js.map +1 -1
  54. package/dist/src/commands/scripts.js +15 -6
  55. package/dist/src/commands/scripts.js.map +1 -1
  56. package/dist/src/commands/secrets.js +2 -1
  57. package/dist/src/commands/secrets.js.map +1 -1
  58. package/dist/src/commands/sessions.js +10 -9
  59. package/dist/src/commands/sessions.js.map +1 -1
  60. package/dist/src/commands/sync.d.ts +64 -0
  61. package/dist/src/commands/sync.js +347 -17
  62. package/dist/src/commands/sync.js.map +1 -1
  63. package/dist/src/commands/tokens.js +2 -1
  64. package/dist/src/commands/tokens.js.map +1 -1
  65. package/dist/src/commands/users.js +16 -9
  66. package/dist/src/commands/users.js.map +1 -1
  67. package/dist/src/commands/vars.js +2 -1
  68. package/dist/src/commands/vars.js.map +1 -1
  69. package/dist/src/commands/waitlist.js +2 -1
  70. package/dist/src/commands/waitlist.js.map +1 -1
  71. package/dist/src/commands/webhooks.js +17 -10
  72. package/dist/src/commands/webhooks.js.map +1 -1
  73. package/dist/src/commands/workflows.js +36 -18
  74. package/dist/src/commands/workflows.js.map +1 -1
  75. package/dist/src/lib/api-client.d.ts +111 -1
  76. package/dist/src/lib/api-client.js +179 -66
  77. package/dist/src/lib/api-client.js.map +1 -1
  78. package/dist/src/lib/config-object-descriptor.js +29 -7
  79. package/dist/src/lib/config-object-descriptor.js.map +1 -1
  80. package/dist/src/lib/config-payload.js +27 -2
  81. package/dist/src/lib/config-payload.js.map +1 -1
  82. package/dist/src/lib/config-surface.d.ts +2 -1
  83. package/dist/src/lib/config-surface.js +50 -3
  84. package/dist/src/lib/config-surface.js.map +1 -1
  85. package/dist/src/lib/db-codegen/dbTypeIR.js +8 -0
  86. package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -1
  87. package/dist/src/lib/document-ingest-rows.d.ts +137 -0
  88. package/dist/src/lib/document-ingest-rows.js +181 -0
  89. package/dist/src/lib/document-ingest-rows.js.map +1 -0
  90. package/dist/src/lib/document-ingest.d.ts +25 -2
  91. package/dist/src/lib/document-ingest.js +184 -31
  92. package/dist/src/lib/document-ingest.js.map +1 -1
  93. package/dist/src/lib/function-db-types.d.ts +1 -1
  94. package/dist/src/lib/function-db-types.js +1 -1
  95. package/dist/src/lib/function-document-types.js +23 -2
  96. package/dist/src/lib/function-document-types.js.map +1 -1
  97. package/dist/src/lib/generated-config-surfaces.d.ts +182 -1
  98. package/dist/src/lib/generated-config-surfaces.js +441 -2
  99. package/dist/src/lib/generated-config-surfaces.js.map +1 -1
  100. package/dist/src/lib/generated-sdk-types.d.ts +1 -1
  101. package/dist/src/lib/generated-sdk-types.js +1 -1
  102. package/dist/src/lib/generated-sdk-types.js.map +1 -1
  103. package/dist/src/lib/list-options.d.ts +68 -0
  104. package/dist/src/lib/list-options.js +89 -0
  105. package/dist/src/lib/list-options.js.map +1 -0
  106. package/dist/src/lib/log-inspection.d.ts +16 -0
  107. package/dist/src/lib/log-inspection.js +23 -0
  108. package/dist/src/lib/log-inspection.js.map +1 -1
  109. package/dist/src/lib/paginate.d.ts +15 -0
  110. package/dist/src/lib/paginate.js +17 -0
  111. package/dist/src/lib/paginate.js.map +1 -1
  112. package/dist/src/lib/resolve-owner.d.ts +19 -0
  113. package/dist/src/lib/resolve-owner.js +20 -0
  114. package/dist/src/lib/resolve-owner.js.map +1 -0
  115. package/dist/src/lib/skill-installer.d.ts +49 -3
  116. package/dist/src/lib/skill-installer.js +275 -100
  117. package/dist/src/lib/skill-installer.js.map +1 -1
  118. package/dist/src/lib/snapshot-build.d.ts +50 -0
  119. package/dist/src/lib/snapshot-build.js +111 -0
  120. package/dist/src/lib/snapshot-build.js.map +1 -0
  121. package/package.json +2 -2
@@ -365,13 +365,98 @@ export function handlerReferences(table) {
365
365
  }
366
366
  return [...refs].sort();
367
367
  }
368
- /** TOML keys this table accepts: the exposed fields plus the declared extras. */
368
+ // ── TOML groups (#3626) ──────────────────────────────────────────────────
369
+ //
370
+ // A field may declare the sub-table it is authored under (`tomlGroup`), and the
371
+ // table declares the closed set of names it admits (`tomlGroups`). These four
372
+ // functions are the whole mechanism: everything else — the pull projection, the
373
+ // push payload builder, the CLI's unknown-key and declared-type checks — reads
374
+ // the declaration through them.
375
+ /** The group names this table declares, sorted. Empty for an ungrouped table. */
376
+ export function tomlGroupNames(table) {
377
+ return Object.keys(table.tomlGroups ?? {}).sort();
378
+ }
379
+ /** The TOML keys accepted INSIDE `group`: every field that declares it. */
380
+ export function acceptedGroupTomlKeys(table, group) {
381
+ return new Set(table.fields.filter((f) => f.tomlGroup === group).map((f) => f.tomlKey));
382
+ }
383
+ /**
384
+ * TOML keys this table accepts AT ITS ROOT: the UNGROUPED fields, the declared
385
+ * group names, and the declared extras.
386
+ *
387
+ * A grouped leaf written at the root (`questions` directly under `[[configs]]`)
388
+ * is therefore an unknown key rather than one the payload builder silently
389
+ * ignores (SO3626-013) — the builder reads a grouped field only from
390
+ * `source[group][tomlKey]`, so accepting it here would drop the author's value
391
+ * without a word. The nine deprecated flat chat keys stay accepted through
392
+ * their `tomlOnlyKeys` declarations, which is what keeps old files pushing.
393
+ */
369
394
  export function acceptedTomlKeys(table) {
370
395
  return new Set([
371
- ...table.fields.map((f) => f.tomlKey),
396
+ ...table.fields.filter((f) => !f.tomlGroup).map((f) => f.tomlKey),
397
+ ...tomlGroupNames(table),
372
398
  ...Object.keys(table.tomlOnlyKeys),
373
399
  ]);
374
400
  }
401
+ /**
402
+ * Keys inside a group table that no field declares under it, as
403
+ * `"<group>.<key>"` — plus the bare `"<group>"` for a group value that is not a
404
+ * table at all (`chat = "yes"`), which has no keys to check and would otherwise
405
+ * be read as an empty block.
406
+ */
407
+ export function findUnknownGroupedTomlKeys(tomlTable, table) {
408
+ if (!tomlTable || typeof tomlTable !== "object" || Array.isArray(tomlTable)) {
409
+ return [];
410
+ }
411
+ const entry = tomlTable;
412
+ const out = [];
413
+ for (const group of tomlGroupNames(table)) {
414
+ if (!(group in entry))
415
+ continue;
416
+ const value = entry[group];
417
+ if (value === undefined || value === null)
418
+ continue;
419
+ if (typeof value !== "object" || Array.isArray(value)) {
420
+ out.push(group);
421
+ continue;
422
+ }
423
+ const known = acceptedGroupTomlKeys(table, group);
424
+ for (const key of Object.keys(value)) {
425
+ if (!known.has(key))
426
+ out.push(`${group}.${key}`);
427
+ }
428
+ }
429
+ return out.sort();
430
+ }
431
+ /**
432
+ * Group declarations that do not hold together: a field naming a group the
433
+ * table does not declare, a declared group no field is authored under, and a
434
+ * declaration with no reason. All three are failures, for the reason
435
+ * `notExposed` carries a note — a name in a list with nothing behind it reads
436
+ * as a decision and is not one.
437
+ */
438
+ export function findBadTomlGroupDeclarations(table) {
439
+ const declared = new Set(tomlGroupNames(table));
440
+ const used = new Set(table.fields
441
+ .map((f) => f.tomlGroup)
442
+ .filter((g) => typeof g === "string"));
443
+ const bad = [];
444
+ for (const field of table.fields) {
445
+ if (field.tomlGroup && !declared.has(field.tomlGroup)) {
446
+ bad.push(`${field.field}: tomlGroup "${field.tomlGroup}" is not declared in tomlGroups`);
447
+ }
448
+ }
449
+ for (const [group, entry] of Object.entries(table.tomlGroups ?? {})) {
450
+ if (!entry || typeof entry.note !== "string" || !entry.note.trim()) {
451
+ bad.push(`${group}: tomlGroups entries need a note`);
452
+ continue;
453
+ }
454
+ if (!used.has(group)) {
455
+ bad.push(`${group}: declared in tomlGroups but no field is authored under it`);
456
+ }
457
+ }
458
+ return bad.sort();
459
+ }
375
460
  // ── Document-level acceptance ────────────────────────────────────────────
376
461
  //
377
462
  // `acceptedTomlKeys` answers "is this key accepted inside this table". These
@@ -4770,6 +4855,260 @@ function geminiResolution(model, effort, budget) {
4770
4855
  },
4771
4856
  };
4772
4857
  }
4858
+ // ── src/config-surface/prompt-kinds.ts ───────────────────────────────────
4859
+ /**
4860
+ * What a prompt's KIND admits — issue #3626.
4861
+ *
4862
+ * A prompt declares `[prompt] kind = "chat" | "decisions"`. The kind decides
4863
+ * the input envelope (`variables` rendered into templates for chat, a `state`
4864
+ * value for decisions), the provider endpoint the executor posts to, and which
4865
+ * `[configs.<kind>]` block a `[[configs]]` entry may carry. Adding a kind is a
4866
+ * group declaration plus its fields, not another round of implicit
4867
+ * discrimination: the earlier design inferred "this is a decisions config" from
4868
+ * the presence of `questions` on a table otherwise shaped for chat, and refused
4869
+ * the nine chat keys one by one.
4870
+ *
4871
+ * This module is the whole rule. `resolvePromptKind` is called by the CLI's
4872
+ * push preflight through the vendored artifact, by `createAppPrompt`,
4873
+ * `createPromptConfig` and `updatePromptConfig`, so a refusal at push and a
4874
+ * refusal at the admin route cannot disagree — the `resolvePromptReasoning`
4875
+ * posture of #3358, for the same reason.
4876
+ *
4877
+ * Pure and dependency-free, like every module under `src/config-surface/`.
4878
+ */
4879
+ /** The kinds a prompt may declare. An omitted `kind` is `chat`. */
4880
+ export const PROMPT_KINDS = ["chat", "decisions"];
4881
+ /**
4882
+ * The nine settings that describe a CHAT COMPLETION and nothing else. They are
4883
+ * authored under `[configs.chat]`; the flat spelling is deprecated (#3642).
4884
+ */
4885
+ export const CHAT_CONFIG_KEYS = [
4886
+ "systemPrompt",
4887
+ "userPromptTemplate",
4888
+ "temperature",
4889
+ "topP",
4890
+ "maxTokens",
4891
+ "outputFormat",
4892
+ "outputSchema",
4893
+ "reasoningEffort",
4894
+ "reasoningBudget",
4895
+ ];
4896
+ /** The settings that describe a DECISIONS request. */
4897
+ export const DECISIONS_CONFIG_KEYS = ["questions"];
4898
+ /**
4899
+ * The WIRE field names each kind's block holds. The config surface declares the
4900
+ * same grouping through `ConfigField.tomlGroup`, and the drift guard asserts
4901
+ * the two agree — so a chat key added to the surface without a home here (or
4902
+ * the reverse) fails `cli-unit-tests` rather than being accepted under a
4903
+ * decisions prompt.
4904
+ */
4905
+ export const PROMPT_KIND_BLOCKS = {
4906
+ chat: CHAT_CONFIG_KEYS,
4907
+ decisions: DECISIONS_CONFIG_KEYS,
4908
+ };
4909
+ /** The question types TypeSafe's System One models answer. */
4910
+ export const DECISIONS_QUESTION_TYPES = ["choice", "score", "noul"];
4911
+ /** A question name the endpoint accepts, and that reads as a JSON key. */
4912
+ const QUESTION_NAME = /^[A-Za-z_][A-Za-z0-9_-]*$/;
4913
+ /**
4914
+ * The prompt's kind, normalized.
4915
+ *
4916
+ * Every reader goes through this: rows created before #3626 carry no `kind`
4917
+ * attribute at all, and an absent one is `chat` everywhere — the detail route,
4918
+ * pull, diff, execution and codegen.
4919
+ */
4920
+ export function promptKindOf(prompt) {
4921
+ const raw = prompt?.kind;
4922
+ if (typeof raw !== "string")
4923
+ return "chat";
4924
+ const text = raw.trim();
4925
+ return PROMPT_KINDS.includes(text)
4926
+ ? text
4927
+ : "chat";
4928
+ }
4929
+ /** `null`, `undefined` and `""` are absence; `false` and `0` are values. */
4930
+ function isUnset(value) {
4931
+ if (value === null || value === undefined)
4932
+ return true;
4933
+ return typeof value === "string" && value.trim() === "";
4934
+ }
4935
+ function isPlainObject(value) {
4936
+ return (typeof value === "object" && value !== null && !Array.isArray(value));
4937
+ }
4938
+ function isNonEmptyString(value) {
4939
+ return typeof value === "string" && value.trim() !== "";
4940
+ }
4941
+ /**
4942
+ * `questions` as a value: a TOML table arrives as an object, a JSON-string
4943
+ * spelling as text (a `json` field takes either transport, #2880 criterion 5).
4944
+ * `undefined` for anything that is not a JSON object.
4945
+ */
4946
+ function parseQuestions(value) {
4947
+ if (isPlainObject(value))
4948
+ return value;
4949
+ if (typeof value === "string" && value.trim() !== "") {
4950
+ try {
4951
+ const parsed = JSON.parse(value);
4952
+ return isPlainObject(parsed) ? parsed : undefined;
4953
+ }
4954
+ catch {
4955
+ return undefined;
4956
+ }
4957
+ }
4958
+ return undefined;
4959
+ }
4960
+ /** The chat keys this config actually sets, in declaration order. */
4961
+ function chatKeysSet(config) {
4962
+ return CHAT_CONFIG_KEYS.filter((key) => !isUnset(config[key]));
4963
+ }
4964
+ /**
4965
+ * One rule replaces the nine per-key refusals: a `[[configs]]` entry may carry
4966
+ * only the block named by its prompt's kind.
4967
+ */
4968
+ function wrongBlockError(kind, offending) {
4969
+ const wrongKind = kind === "chat" ? "decisions" : "chat";
4970
+ return (`prompt kind is "${kind}", so a [[configs]] entry may carry only a ` +
4971
+ `[configs.${kind}] block; this one sets ${offending.join(", ")} ` +
4972
+ `([configs.${wrongKind}] in TOML). Move them to a ${wrongKind} prompt or ` +
4973
+ "remove them.");
4974
+ }
4975
+ function questionError(name, detail) {
4976
+ return `questions.${name} ${detail}`;
4977
+ }
4978
+ /** Every refusal a single decisions question can earn, or `null`. */
4979
+ function decisionsQuestionError(name, question) {
4980
+ if (!QUESTION_NAME.test(name)) {
4981
+ return (`questions.${name} is not a usable question name; names match ` +
4982
+ "[A-Za-z_][A-Za-z0-9_-]* (the endpoint keys its answers by them)");
4983
+ }
4984
+ if (!isPlainObject(question)) {
4985
+ return questionError(name, "must be a table declaring the question");
4986
+ }
4987
+ const type = question.type;
4988
+ if (typeof type !== "string" ||
4989
+ !DECISIONS_QUESTION_TYPES.includes(type)) {
4990
+ return questionError(name, `needs type = one of ${DECISIONS_QUESTION_TYPES.join(" | ")}`);
4991
+ }
4992
+ if (!isNonEmptyString(question.instructions)) {
4993
+ return questionError(name, "needs instructions — the sentence the model answers");
4994
+ }
4995
+ if (type === "choice") {
4996
+ const criteria = question.criteria;
4997
+ if (!isPlainObject(criteria)) {
4998
+ return questionError(name, "is a choice question, so it needs a criteria table of option key = " +
4999
+ "description");
5000
+ }
5001
+ const entries = Object.entries(criteria);
5002
+ if (entries.length < 2) {
5003
+ return questionError(name, "needs at least two criteria entries; a choice between one option is " +
5004
+ "not a choice");
5005
+ }
5006
+ const blank = entries.find(([, value]) => !isNonEmptyString(value));
5007
+ if (blank) {
5008
+ return questionError(name, `criteria.${blank[0]} needs a non-empty description`);
5009
+ }
5010
+ }
5011
+ // Every other key on a question — `score`'s array `criteria`, anything
5012
+ // TypeSafe adds next — passes through; the provider validates it.
5013
+ return null;
5014
+ }
5015
+ /**
5016
+ * The one rule: does this (kind, provider, config) triple describe something
5017
+ * the platform can run?
5018
+ *
5019
+ * `{ ok: true, kind: "chat" }` is today's behavior byte for byte. The decisions
5020
+ * arm returns the NORMALIZED questions so the caller stores one spelling
5021
+ * whichever transport the author used.
5022
+ */
5023
+ export function resolvePromptKind(input) {
5024
+ const rawKind = input.kind;
5025
+ let kind = "chat";
5026
+ if (!isUnset(rawKind)) {
5027
+ const text = typeof rawKind === "string" ? rawKind.trim() : String(rawKind);
5028
+ if (!PROMPT_KINDS.includes(text)) {
5029
+ return {
5030
+ ok: false,
5031
+ error: `kind must be one of ${PROMPT_KINDS.join(" | ")}; found ` +
5032
+ `"${text}"`,
5033
+ };
5034
+ }
5035
+ kind = text;
5036
+ }
5037
+ const config = input.config || {};
5038
+ if (kind === "chat") {
5039
+ if (!isUnset(config.questions)) {
5040
+ return { ok: false, error: wrongBlockError("chat", ["questions"]) };
5041
+ }
5042
+ if (isUnset(config.userPromptTemplate)) {
5043
+ return { ok: false, error: "userPromptTemplate is required" };
5044
+ }
5045
+ return { ok: true, kind: "chat" };
5046
+ }
5047
+ // ── decisions ──────────────────────────────────────────────────────────
5048
+ const strayChatKeys = chatKeysSet(config);
5049
+ if (strayChatKeys.length > 0) {
5050
+ return { ok: false, error: wrongBlockError("decisions", strayChatKeys) };
5051
+ }
5052
+ const provider = typeof input.provider === "string" ? input.provider.trim().toLowerCase() : "";
5053
+ if (provider !== "openrouter") {
5054
+ return {
5055
+ ok: false,
5056
+ error: "a decisions config runs on the openrouter provider (OpenRouter's " +
5057
+ `/api/alpha/decisions endpoint); found "${provider || "none"}"`,
5058
+ };
5059
+ }
5060
+ const questions = parseQuestions(config.questions);
5061
+ if (!questions || Object.keys(questions).length === 0) {
5062
+ return {
5063
+ ok: false,
5064
+ error: "a decisions config needs questions — declare at least one under " +
5065
+ "[configs.decisions.questions.<name>]",
5066
+ };
5067
+ }
5068
+ for (const [name, question] of Object.entries(questions)) {
5069
+ const error = decisionsQuestionError(name, question);
5070
+ if (error)
5071
+ return { ok: false, error };
5072
+ }
5073
+ return { ok: true, kind: "decisions", questions };
5074
+ }
5075
+ /**
5076
+ * Move the deprecated flat chat keys of one `[[configs]]` entry under `chat`
5077
+ * — #3626 §4, the blob-bucket `accessPolicy` precedent made reusable.
5078
+ *
5079
+ * Push keeps accepting the nine keys flat so no existing file breaks, and pull
5080
+ * rewrites them, so one pull-then-push migrates a file. A key present in BOTH
5081
+ * spellings is not merged: the grouped value is left standing and the key is
5082
+ * reported in `conflicts`, which the caller turns into a refusal naming the
5083
+ * entry. Nothing else in the entry is touched — `providerConfig` is
5084
+ * provider-keyed rather than chat-keyed (D3626-002) and `questions` has no flat
5085
+ * spelling at all (SO3626-013).
5086
+ */
5087
+ export function hoistDeprecatedChatKeys(entry) {
5088
+ if (!isPlainObject(entry)) {
5089
+ return { entry: {}, moved: [], conflicts: [] };
5090
+ }
5091
+ const out = { ...entry };
5092
+ const existingGroup = isPlainObject(out.chat) ? { ...out.chat } : undefined;
5093
+ const group = existingGroup ?? {};
5094
+ const moved = [];
5095
+ const conflicts = [];
5096
+ for (const key of CHAT_CONFIG_KEYS) {
5097
+ if (!(key in out))
5098
+ continue;
5099
+ if (existingGroup && key in existingGroup) {
5100
+ conflicts.push(key);
5101
+ delete out[key];
5102
+ continue;
5103
+ }
5104
+ group[key] = out[key];
5105
+ delete out[key];
5106
+ moved.push(key);
5107
+ }
5108
+ if (existingGroup || moved.length > 0)
5109
+ out.chat = group;
5110
+ return { entry: out, moved: moved.sort(), conflicts: conflicts.sort() };
5111
+ }
4773
5112
  // ── src/config-surface/prompt.ts ─────────────────────────────────────────
4774
5113
  /**
4775
5114
  * The `prompt` configuration object's definition (issue #2644, phase 2).
@@ -4784,6 +5123,27 @@ function geminiResolution(model, effort, budget) {
4784
5123
  * (`createPromptConfig` / `updatePromptConfig`) and pulls, not an opaque blob.
4785
5124
  */
4786
5125
  const PROMPT_HANDLER = "src/admin-api.ts";
5126
+ /**
5127
+ * #3626 §4 — the nine chat keys, still accepted FLAT under `[[configs]]`.
5128
+ *
5129
+ * The blob-bucket `accessPolicy` precedent: declared `deprecated`, so
5130
+ * `config push` keeps taking a file written before the `[configs.chat]` block
5131
+ * existed, while `config pull` never writes the spelling again. One
5132
+ * pull-then-push migrates a file. `hoistDeprecatedChatKeys` moves them into the
5133
+ * group before the payload is built; rejecting the flat spelling outright is
5134
+ * #3642.
5135
+ */
5136
+ const DEPRECATED_FLAT_CHAT_KEYS = Object.fromEntries(CHAT_CONFIG_KEYS.map((key) => [
5137
+ key,
5138
+ {
5139
+ kind: "deprecated",
5140
+ note: `#3626 — the pre-kind spelling of \`[configs.chat].${key}\`. Still ` +
5141
+ "accepted flat so no existing prompt file breaks (push hoists it " +
5142
+ "into the block and warns); `config pull` writes only the grouped " +
5143
+ "spelling, so one pull-then-push migrates the file. Rejecting the " +
5144
+ "flat spelling is #3642.",
5145
+ },
5146
+ ]));
4787
5147
  export const PROMPT_SURFACE = {
4788
5148
  label: "prompt",
4789
5149
  tables: [
@@ -4793,6 +5153,34 @@ export const PROMPT_SURFACE = {
4793
5153
  model: "AppPrompt",
4794
5154
  // Declaration order reproduces the pre-#2644 serializer's key order.
4795
5155
  fields: [
5156
+ // #3626 — WHAT this prompt runs. `chat` is today's prompt (a chat
5157
+ // completion rendered from templates); `decisions` names OpenRouter's
5158
+ // `/api/alpha/decisions` endpoint, which takes a `state` value plus
5159
+ // named typed questions. It lives on the PROMPT because it decides the
5160
+ // input envelope, the output type, the meaning of `inputSchema` /
5161
+ // `outputSchema` and the shape of every test case's `inputVariables` —
5162
+ // all of which are asserted per prompt KEY.
5163
+ //
5164
+ // `emit: "always"` so a pulled file says which kind it is; an omitted
5165
+ // key is `chat`, so no file written before this changes meaning, and
5166
+ // `serverDefault` is what lets push send `chat` for an absent key and
5167
+ // `config diff` read both spellings as one value.
5168
+ //
5169
+ // CREATE_ONLY, and immutable in the strong sense (D3626-008): changing
5170
+ // a prompt's kind under its key would change the contract of every
5171
+ // caller compiled against it, so the update handler never reads it and
5172
+ // the push gate declines the change as `immutable` — the `ttlTier`
5173
+ // precedent. The prompt comparator projects `[prompt]` in CREATE mode
5174
+ // for exactly that reason (SO3626-008).
5175
+ {
5176
+ field: "kind",
5177
+ tomlKey: "kind",
5178
+ type: "string",
5179
+ emit: "always",
5180
+ writableOn: CREATE_ONLY,
5181
+ serverDefault: "chat",
5182
+ validation: handledBy("src/config-surface/prompt-kinds.ts", "resolvePromptKind", "apply"),
5183
+ },
4796
5184
  {
4797
5185
  field: "displayName",
4798
5186
  tomlKey: "displayName",
@@ -4935,6 +5323,26 @@ export const PROMPT_SURFACE = {
4935
5323
  tomlPath: ["configs"],
4936
5324
  repeated: true,
4937
5325
  model: "AppPromptConfig",
5326
+ // #3626 — one block per prompt kind. A `[[configs]]` entry carries the
5327
+ // SHARED keys flat (name, status, description, provider, model,
5328
+ // providerConfig, plus the `active` marker) and then exactly one block,
5329
+ // named after the prompt's kind. The rule that a config may carry only
5330
+ // its kind's block is `resolvePromptKind`; this declaration is what makes
5331
+ // the two blocks a property of the surface rather than of prompt-specific
5332
+ // code, so the next kind is a group plus its fields.
5333
+ tomlGroups: {
5334
+ chat: {
5335
+ note: "#3626 — the chat-completion settings of a `kind = \"chat\"` " +
5336
+ "prompt: the nine keys that describe a chat call and nothing " +
5337
+ "else. Authored under `[configs.chat]`; the flat spelling is " +
5338
+ "accepted as deprecated (#3642) and rewritten by `config pull`.",
5339
+ },
5340
+ decisions: {
5341
+ note: "#3626 — the settings of a `kind = \"decisions\"` prompt: the " +
5342
+ "named typed questions OpenRouter's /api/alpha/decisions endpoint " +
5343
+ "answers. Authored under `[configs.decisions.questions.<name>]`.",
5344
+ },
5345
+ },
4938
5346
  fields: [
4939
5347
  {
4940
5348
  field: "configName",
@@ -4997,6 +5405,7 @@ export const PROMPT_SURFACE = {
4997
5405
  {
4998
5406
  field: "systemPrompt",
4999
5407
  tomlKey: "systemPrompt",
5408
+ tomlGroup: "chat",
5000
5409
  type: "string",
5001
5410
  emit: "whenSet",
5002
5411
  writableOn: BOTH,
@@ -5007,6 +5416,7 @@ export const PROMPT_SURFACE = {
5007
5416
  {
5008
5417
  field: "userPromptTemplate",
5009
5418
  tomlKey: "userPromptTemplate",
5419
+ tomlGroup: "chat",
5010
5420
  type: "string",
5011
5421
  emit: "whenSet",
5012
5422
  writableOn: BOTH,
@@ -5015,6 +5425,7 @@ export const PROMPT_SURFACE = {
5015
5425
  {
5016
5426
  field: "temperature",
5017
5427
  tomlKey: "temperature",
5428
+ tomlGroup: "chat",
5018
5429
  type: "number",
5019
5430
  emit: "whenSet",
5020
5431
  writableOn: BOTH,
@@ -5032,6 +5443,7 @@ export const PROMPT_SURFACE = {
5032
5443
  {
5033
5444
  field: "topP",
5034
5445
  tomlKey: "topP",
5446
+ tomlGroup: "chat",
5035
5447
  type: "number",
5036
5448
  emit: "whenSet",
5037
5449
  writableOn: BOTH,
@@ -5043,6 +5455,7 @@ export const PROMPT_SURFACE = {
5043
5455
  {
5044
5456
  field: "maxTokens",
5045
5457
  tomlKey: "maxTokens",
5458
+ tomlGroup: "chat",
5046
5459
  type: "number",
5047
5460
  emit: "whenSet",
5048
5461
  writableOn: BOTH,
@@ -5051,6 +5464,7 @@ export const PROMPT_SURFACE = {
5051
5464
  {
5052
5465
  field: "outputFormat",
5053
5466
  tomlKey: "outputFormat",
5467
+ tomlGroup: "chat",
5054
5468
  type: "string",
5055
5469
  emit: "whenSet",
5056
5470
  writableOn: BOTH,
@@ -5061,6 +5475,7 @@ export const PROMPT_SURFACE = {
5061
5475
  {
5062
5476
  field: "outputSchema",
5063
5477
  tomlKey: "outputSchema",
5478
+ tomlGroup: "chat",
5064
5479
  type: "json",
5065
5480
  emit: "whenSet",
5066
5481
  writableOn: BOTH,
@@ -5089,6 +5504,7 @@ export const PROMPT_SURFACE = {
5089
5504
  {
5090
5505
  field: "reasoningEffort",
5091
5506
  tomlKey: "reasoningEffort",
5507
+ tomlGroup: "chat",
5092
5508
  type: "string",
5093
5509
  emit: "whenSet",
5094
5510
  writableOn: BOTH,
@@ -5097,11 +5513,30 @@ export const PROMPT_SURFACE = {
5097
5513
  {
5098
5514
  field: "reasoningBudget",
5099
5515
  tomlKey: "reasoningBudget",
5516
+ tomlGroup: "chat",
5100
5517
  type: "number",
5101
5518
  emit: "whenSet",
5102
5519
  writableOn: BOTH,
5103
5520
  validation: handledBy("src/config-surface/prompt-reasoning.ts", "resolvePromptReasoning", "apply"),
5104
5521
  },
5522
+ // #3626 — the named typed questions a DECISIONS config asks. A `json`
5523
+ // field like `outputSchema`: stored as a JSON string, returned parsed,
5524
+ // and authored either as nested tables
5525
+ // (`[configs.decisions.questions.category]`) or as a JSON string.
5526
+ //
5527
+ // It has no flat spelling at all — `acceptedTomlKeys` refuses it at the
5528
+ // `[[configs]]` root and names the block it belongs under (SO3626-013),
5529
+ // because the payload builder reads a grouped field only from its
5530
+ // group and a flat value would otherwise be dropped in silence.
5531
+ {
5532
+ field: "questions",
5533
+ tomlKey: "questions",
5534
+ tomlGroup: "decisions",
5535
+ type: "json",
5536
+ emit: "whenSet",
5537
+ writableOn: BOTH,
5538
+ validation: handledBy("src/config-surface/prompt-kinds.ts", "resolvePromptKind", "apply"),
5539
+ },
5105
5540
  ],
5106
5541
  notExposed: {
5107
5542
  configId: {
@@ -5162,6 +5597,8 @@ export const PROMPT_SURFACE = {
5162
5597
  "rejection does not fail a file that has always used it; pull " +
5163
5598
  "never writes it.",
5164
5599
  },
5600
+ // #3626 §4 — the nine chat keys in their pre-kind FLAT spelling.
5601
+ ...DEPRECATED_FLAT_CHAT_KEYS,
5165
5602
  },
5166
5603
  responseOnlyKeys: {},
5167
5604
  },
@@ -8138,6 +8575,7 @@ export const GENERATED_CONFIG_MODEL_FIELDS = {
8138
8575
  "promptId",
8139
8576
  "appId",
8140
8577
  "promptKey",
8578
+ "kind",
8141
8579
  "displayName",
8142
8580
  "description",
8143
8581
  "copiedFromCatalogId",
@@ -8170,6 +8608,7 @@ export const GENERATED_CONFIG_MODEL_FIELDS = {
8170
8608
  "outputFormat",
8171
8609
  "outputSchema",
8172
8610
  "providerConfig",
8611
+ "questions",
8173
8612
  "reasoningEffort",
8174
8613
  "reasoningBudget",
8175
8614
  "status",