primitive-admin 1.1.0-alpha.84 → 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.
- package/README.md +43 -1
- package/dist/src/commands/admins.js +29 -32
- package/dist/src/commands/admins.js.map +1 -1
- package/dist/src/commands/analytics.js +2 -1
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.js +12 -5
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/blob-buckets.js +2 -1
- package/dist/src/commands/blob-buckets.js.map +1 -1
- package/dist/src/commands/catalog.js +26 -10
- package/dist/src/commands/catalog.js.map +1 -1
- package/dist/src/commands/collection-type-configs.js +2 -1
- package/dist/src/commands/collection-type-configs.js.map +1 -1
- package/dist/src/commands/collections.js +27 -18
- package/dist/src/commands/collections.js.map +1 -1
- package/dist/src/commands/connections.js +8 -9
- package/dist/src/commands/connections.js.map +1 -1
- package/dist/src/commands/cron-triggers.js +2 -1
- package/dist/src/commands/cron-triggers.js.map +1 -1
- package/dist/src/commands/database-type-configs.js +3 -2
- package/dist/src/commands/database-type-configs.js.map +1 -1
- package/dist/src/commands/databases.js +16 -8
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.js +23 -11
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/email-templates.js +2 -1
- package/dist/src/commands/email-templates.js.map +1 -1
- package/dist/src/commands/env.js +11 -1
- package/dist/src/commands/env.js.map +1 -1
- package/dist/src/commands/feature-flags.js +2 -1
- package/dist/src/commands/feature-flags.js.map +1 -1
- package/dist/src/commands/functions.js +2 -1
- package/dist/src/commands/functions.js.map +1 -1
- package/dist/src/commands/group-type-configs.js +2 -1
- package/dist/src/commands/group-type-configs.js.map +1 -1
- package/dist/src/commands/groups.js +29 -8
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/guides.js +17 -11
- package/dist/src/commands/guides.js.map +1 -1
- package/dist/src/commands/integrations.js +26 -9
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/locks.js +2 -1
- package/dist/src/commands/locks.js.map +1 -1
- package/dist/src/commands/metadata-category-configs.js +2 -1
- package/dist/src/commands/metadata-category-configs.js.map +1 -1
- package/dist/src/commands/metadata.js +8 -1
- package/dist/src/commands/metadata.js.map +1 -1
- package/dist/src/commands/prompts.js +28 -11
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/rule-sets.js +2 -1
- package/dist/src/commands/rule-sets.js.map +1 -1
- package/dist/src/commands/scripts.js +15 -6
- package/dist/src/commands/scripts.js.map +1 -1
- package/dist/src/commands/secrets.js +2 -1
- package/dist/src/commands/secrets.js.map +1 -1
- package/dist/src/commands/sessions.js +10 -9
- package/dist/src/commands/sessions.js.map +1 -1
- package/dist/src/commands/sync.d.ts +64 -0
- package/dist/src/commands/sync.js +347 -17
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/tokens.js +2 -1
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/commands/users.js +16 -9
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/vars.js +2 -1
- package/dist/src/commands/vars.js.map +1 -1
- package/dist/src/commands/waitlist.js +2 -1
- package/dist/src/commands/waitlist.js.map +1 -1
- package/dist/src/commands/webhooks.js +17 -10
- package/dist/src/commands/webhooks.js.map +1 -1
- package/dist/src/commands/workflows.js +36 -18
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +83 -1
- package/dist/src/lib/api-client.js +139 -55
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/config-object-descriptor.js +29 -7
- package/dist/src/lib/config-object-descriptor.js.map +1 -1
- package/dist/src/lib/config-payload.js +27 -2
- package/dist/src/lib/config-payload.js.map +1 -1
- package/dist/src/lib/config-surface.d.ts +2 -1
- package/dist/src/lib/config-surface.js +50 -3
- package/dist/src/lib/config-surface.js.map +1 -1
- package/dist/src/lib/generated-config-surfaces.d.ts +182 -1
- package/dist/src/lib/generated-config-surfaces.js +441 -2
- package/dist/src/lib/generated-config-surfaces.js.map +1 -1
- package/dist/src/lib/list-options.d.ts +68 -0
- package/dist/src/lib/list-options.js +89 -0
- package/dist/src/lib/list-options.js.map +1 -0
- package/dist/src/lib/log-inspection.d.ts +16 -0
- package/dist/src/lib/log-inspection.js +23 -0
- package/dist/src/lib/log-inspection.js.map +1 -1
- package/dist/src/lib/paginate.d.ts +15 -0
- package/dist/src/lib/paginate.js +17 -0
- package/dist/src/lib/paginate.js.map +1 -1
- package/package.json +2 -2
|
@@ -32,9 +32,12 @@ import { buildWorkflowPayloadFromToml, parseWorkflowExpressionsToml, } from "../
|
|
|
32
32
|
// payload builder so the diff normalizer and the wire cannot disagree.
|
|
33
33
|
import { WORKFLOW_MODEL_DEFAULTS } from "../lib/workflow-defaults.js";
|
|
34
34
|
// #2644 — the one definition per configuration object, vendored from the server.
|
|
35
|
-
import { BLOB_BUCKET_TABLE, COLLECTION_TYPE_CONFIG_TABLE, CRON_TRIGGER_TABLE, DATABASE_TYPE_TABLE, EMAIL_TEMPLATE_TABLE, GROUP_TYPE_CONFIG_TABLE, INTEGRATION_TABLE, METADATA_CATEGORY_CONFIG_TABLE, PROMPT_CONFIG_TABLE, PROMPT_TABLE, RULE_SET_TABLE, SERVER_FUNCTION_TABLE, TEST_CASE_SURFACE, TEST_CASE_TABLE, WEBHOOK_TABLE, WORKFLOW_TABLE, configDeclaredTypeErrors, fieldServerDefault, findMisshapenTables, findUnknownDocumentKeys, findUnknownServerKeys, findUnknownTomlKeys, misshapenTableMessage, retiredConfigKey, spellsDecimalNumber, unrecognizedPullKeysMessage, unrecognizedPushKeysMessage, unrecognizedPushTablesMessage, warnUnrecognizedServerKeys, } from "../lib/config-surface.js";
|
|
35
|
+
import { BLOB_BUCKET_TABLE, COLLECTION_TYPE_CONFIG_TABLE, CRON_TRIGGER_TABLE, DATABASE_TYPE_TABLE, EMAIL_TEMPLATE_TABLE, GROUP_TYPE_CONFIG_TABLE, INTEGRATION_TABLE, METADATA_CATEGORY_CONFIG_TABLE, PROMPT_CONFIG_TABLE, PROMPT_TABLE, RULE_SET_TABLE, SERVER_FUNCTION_TABLE, TEST_CASE_SURFACE, TEST_CASE_TABLE, WEBHOOK_TABLE, WORKFLOW_TABLE, configDeclaredTypeErrors, fieldServerDefault, findMisshapenTables, findUnknownDocumentKeys, findUnknownGroupedTomlKeys, findUnknownServerKeys, findUnknownTomlKeys, misshapenTableMessage, retiredConfigKey, spellsDecimalNumber, unrecognizedPullKeysMessage, unrecognizedPushKeysMessage, unrecognizedPushTablesMessage, warnUnrecognizedServerKeys, } from "../lib/config-surface.js";
|
|
36
36
|
import { buildPayloadFromToml, projectEntityToPayload, projectRecordToToml, tomlOwned, } from "../lib/config-payload.js";
|
|
37
37
|
import { CONFIG_SURFACES, isEmptyForEmit, } from "../lib/generated-config-surfaces.js";
|
|
38
|
+
// #3626 — the prompt KIND rule, vendored with the definitions so the CLI's
|
|
39
|
+
// preflight refuses exactly what the admin routes refuse.
|
|
40
|
+
import { hoistDeprecatedChatKeys, promptKindOf, resolvePromptKind, } from "../lib/generated-config-surfaces.js";
|
|
38
41
|
// #3179 — the function's two halves: the header payloads and the envelope
|
|
39
42
|
// `config push` sends, and the bytes `config pull` writes back.
|
|
40
43
|
import { applyFunctionPull, buildFunctionConfigPush, createDocumentSchemaDropWarner, documentSchemaRetentionFailure, buildFunctionCreatePayload, buildFunctionUpdatePayload, functionDiffEntity, functionTomlIgnoredKeyWarnings, functionTomlPreflightErrors, planDocumentSchemaPull, planFunctionPull, serializeFunction, } from "../lib/function-sync.js";
|
|
@@ -713,6 +716,20 @@ export function validateVarsFile(parsedVars, options = {}) {
|
|
|
713
716
|
* edit — reported as a conflict instead. `force` skips the conflict guard,
|
|
714
717
|
* matching `config push --force` for every other entity.
|
|
715
718
|
*
|
|
719
|
+
* A var only the SERVER moved is the other half of the same comparison, and it
|
|
720
|
+
* is reported rather than skipped (issue #3714). Its local value still equals
|
|
721
|
+
* the baseline, so there is nothing to push and no conflict — but the skip that
|
|
722
|
+
* follows from "local is unchanged" used to be taken before the snapshot was
|
|
723
|
+
* consulted at all, so the push said nothing about a var edited in the Admin
|
|
724
|
+
* Console and counted it among the skipped-unchanged. The `drifted[]` keys are
|
|
725
|
+
* what every other synced type reaches through `classifyPushChange` →
|
|
726
|
+
* `server-drifted`: the caller prints `DRIFT var: <key>` under "Server drift —
|
|
727
|
+
* not pushed", writes nothing, and leaves the exit code alone. A baseline key
|
|
728
|
+
* the snapshot no longer holds at all is the same one-sided move (the server
|
|
729
|
+
* deleted the row; the local file stands still) and is reported the same way.
|
|
730
|
+
* `--force` is the resolution that keeps the local value, `config pull` the one
|
|
731
|
+
* that takes the server's.
|
|
732
|
+
*
|
|
716
733
|
* Pure and side-effect-free (the caller owns the API calls and sync-state
|
|
717
734
|
* updates) so the guard is unit-testable without a live server. Assumes
|
|
718
735
|
* `parsedVars` has already passed `validateVarsFile` (values are strings). The
|
|
@@ -726,6 +743,7 @@ export function planVarsPush(parsedVars, baseline, remoteVars, options = {}) {
|
|
|
726
743
|
const remoteByKey = new Map(remoteVars.map((r) => [r.key, r]));
|
|
727
744
|
const upserts = [];
|
|
728
745
|
const conflicts = [];
|
|
746
|
+
const drifted = [];
|
|
729
747
|
const deletions = [];
|
|
730
748
|
let skippedCount = 0;
|
|
731
749
|
const localKeys = new Set();
|
|
@@ -733,9 +751,32 @@ export function planVarsPush(parsedVars, baseline, remoteVars, options = {}) {
|
|
|
733
751
|
localKeys.add(key);
|
|
734
752
|
const valueHash = hashVarValue(value);
|
|
735
753
|
const existing = baseline?.[key];
|
|
736
|
-
// Unchanged since last sync —
|
|
737
|
-
// content-hash skip the other file-backed entities use.
|
|
754
|
+
// Unchanged since last sync — nothing to push (unless forced), mirroring
|
|
755
|
+
// the content-hash skip the other file-backed entities use. But "the local
|
|
756
|
+
// file did not move" is only half the comparison: ask the snapshot whether
|
|
757
|
+
// the SERVER moved before calling the pair unchanged (issue #3714). One
|
|
758
|
+
// side having moved is drift, which is reported and not written; only a
|
|
759
|
+
// pair that still agrees is a skipped-unchanged file.
|
|
738
760
|
if (!options.force && existing && existing.contentHash === valueHash) {
|
|
761
|
+
const remote = remoteByKey.get(key);
|
|
762
|
+
if (!remote) {
|
|
763
|
+
// The row is gone from an authoritative snapshot (the caller fails the
|
|
764
|
+
// push closed when the fetch fails, and the `--force` fallback that
|
|
765
|
+
// leaves it empty never reaches here): the server deleted what we last
|
|
766
|
+
// synced while the file stood still. `--force` recreates it from the
|
|
767
|
+
// local value; `config pull` drops the line.
|
|
768
|
+
drifted.push({ key, serverModifiedAt: "deleted" });
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
if (typeof remote.value === "string" &&
|
|
772
|
+
hashVarValue(remote.value) !== existing.contentHash) {
|
|
773
|
+
drifted.push({ key, serverModifiedAt: remote.updatedAt || "unknown" });
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
// Either both sides still hold the last-synced value, or the snapshot
|
|
777
|
+
// carries no readable value for the key and nothing establishes a move.
|
|
778
|
+
// Nothing is written on this path either way, so the only question is
|
|
779
|
+
// what to report, and an unprovable drift is not one to claim.
|
|
739
780
|
skippedCount++;
|
|
740
781
|
continue;
|
|
741
782
|
}
|
|
@@ -823,7 +864,7 @@ export function planVarsPush(parsedVars, baseline, remoteVars, options = {}) {
|
|
|
823
864
|
}
|
|
824
865
|
deletions.push(key);
|
|
825
866
|
}
|
|
826
|
-
return { upserts, deletions, conflicts, skippedCount };
|
|
867
|
+
return { upserts, deletions, conflicts, drifted, skippedCount };
|
|
827
868
|
}
|
|
828
869
|
/**
|
|
829
870
|
* Compute the number of config vars the server would hold after a push plan
|
|
@@ -1771,6 +1812,13 @@ export function serializePrompt(prompt, logger) {
|
|
|
1771
1812
|
topP: (record) => (record.topP != null ? Number(record.topP) : undefined),
|
|
1772
1813
|
outputSchema: jsonField(`Prompt '${prompt?.promptKey}' config '${config?.configName}'`),
|
|
1773
1814
|
providerConfig: jsonField(`Prompt '${prompt?.promptKey}' config '${config?.configName}'`),
|
|
1815
|
+
// #3626, CR3626-002 — the decisions declaration is a `json` field and
|
|
1816
|
+
// goes out the same way its siblings do. Written straight through, a
|
|
1817
|
+
// stored question carrying a `null` anywhere lost that member on pull
|
|
1818
|
+
// (smol-toml has no null), and a `null` inside a LIST threw, taking
|
|
1819
|
+
// `config pull` and `config diff` down with it. The JSON-string
|
|
1820
|
+
// fallback keeps the declaration whole either way.
|
|
1821
|
+
questions: jsonField(`Prompt '${prompt?.promptKey}' config '${config?.configName}'`),
|
|
1774
1822
|
}),
|
|
1775
1823
|
// #2645 — the live config has to be identifiable in the repo. Emitted
|
|
1776
1824
|
// only on the active entry (`undefined` elsewhere so the key is omitted),
|
|
@@ -1843,6 +1891,24 @@ export function buildPromptPushPayload(key, tomlData, { mode = "update" } = {})
|
|
|
1843
1891
|
// dropped and leaving a stale rule live. On create it is omitted so
|
|
1844
1892
|
// the push-adopt path (#1006) can still reach the server's 409.
|
|
1845
1893
|
accessRule: (value) => (create ? value : (value ?? null)),
|
|
1894
|
+
// #3626, CR3626-004 — the kind NORMALIZED, exactly as the resolver
|
|
1895
|
+
// that validates it reads it. `kind = " chat "` and `kind = ""` both
|
|
1896
|
+
// mean `chat` to `resolvePromptKind`, so the server stores `chat` and
|
|
1897
|
+
// the comparison — which projects `[prompt]` in create mode precisely
|
|
1898
|
+
// so a changed kind is visible (SO3626-008) — would otherwise read the
|
|
1899
|
+
// authored spelling against the stored one and call a freshly created
|
|
1900
|
+
// prompt an attempted kind change, refused as immutable on every later
|
|
1901
|
+
// push including `--force`. An UNKNOWN kind is passed through as
|
|
1902
|
+
// written: it is a refusal (preflight, then the create handler), and it
|
|
1903
|
+
// has to name what the file says.
|
|
1904
|
+
kind: (value) => {
|
|
1905
|
+
if (value === undefined || value === null)
|
|
1906
|
+
return "chat";
|
|
1907
|
+
if (typeof value !== "string")
|
|
1908
|
+
return value;
|
|
1909
|
+
const text = value.trim();
|
|
1910
|
+
return text === "" ? "chat" : text;
|
|
1911
|
+
},
|
|
1846
1912
|
},
|
|
1847
1913
|
}),
|
|
1848
1914
|
};
|
|
@@ -1877,11 +1943,19 @@ export function buildPromptCreatePayload(key, tomlData) {
|
|
|
1877
1943
|
for (const owned of ["configName", ...SEEDED_CONFIG_DEFERRED]) {
|
|
1878
1944
|
delete seed[owned];
|
|
1879
1945
|
}
|
|
1946
|
+
const prompt = buildPromptPushPayload(key, tomlData, { mode: "create" });
|
|
1947
|
+
// #3626, SO3626-001 — the placeholder is a CHAT fallback. A decisions prompt
|
|
1948
|
+
// has no template at all (its input is a `state` value plus its questions),
|
|
1949
|
+
// and injecting one would store a chat key the block rule then refuses on
|
|
1950
|
+
// every later update.
|
|
1951
|
+
if (promptKindOf(prompt) === "decisions") {
|
|
1952
|
+
return { ...prompt, ...seed };
|
|
1953
|
+
}
|
|
1880
1954
|
return {
|
|
1881
|
-
...
|
|
1955
|
+
...prompt,
|
|
1882
1956
|
...seed,
|
|
1883
|
-
// The server requires a template; keep the historical
|
|
1884
|
-
// prompt authored without one still creates.
|
|
1957
|
+
// The server requires a template for a chat config; keep the historical
|
|
1958
|
+
// placeholder so a prompt authored without one still creates.
|
|
1885
1959
|
userPromptTemplate: seed.userPromptTemplate || "{{ input }}",
|
|
1886
1960
|
};
|
|
1887
1961
|
}
|
|
@@ -1925,9 +1999,14 @@ export function seededOnlyPromptConfigFields(firstConfig, seededConfigName = "de
|
|
|
1925
1999
|
const patch = {};
|
|
1926
2000
|
for (const field of SEEDED_CONFIG_DEFERRED) {
|
|
1927
2001
|
const value = full[field];
|
|
1928
|
-
//
|
|
1929
|
-
//
|
|
1930
|
-
|
|
2002
|
+
// #3626 — an omitted value CLEARS. The create handler does not merely skip
|
|
2003
|
+
// `description`, it writes the hard-coded "Default configuration", so
|
|
2004
|
+
// leaving it alone left every newly-pushed prompt reporting `Modified`
|
|
2005
|
+
// forever against a description its file never asked for: push's own
|
|
2006
|
+
// "the file is the whole truth" rule (#1567), broken by the one field the
|
|
2007
|
+
// create body cannot carry. `outputSchema` is unaffected — create never
|
|
2008
|
+
// sets the seeded config's, so the null is a no-op there.
|
|
2009
|
+
if (value === undefined)
|
|
1931
2010
|
continue;
|
|
1932
2011
|
patch[field] = value;
|
|
1933
2012
|
}
|
|
@@ -1958,7 +2037,15 @@ export function buildPromptConfigPayload(tomlConfig, { mode = "update" } = {}) {
|
|
|
1958
2037
|
* differ from the one the server has always received.
|
|
1959
2038
|
*/
|
|
1960
2039
|
const tomlOwned = (value) => (create ? value : (value ?? null));
|
|
1961
|
-
|
|
2040
|
+
// #3626 §4 — the nine chat keys moved under `[configs.chat]`, and the flat
|
|
2041
|
+
// spelling is accepted as DEPRECATED. Hoisting here rather than at each call
|
|
2042
|
+
// site means push, `config diff` and the create-seed builder all read a file
|
|
2043
|
+
// the same way, so an unmigrated file and its pulled rewrite compare Synced.
|
|
2044
|
+
// A key set BOTH ways is refused in preflight
|
|
2045
|
+
// (`promptDeprecatedChatKeyErrors`); the hoist leaves the grouped value
|
|
2046
|
+
// standing so nothing here has to guess.
|
|
2047
|
+
const entry = hoistDeprecatedChatKeys(tomlConfig).entry;
|
|
2048
|
+
return buildPayloadFromToml(PROMPT_CONFIG_TABLE, entry, {
|
|
1962
2049
|
mode,
|
|
1963
2050
|
values: {
|
|
1964
2051
|
provider: (value) => (create ? value || "openrouter" : value),
|
|
@@ -1978,6 +2065,10 @@ export function buildPromptConfigPayload(tomlConfig, { mode = "update" } = {}) {
|
|
|
1978
2065
|
// the merged config refused as "both set".
|
|
1979
2066
|
reasoningEffort: tomlOwned,
|
|
1980
2067
|
reasoningBudget: tomlOwned,
|
|
2068
|
+
// #3626, SO3626-002 — the decisions declaration is TOML-owned on the
|
|
2069
|
+
// same terms: deleting `[configs.decisions]` has to clear `questions`
|
|
2070
|
+
// server-side, or the file stops describing what the model is asked.
|
|
2071
|
+
questions: tomlOwned,
|
|
1981
2072
|
},
|
|
1982
2073
|
});
|
|
1983
2074
|
}
|
|
@@ -2053,11 +2144,19 @@ export function configUnknownKeyError(filePath, tomlData, table) {
|
|
|
2053
2144
|
: []
|
|
2054
2145
|
: [at];
|
|
2055
2146
|
const unknown = new Set();
|
|
2147
|
+
// #3626 — a key inside a declared `[configs.<group>]` block that no field
|
|
2148
|
+
// declares under it, and a group value that is not a table at all. Reported
|
|
2149
|
+
// with the group in the path (`configs.chat.nope`) so the author is told
|
|
2150
|
+
// where the CLI looked.
|
|
2151
|
+
const groupedUnknown = new Set();
|
|
2056
2152
|
for (const entry of entries) {
|
|
2057
2153
|
for (const key of findUnknownTomlKeys(entry, table))
|
|
2058
2154
|
unknown.add(key);
|
|
2155
|
+
for (const key of findUnknownGroupedTomlKeys(entry, table)) {
|
|
2156
|
+
groupedUnknown.add(key);
|
|
2157
|
+
}
|
|
2059
2158
|
}
|
|
2060
|
-
if (unknown.size === 0)
|
|
2159
|
+
if (unknown.size === 0 && groupedUnknown.size === 0)
|
|
2061
2160
|
return null;
|
|
2062
2161
|
// #2645/#2803 — a key this CLI removed on purpose gets the guidance stated
|
|
2063
2162
|
// once beside the definitions (`src/config-surface/retired-keys.ts`), not the
|
|
@@ -2074,8 +2173,32 @@ export function configUnknownKeyError(filePath, tomlData, table) {
|
|
|
2074
2173
|
for (const key of retiredHere) {
|
|
2075
2174
|
messages.push(`${filePath}: ${prefix}.${key} — ${retiredConfigKey(prefix, key).toml}`);
|
|
2076
2175
|
}
|
|
2077
|
-
|
|
2078
|
-
|
|
2176
|
+
// #3626, SO3626-013 — a key the definition DOES declare, written at the
|
|
2177
|
+
// table root when it is authored under a group. Before grouping,
|
|
2178
|
+
// `acceptedTomlKeys` took every field key at the root, so a flat `questions`
|
|
2179
|
+
// passed this check and was then dropped in silence by the payload builder.
|
|
2180
|
+
// It is an unknown key now, and the message says where it belongs rather
|
|
2181
|
+
// than sending the author to the generic "check the spelling" hint.
|
|
2182
|
+
const misplaced = trulyUnknown
|
|
2183
|
+
.map((key) => ({
|
|
2184
|
+
key,
|
|
2185
|
+
group: table.fields.find((f) => f.tomlKey === key && f.tomlGroup)
|
|
2186
|
+
?.tomlGroup,
|
|
2187
|
+
}))
|
|
2188
|
+
.filter((entry) => !!entry.group);
|
|
2189
|
+
for (const { key, group } of misplaced) {
|
|
2190
|
+
messages.push(`${filePath}: ${prefix}[0].${key} is not a [[${prefix}]] key; ` +
|
|
2191
|
+
`write it under [${prefix}.${group}]`);
|
|
2192
|
+
}
|
|
2193
|
+
const misplacedKeys = new Set(misplaced.map((m) => m.key));
|
|
2194
|
+
const stillUnknown = trulyUnknown.filter((k) => !misplacedKeys.has(k));
|
|
2195
|
+
if (stillUnknown.length > 0) {
|
|
2196
|
+
messages.push(unrecognizedPushKeysMessage(filePath, table.tomlPath, stillUnknown));
|
|
2197
|
+
}
|
|
2198
|
+
if (groupedUnknown.size > 0) {
|
|
2199
|
+
messages.push(unrecognizedPushKeysMessage(filePath, table.tomlPath, [
|
|
2200
|
+
...groupedUnknown,
|
|
2201
|
+
].sort()));
|
|
2079
2202
|
}
|
|
2080
2203
|
return messages.join("\n");
|
|
2081
2204
|
}
|
|
@@ -2507,6 +2630,28 @@ export async function runConfigPushPreflight(input) {
|
|
|
2507
2630
|
preflightValidationErrors.push(` ${message}`);
|
|
2508
2631
|
}
|
|
2509
2632
|
}
|
|
2633
|
+
// #3626 §4 — one advisory line per prompt file still written in the flat
|
|
2634
|
+
// chat spelling, beside the `celContextAccess` notice above. The push
|
|
2635
|
+
// continues and the keys are applied; `config pull` rewrites the file, so a
|
|
2636
|
+
// single pull-then-push clears the notice. Printed even under `--dry-run`,
|
|
2637
|
+
// which never reaches the apply — the notice is about the FILE.
|
|
2638
|
+
const preflightPromptsDir = join(configDir, "prompts");
|
|
2639
|
+
if (existsSync(preflightPromptsDir)) {
|
|
2640
|
+
for (const file of selectFiles("prompt", preflightPromptsDir, readdirSync(preflightPromptsDir).filter((f) => f.endsWith(".toml")))) {
|
|
2641
|
+
const filePath = join(preflightPromptsDir, file);
|
|
2642
|
+
let tomlData;
|
|
2643
|
+
try {
|
|
2644
|
+
tomlData = parseTomlFile(filePath);
|
|
2645
|
+
}
|
|
2646
|
+
catch {
|
|
2647
|
+
// A file that does not parse is reported by the collector above.
|
|
2648
|
+
continue;
|
|
2649
|
+
}
|
|
2650
|
+
const notice = promptFlatChatKeyWarning(filePath, tomlData);
|
|
2651
|
+
if (notice)
|
|
2652
|
+
warn(` ${notice}`);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2510
2655
|
// The `<key>.tests/*.toml` sidecars, which sit beside the block they
|
|
2511
2656
|
// test rather than in a type directory of their own. Same posture,
|
|
2512
2657
|
// reached through the registry's `test-case` surface — and through the
|
|
@@ -2546,6 +2691,127 @@ export async function runConfigPushPreflight(input) {
|
|
|
2546
2691
|
* whose spelling is not the declared type. A file that does not parse is left
|
|
2547
2692
|
* to the per-type loop that reads it, which names the parse error itself.
|
|
2548
2693
|
*/
|
|
2694
|
+
/**
|
|
2695
|
+
* A prompt file's KIND errors — #3626, decidable from the file alone.
|
|
2696
|
+
*
|
|
2697
|
+
* Three refusals, all of them the same rule seen from different angles: a key
|
|
2698
|
+
* set both flat and under `[configs.chat]` (the hoist will not guess), a
|
|
2699
|
+
* `[[configs]]` entry carrying the block of the OTHER kind, and a malformed
|
|
2700
|
+
* decisions declaration. The last two come from `resolvePromptKind`, the same
|
|
2701
|
+
* vendored function the admin routes validate with, so the CLI's refusal and
|
|
2702
|
+
* the server's cannot disagree — and this one lands in the preflight, before
|
|
2703
|
+
* the first mutating call.
|
|
2704
|
+
*/
|
|
2705
|
+
export function promptKindFileErrors(filePath, tomlData) {
|
|
2706
|
+
const entries = Array.isArray(tomlData?.configs) ? tomlData.configs : [];
|
|
2707
|
+
const errors = [];
|
|
2708
|
+
for (const [index, entry] of entries.entries()) {
|
|
2709
|
+
const hoisted = hoistDeprecatedChatKeys(entry);
|
|
2710
|
+
for (const key of hoisted.conflicts) {
|
|
2711
|
+
errors.push(`${filePath}: configs[${index}].${key} is set both flat and under ` +
|
|
2712
|
+
`[configs.chat]; keep the [configs.chat] spelling`);
|
|
2713
|
+
}
|
|
2714
|
+
const chat = hoisted.entry.chat ?? {};
|
|
2715
|
+
const decisions = hoisted.entry.decisions ?? {};
|
|
2716
|
+
// #3626 — the FIRST entry of a chat prompt carries the create seed's
|
|
2717
|
+
// placeholder. `buildPromptCreatePayload` sends `{{ input }}` when the
|
|
2718
|
+
// file names no template, which is how a prompt authored without one has
|
|
2719
|
+
// always created; refusing it here would break a file that pushed before
|
|
2720
|
+
// the kind existed. An entry that really names none on the UPDATE path is
|
|
2721
|
+
// still refused, by the server, with this same message.
|
|
2722
|
+
//
|
|
2723
|
+
// CR3626-005 — the fallback is `||`, not `??`, because that is what
|
|
2724
|
+
// `buildPromptCreatePayload` applies (`seed.userPromptTemplate ||
|
|
2725
|
+
// "{{ input }}"`). `config create prompt` scaffolds an EMPTY
|
|
2726
|
+
// `userPromptTemplate`, so reading absence more narrowly than the builder
|
|
2727
|
+
// does refused the file this CLI had just written, for a value the create
|
|
2728
|
+
// would have replaced with the placeholder anyway.
|
|
2729
|
+
const seedsTemplate = index === 0 && promptKindOf(tomlData?.prompt) !== "decisions";
|
|
2730
|
+
const userPromptTemplate = seedsTemplate
|
|
2731
|
+
? chat.userPromptTemplate || "{{ input }}"
|
|
2732
|
+
: chat.userPromptTemplate;
|
|
2733
|
+
const resolution = resolvePromptKind({
|
|
2734
|
+
kind: tomlData?.prompt?.kind,
|
|
2735
|
+
provider: hoisted.entry.provider,
|
|
2736
|
+
model: hoisted.entry.model,
|
|
2737
|
+
config: {
|
|
2738
|
+
systemPrompt: chat.systemPrompt,
|
|
2739
|
+
userPromptTemplate,
|
|
2740
|
+
temperature: chat.temperature,
|
|
2741
|
+
topP: chat.topP,
|
|
2742
|
+
maxTokens: chat.maxTokens,
|
|
2743
|
+
outputFormat: chat.outputFormat,
|
|
2744
|
+
outputSchema: chat.outputSchema,
|
|
2745
|
+
reasoningEffort: chat.reasoningEffort,
|
|
2746
|
+
reasoningBudget: chat.reasoningBudget,
|
|
2747
|
+
questions: decisions.questions,
|
|
2748
|
+
},
|
|
2749
|
+
});
|
|
2750
|
+
if (resolution.ok === false) {
|
|
2751
|
+
errors.push(`${filePath}: configs[${index}] — ${resolution.error}`);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
return errors;
|
|
2755
|
+
}
|
|
2756
|
+
/**
|
|
2757
|
+
* The immutable-`kind` refusal for a prompt update the comparison gate did not
|
|
2758
|
+
* decide — #3626, CR3626-001.
|
|
2759
|
+
*
|
|
2760
|
+
* `kind` is create-only (D3626-008), and the gate refuses a change to it on
|
|
2761
|
+
* every path that compares the two sides. Two update paths do not compare: a
|
|
2762
|
+
* prompt whose DETAIL read failed falls back to the manifest byte-hash gate,
|
|
2763
|
+
* which `--force` writes straight through, and a prompt ADOPTED after a
|
|
2764
|
+
* create's 409 is re-issued as an update without a comparison at all. Both
|
|
2765
|
+
* would otherwise PATCH the file's schemas, access rule and metadata onto a
|
|
2766
|
+
* row of the other kind, and only then have its configs refused against the
|
|
2767
|
+
* kind the update cannot change.
|
|
2768
|
+
*
|
|
2769
|
+
* Returns the gate's own outcome shape, so `recordDeclined` prints the same
|
|
2770
|
+
* two values and the same remedy — create the prompt under a new key.
|
|
2771
|
+
*
|
|
2772
|
+
* `null` when the kinds agree and when the server's kind is simply not KNOWN:
|
|
2773
|
+
* a push that could not read live state at all has nothing to compare, and
|
|
2774
|
+
* that path's contract (`decideDegradedPush`, #2880) is already "decline
|
|
2775
|
+
* unless the operator forces it", stated for every field at once.
|
|
2776
|
+
*/
|
|
2777
|
+
export function promptKindImmutableOutcome(storedKind, localDoc) {
|
|
2778
|
+
if (!storedKind)
|
|
2779
|
+
return null;
|
|
2780
|
+
const localKind = promptKindOf(localDoc?.prompt);
|
|
2781
|
+
if (localKind === storedKind)
|
|
2782
|
+
return null;
|
|
2783
|
+
return {
|
|
2784
|
+
action: "immutable",
|
|
2785
|
+
// The hashes are the gate's evidence for WHICH side moved, and this
|
|
2786
|
+
// refusal does not depend on that: the field cannot be written either way.
|
|
2787
|
+
localHash: "",
|
|
2788
|
+
remoteHash: "",
|
|
2789
|
+
fields: [{ field: "kind", local: localKind, server: storedKind }],
|
|
2790
|
+
immutableFields: ["kind"],
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
/**
|
|
2794
|
+
* The one deprecation notice a prompt file using the FLAT chat spelling earns
|
|
2795
|
+
* — #3626 §4, beside the `celContextAccess` notice's precedent.
|
|
2796
|
+
*
|
|
2797
|
+
* Advisory: the push continues and the keys are applied. `null` when the file
|
|
2798
|
+
* is already written in the grouped spelling, so a migrated tree is silent.
|
|
2799
|
+
*/
|
|
2800
|
+
export function promptFlatChatKeyWarning(filePath, tomlData) {
|
|
2801
|
+
const entries = Array.isArray(tomlData?.configs) ? tomlData.configs : [];
|
|
2802
|
+
const moved = new Set();
|
|
2803
|
+
for (const entry of entries) {
|
|
2804
|
+
for (const key of hoistDeprecatedChatKeys(entry).moved)
|
|
2805
|
+
moved.add(key);
|
|
2806
|
+
}
|
|
2807
|
+
if (moved.size === 0)
|
|
2808
|
+
return null;
|
|
2809
|
+
const named = [...moved].sort().join(", ");
|
|
2810
|
+
return (`${filePath}: ${named} ${moved.size === 1 ? "is" : "are"} written flat ` +
|
|
2811
|
+
"under [[configs]]; this spelling is deprecated — move them under " +
|
|
2812
|
+
"[configs.chat] (`config pull` rewrites the file). Rejecting the flat " +
|
|
2813
|
+
"spelling is tracked in #3642.");
|
|
2814
|
+
}
|
|
2549
2815
|
export function collectConfigFileValidationErrors(configDir) {
|
|
2550
2816
|
const found = [];
|
|
2551
2817
|
for (const { dir, surface, tables } of unknownKeyPreflightTargets()) {
|
|
@@ -2581,6 +2847,12 @@ export function collectConfigFileValidationErrors(configDir) {
|
|
|
2581
2847
|
for (const table of surface.tables) {
|
|
2582
2848
|
messages.push(...configDeclaredTypeErrors(filePath, tomlData, table));
|
|
2583
2849
|
}
|
|
2850
|
+
// #3626 — the prompt kind's own file-decidable refusals, run by the same
|
|
2851
|
+
// collector so `config push` and `config diff` cannot disagree about
|
|
2852
|
+
// which prompt files are pushable.
|
|
2853
|
+
if (surface.label === "prompt") {
|
|
2854
|
+
messages.push(...promptKindFileErrors(filePath, tomlData));
|
|
2855
|
+
}
|
|
2584
2856
|
if (messages.length === 0)
|
|
2585
2857
|
continue;
|
|
2586
2858
|
found.push({
|
|
@@ -4055,7 +4327,17 @@ const CONFIG_DIFF_SPECS = {
|
|
|
4055
4327
|
// (`[prompt].key`) is still read here, because it is what `displayName`
|
|
4056
4328
|
// falls back to. Diff injects the resolved key into a file that omits it,
|
|
4057
4329
|
// exactly as push derives it from the file name.
|
|
4058
|
-
|
|
4330
|
+
//
|
|
4331
|
+
// #3626, SO3626-008 — projected in CREATE mode. `kind` is `writableOn:
|
|
4332
|
+
// ["create"]`, and `buildPayloadFromToml` drops a field not writable in the
|
|
4333
|
+
// mode, so an update-mode projection would leave a CHANGED kind out of the
|
|
4334
|
+
// compared surface entirely: `findImmutableFieldDiffs` would never see it,
|
|
4335
|
+
// diff would call the file Synced, and push would start applying the
|
|
4336
|
+
// prompt's other changes before the server refused a config against the
|
|
4337
|
+
// unchanged kind. The blob-bucket comparator projects in create mode for
|
|
4338
|
+
// exactly this reason (`ttlTier`, below). PATCH payloads still omit `kind`
|
|
4339
|
+
// — this is the comparison's projection, not push's.
|
|
4340
|
+
parse: (doc) => buildPromptPushPayload(doc?.prompt?.key ?? "", doc, { mode: "create" }),
|
|
4059
4341
|
serialize: (record) => serializePrompt(record),
|
|
4060
4342
|
// Read from the DOCUMENT, not the parsed entity: `[[configs]]` is a sibling
|
|
4061
4343
|
// table of `[prompt]`, and the prompt's own push payload does not carry it
|
|
@@ -8908,6 +9190,9 @@ What a push guarantees (two stages):
|
|
|
8908
9190
|
upserts: varPlan.upserts.filter((u) => wanted.has(u.key)),
|
|
8909
9191
|
deletions: varPlan.deletions.filter((k) => wanted.has(k)),
|
|
8910
9192
|
conflicts: varPlan.conflicts.filter((c) => wanted.has(c.key)),
|
|
9193
|
+
// A drifted var nobody selected is not this push's business to
|
|
9194
|
+
// report, the same way its writes are not this push's to make.
|
|
9195
|
+
drifted: varPlan.drifted.filter((d) => wanted.has(d.key)),
|
|
8911
9196
|
skippedCount: 0,
|
|
8912
9197
|
};
|
|
8913
9198
|
}
|
|
@@ -9200,6 +9485,13 @@ What a push guarantees (two stages):
|
|
|
9200
9485
|
localModifiedAt: c.localModifiedAt,
|
|
9201
9486
|
});
|
|
9202
9487
|
}
|
|
9488
|
+
// …and var drift into the shared drift accumulator (#3714), so a var
|
|
9489
|
+
// only the server moved prints the `DRIFT var: KEY` row every other
|
|
9490
|
+
// type prints for that shape. It is not a failure: nothing was
|
|
9491
|
+
// written and neither the exit code nor the pushed count moves.
|
|
9492
|
+
for (const d of varPlan.drifted) {
|
|
9493
|
+
drifted.push({ type: "var", key: d.key });
|
|
9494
|
+
}
|
|
9203
9495
|
// Delete removed keys BEFORE upserting new ones (issue #1423 review
|
|
9204
9496
|
// pass 3). At the server's 100-var cap, a replacement (remove one key,
|
|
9205
9497
|
// add another) is a valid final state, but upserting the new key first
|
|
@@ -10502,13 +10794,26 @@ What a push guarantees (two stages):
|
|
|
10502
10794
|
// manifest byte-hash gate with the type named, like every other
|
|
10503
10795
|
// converted type.
|
|
10504
10796
|
const livePromptIds = new Map();
|
|
10797
|
+
// #3626, CR3626-001 — the KIND the server holds for each prompt,
|
|
10798
|
+
// taken from the same list. `kind` is immutable after create
|
|
10799
|
+
// (D3626-008), and the comparison gate refuses a change to it — but
|
|
10800
|
+
// only on the paths that reach that gate. This map is what the other
|
|
10801
|
+
// two read: a prompt whose DETAIL read failed (degraded to the
|
|
10802
|
+
// byte-hash gate, which `--force` writes through) and a prompt
|
|
10803
|
+
// adopted by key after a 409 (which re-issues the file as an UPDATE
|
|
10804
|
+
// without ever comparing). Both would otherwise PATCH the prompt's
|
|
10805
|
+
// schemas, access rule and metadata onto a row of the other kind
|
|
10806
|
+
// before a config was refused against the kind that did not change.
|
|
10807
|
+
const livePromptKinds = new Map();
|
|
10505
10808
|
let promptsLiveOk = true;
|
|
10506
10809
|
if (files.length > 0) {
|
|
10507
10810
|
try {
|
|
10508
10811
|
const listed = await fetchAll((p) => client.listPrompts(resolvedAppId, p));
|
|
10509
10812
|
for (const p of listed) {
|
|
10510
|
-
if (p?.promptKey)
|
|
10511
|
-
|
|
10813
|
+
if (!p?.promptKey)
|
|
10814
|
+
continue;
|
|
10815
|
+
livePromptIds.set(p.promptKey, p.promptId);
|
|
10816
|
+
livePromptKinds.set(p.promptKey, promptKindOf(p));
|
|
10512
10817
|
}
|
|
10513
10818
|
}
|
|
10514
10819
|
catch (err) {
|
|
@@ -10831,6 +11136,18 @@ What a push guarantees (two stages):
|
|
|
10831
11136
|
}
|
|
10832
11137
|
};
|
|
10833
11138
|
if (existingId) {
|
|
11139
|
+
// #3626, CR3626-001 — the kind, before anything is written. The
|
|
11140
|
+
// comparison gate above already refuses a changed `kind`, but it
|
|
11141
|
+
// only ran when the DETAIL read succeeded; a prompt degraded to
|
|
11142
|
+
// the byte-hash gate reaches this line with `--force` and no
|
|
11143
|
+
// comparison at all. The list's kind is enough to answer it, and
|
|
11144
|
+
// answering it here keeps the promise the gate makes: a kind
|
|
11145
|
+
// change is declined before the first mutating call.
|
|
11146
|
+
const degradedKindDecline = promptKindImmutableOutcome(livePrompt ? promptKindOf(livePrompt) : livePromptKinds.get(key), tomlData);
|
|
11147
|
+
if (degradedKindDecline) {
|
|
11148
|
+
recordDeclined("prompt", key, degradedKindDecline, existingEntry?.modifiedAt);
|
|
11149
|
+
continue;
|
|
11150
|
+
}
|
|
10834
11151
|
// Update existing prompt
|
|
10835
11152
|
changes.push({ type: "prompt", action: "update", key });
|
|
10836
11153
|
promptKeyToId.set(key, existingId);
|
|
@@ -10923,6 +11240,19 @@ What a push guarantees (two stages):
|
|
|
10923
11240
|
});
|
|
10924
11241
|
adopted = true;
|
|
10925
11242
|
const adoptedId = existing.promptId;
|
|
11243
|
+
// #3626, CR3626-001 — the adopted prompt's KIND, before the
|
|
11244
|
+
// re-issue writes anything. This path never reaches the
|
|
11245
|
+
// comparison gate (the create is what failed), so without
|
|
11246
|
+
// this an orphaned chat prompt would take a decisions file's
|
|
11247
|
+
// schemas, access rule and metadata by PATCH and only then
|
|
11248
|
+
// refuse its configs against the kind the update cannot
|
|
11249
|
+
// change. The lookup already read the record, so the answer
|
|
11250
|
+
// costs nothing.
|
|
11251
|
+
const adoptKindDecline = promptKindImmutableOutcome(promptKindOf(existing), tomlData);
|
|
11252
|
+
if (adoptKindDecline) {
|
|
11253
|
+
recordDeclined("prompt", key, adoptKindDecline);
|
|
11254
|
+
continue;
|
|
11255
|
+
}
|
|
10926
11256
|
// Seed the sync-state entity record so the shared update body
|
|
10927
11257
|
// can stamp modifiedAt/contentHash into it (sync state has no
|
|
10928
11258
|
// entry for an orphaned prompt), and track the key→ID map.
|