primitive-admin 1.1.0-alpha.65 → 1.1.0-alpha.66

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 (54) hide show
  1. package/README.md +4 -4
  2. package/assets/skill/skills/primitive-platform/SKILL.md +15 -7
  3. package/dist/bin/primitive.js +4 -3
  4. package/dist/bin/primitive.js.map +1 -1
  5. package/dist/src/commands/apps.js +14 -5
  6. package/dist/src/commands/apps.js.map +1 -1
  7. package/dist/src/commands/env.d.ts +12 -1
  8. package/dist/src/commands/env.js +96 -21
  9. package/dist/src/commands/env.js.map +1 -1
  10. package/dist/src/commands/init.d.ts +1 -1
  11. package/dist/src/commands/init.js +78 -25
  12. package/dist/src/commands/init.js.map +1 -1
  13. package/dist/src/commands/sync-app-settings.d.ts +83 -55
  14. package/dist/src/commands/sync-app-settings.js +131 -200
  15. package/dist/src/commands/sync-app-settings.js.map +1 -1
  16. package/dist/src/commands/sync.d.ts +240 -28
  17. package/dist/src/commands/sync.js +2022 -309
  18. package/dist/src/commands/sync.js.map +1 -1
  19. package/dist/src/commands/workflows.js +14 -3
  20. package/dist/src/commands/workflows.js.map +1 -1
  21. package/dist/src/lib/app-settings-descriptor.d.ts +143 -15
  22. package/dist/src/lib/app-settings-descriptor.js +315 -28
  23. package/dist/src/lib/app-settings-descriptor.js.map +1 -1
  24. package/dist/src/lib/config-payload.js +14 -3
  25. package/dist/src/lib/config-payload.js.map +1 -1
  26. package/dist/src/lib/config-surface.d.ts +37 -1
  27. package/dist/src/lib/config-surface.js +118 -0
  28. package/dist/src/lib/config-surface.js.map +1 -1
  29. package/dist/src/lib/env-resolver-core.d.ts +147 -0
  30. package/dist/src/lib/env-resolver-core.js +265 -0
  31. package/dist/src/lib/env-resolver-core.js.map +1 -0
  32. package/dist/src/lib/env-resolver.d.ts +28 -6
  33. package/dist/src/lib/env-resolver.js +44 -32
  34. package/dist/src/lib/env-resolver.js.map +1 -1
  35. package/dist/src/lib/generated-config-surfaces.d.ts +33 -0
  36. package/dist/src/lib/generated-config-surfaces.js +151 -49
  37. package/dist/src/lib/generated-config-surfaces.js.map +1 -1
  38. package/dist/src/lib/integration-request-config.d.ts +30 -0
  39. package/dist/src/lib/integration-request-config.js +145 -0
  40. package/dist/src/lib/integration-request-config.js.map +1 -0
  41. package/dist/src/lib/local-state.d.ts +55 -0
  42. package/dist/src/lib/local-state.js +167 -0
  43. package/dist/src/lib/local-state.js.map +1 -0
  44. package/dist/src/lib/server-text-normalization.d.ts +51 -0
  45. package/dist/src/lib/server-text-normalization.js +90 -0
  46. package/dist/src/lib/server-text-normalization.js.map +1 -0
  47. package/dist/src/lib/sync-resource-types.d.ts +8 -0
  48. package/dist/src/lib/sync-resource-types.js +8 -0
  49. package/dist/src/lib/sync-resource-types.js.map +1 -1
  50. package/dist/src/lib/template.d.ts +27 -0
  51. package/dist/src/lib/template.js +46 -20
  52. package/dist/src/lib/template.js.map +1 -1
  53. package/dist/src/types/index.d.ts +31 -7
  54. package/package.json +2 -2
@@ -784,6 +784,32 @@ export declare function unknownKeyPreflightTargets(): Array<{
784
784
  surface: ConfigObjectSurface;
785
785
  tables: ConfigTable[];
786
786
  }>;
787
+ /** One config file the TOML preflight rejects, and the row it belongs to. */
788
+ export interface ConfigFileValidationError {
789
+ /** The `config diff` row type, e.g. `prompt`. */
790
+ type: string;
791
+ /** The row key the local file pairs on. */
792
+ key: string;
793
+ filePath: string;
794
+ /** The messages, verbatim — the same text `config push` aborts with. */
795
+ messages: string[];
796
+ }
797
+ /**
798
+ * Every per-entity config file whose TOML `config push` would refuse (#2880
799
+ * criterion 1, symmetry).
800
+ *
801
+ * One collector, two commands. Push aborts on the messages before issuing a
802
+ * request; `config diff` renders them as their own rows, because the failure
803
+ * this closes is a file diff calls Synced and push then rejects — the state the
804
+ * issue's reporter hit after #2803 retired `[prompt].status`, where a local key
805
+ * with no server counterpart is invisible to a comparison and fatal to a push.
806
+ *
807
+ * Three checks, all definition-driven: the document's table shape, keys no
808
+ * definition declares (retired keys among them), and — since #2880 — values
809
+ * whose spelling is not the declared type. A file that does not parse is left
810
+ * to the per-type loop that reads it, which names the parse error itself.
811
+ */
812
+ export declare function collectConfigFileValidationErrors(configDir: string): ConfigFileValidationError[];
787
813
  /**
788
814
  * Every `<key>.tests/*.toml` sidecar under a config directory.
789
815
  *
@@ -1108,12 +1134,47 @@ export interface ConfigDiffMaps {
1108
1134
  export interface ConfigDiffSpec {
1109
1135
  label: string;
1110
1136
  table: ConfigTable;
1111
- /** TOML doc -> the wire-shaped entity (the same parse `config push` uses). */
1112
- parse(doc: any): any;
1137
+ /**
1138
+ * TOML doc -> the wire-shaped entity (the same parse `config push` uses).
1139
+ *
1140
+ * `extra` is the per-type context the projection needs and the document does
1141
+ * not carry (#2880): a test case's id→name lookups, through which the two
1142
+ * spellings of a reference meet. Mirrors `serialize`, which has taken one
1143
+ * since the database types joined.
1144
+ */
1145
+ parse(doc: any, extra?: any): any;
1113
1146
  /** Server entity -> the TOML `config pull` would write. */
1114
1147
  serialize(record: any, maps: ConfigDiffMaps, extra?: any): string;
1115
1148
  /** Structural values to hash beside the definition's fields. */
1116
- extras?(entity: any, doc: any): Record<string, any>;
1149
+ extras?(entity: any, doc: any, extra?: any): Record<string, any>;
1150
+ /**
1151
+ * Rewrite the parsed entity the way the SERVER rewrites an accepted one
1152
+ * (#2880 DSO-003).
1153
+ *
1154
+ * Some handlers canonicalize on the way in — an integration's base URL gains
1155
+ * a trailing slash, its methods are upper-cased — so the state the server
1156
+ * returns is not the text the file holds. Comparing them raw makes an
1157
+ * authored-but-noncanonical value `local-edited` on every run and push
1158
+ * re-apply the same update forever; restamping the baseline cannot fix it,
1159
+ * because the local side never moves. Applied to both sides (the remote
1160
+ * reaches it through the same projection), so it must be idempotent.
1161
+ */
1162
+ canonicalize?(entity: any): void;
1163
+ /**
1164
+ * The projected keys THIS FILE does not manage (#2880 criterion 3).
1165
+ *
1166
+ * A handful of fields are present-only by design: push sends them when the
1167
+ * file spells them and leaves the server's value alone when it does not,
1168
+ * because "absent" here means "not authored here" rather than "cleared" — a
1169
+ * webhook with no `[verification]` section at all, whose signing material a
1170
+ * push must not revoke as a side effect. Comparing such a field against a
1171
+ * server that holds one reports a difference push will never act on: the
1172
+ * update omits the key, the server keeps its value, and the row comes back
1173
+ * Modified on every run. So the local side inherits the remote value for
1174
+ * exactly the keys the file leaves unmanaged, and the two commands agree
1175
+ * that there is nothing to do.
1176
+ */
1177
+ unmanaged?(doc: any): string[];
1117
1178
  /** Whether the entity carries a rule-set reference needing resolution. */
1118
1179
  ruleSetRef?: boolean;
1119
1180
  /** How the type names itself in the rule-set resolution message. */
@@ -1121,9 +1182,10 @@ export interface ConfigDiffSpec {
1121
1182
  }
1122
1183
  /**
1123
1184
  * Every type the shared comparator serves — the types `config diff` compares
1124
- * field-for-field, and that `config push` gates on the same projection wherever
1125
- * it has adopted the shared gate (#2731 B1/B2; webhooks joined the comparator
1126
- * in #2757 while push keeps its own byte-hash gate).
1185
+ * field-for-field, and that `config push` gates on the same projection
1186
+ * (#2731 B1/B2; webhooks joined the comparator in #2757 and their push joined
1187
+ * the same gate in #2880, so no registered type answers "changed?" from the
1188
+ * file's bytes any more).
1127
1189
  *
1128
1190
  * Published so the round-trip acceptance bar (#2731 B9) can require a fixture
1129
1191
  * per type instead of listing them a second time by hand: converting a type is
@@ -1138,11 +1200,41 @@ export declare function configDiffSpec(label: string): ConfigDiffSpec;
1138
1200
  * a push that needs to SAY what differs and a diff that only needs to know THAT
1139
1201
  * something differs read the same projection.
1140
1202
  */
1141
- export declare function projectLocalConfig(spec: ConfigDiffSpec, parsedToml: any, maps: ConfigDiffMaps): Record<string, any>;
1203
+ export declare function projectLocalConfig(spec: ConfigDiffSpec, parsedToml: any, maps: ConfigDiffMaps, extra?: any): Record<string, any>;
1142
1204
  /** The same projection for a SERVER entity — see `hashRemoteConfigForDiff`. */
1143
1205
  export declare function projectRemoteConfig(spec: ConfigDiffSpec, record: any, maps: ConfigDiffMaps, extra?: any): Record<string, any>;
1206
+ /**
1207
+ * BOTH sides of one comparison, with the file's unmanaged keys reconciled
1208
+ * (#2880) — the one place `spec.unmanaged` is honored, so `config diff` and
1209
+ * `config push` cannot read the same file differently.
1210
+ */
1211
+ export declare function projectConfigPair(spec: ConfigDiffSpec, localParsed: any, remoteRecord: any, maps: ConfigDiffMaps, extra?: any, localExtra?: any): {
1212
+ local: Record<string, any>;
1213
+ remote: Record<string, any>;
1214
+ };
1215
+ /**
1216
+ * One `config diff` row's content verdict for a registered type (#2880).
1217
+ *
1218
+ * The three outcomes the per-type blocks were each spelling out by hand:
1219
+ * equal (`exists`), different (`modified`, framed as a preview of
1220
+ * `config pull`), and "could not tell" — a missing record or a comparison that
1221
+ * threw, which degrades THIS row and never the whole diff. Written once so a
1222
+ * type joining the comparator cannot accidentally report a fourth thing.
1223
+ *
1224
+ * The two `extra` arguments are the same split `decidePushForConfig` makes:
1225
+ * `extra` carries what only the SERVER side has (a database type's operation
1226
+ * rows), while `localExtra` is context BOTH sides read a value through — a test
1227
+ * case's id→name lookups. Giving the local side nothing was a silent
1228
+ * mistranslation: a sidecar pinned by a resolvable `configId` projected the id
1229
+ * while the server's projection resolved it to the name, so an untouched file
1230
+ * reported Modified in diff and drifted in push.
1231
+ */
1232
+ export declare function compareLocalToRemote(spec: ConfigDiffSpec, localParsed: any, remoteRecord: any, maps?: ConfigDiffMaps, extra?: any, localExtra?: any): {
1233
+ status: string;
1234
+ hint?: string;
1235
+ };
1144
1236
  /** Hash a LOCAL config file's definition-projected field set (#2644). */
1145
- export declare function hashLocalConfigForDiff(spec: ConfigDiffSpec, parsedToml: any, maps: ConfigDiffMaps): string;
1237
+ export declare function hashLocalConfigForDiff(spec: ConfigDiffSpec, parsedToml: any, maps: ConfigDiffMaps, extra?: any): string;
1146
1238
  /**
1147
1239
  * Hash a SERVER entity the same way, by serializing it into the file `sync
1148
1240
  * pull` would write and hashing that — so the two sides are normalized
@@ -1162,10 +1254,13 @@ export interface PushFieldDiff {
1162
1254
  /**
1163
1255
  * What `config push` should do with one converted-type file, and why.
1164
1256
  *
1165
- * `create`/`skip`/`update` are the applying outcomes; `drift`, `conflict` and
1166
- * `immutable` are the three ways push declines to apply and says so.
1257
+ * `create`/`skip`/`update` are the applying outcomes; `drift`, `conflict`,
1258
+ * `immutable` and `live-unavailable` are the four ways push declines to apply
1259
+ * and says so.
1167
1260
  */
1168
1261
  export type PushGateOutcome = {
1262
+ action: "live-unavailable";
1263
+ } | {
1169
1264
  action: "create";
1170
1265
  } | {
1171
1266
  action: "skip";
@@ -1173,7 +1268,7 @@ export type PushGateOutcome = {
1173
1268
  remoteHash: string;
1174
1269
  } | {
1175
1270
  action: "update";
1176
- direction: "local-edited" | "forced";
1271
+ direction: "local-edited" | "forced" | "adopt-untracked";
1177
1272
  localHash: string;
1178
1273
  remoteHash?: string;
1179
1274
  expectedModifiedAt?: string;
@@ -1219,7 +1314,47 @@ export interface PushBaselineEntry {
1219
1314
  *
1220
1315
  * Declining is a first-class outcome here. `drift`, `conflict` and `immutable`
1221
1316
  * each carry the field diff their report prints, and nothing is applied.
1222
- */
1317
+ *
1318
+ * `adoptsUntrackedByKey` is the one exception a type can ask for, and it is
1319
+ * documented on the field: an object the manifest has never recorded is the
1320
+ * adopt its create path would have performed via a 409 (#1006/#2909), not a
1321
+ * difference to refuse.
1322
+ */
1323
+ /**
1324
+ * What the DEGRADED gate does with one file — the path a converted type takes
1325
+ * when its live read failed (#2880 criterion 7).
1326
+ *
1327
+ * Falling back to the manifest's byte hash is right while there is one to fall
1328
+ * back to. Without one, `shouldPushFile(file, undefined)` answers "push it",
1329
+ * so a fresh checkout or a lost manifest turned "we could not read the server"
1330
+ * into "overwrite the server", unconditionally and silently — the one shape of
1331
+ * blind write this issue's direction attribution exists to prevent. So a
1332
+ * baseline-less update DECLINES: the resource is not written and the operator
1333
+ * is told which of `config pull` / `--force` clears it.
1334
+ *
1335
+ * A file the manifest does not name is declined for the same reason, and this
1336
+ * is the part that is easy to get wrong: an unrecorded file looks like a
1337
+ * create, but only LIVE STATE can say the server has nothing under that key —
1338
+ * and live state is exactly what this path could not read. The create that
1339
+ * follows is adopted by key on the 409 and re-issued as an update, so
1340
+ * "there is no recorded entity to overwrite" would have been a blind overwrite
1341
+ * of a resource this run never compared. Every section's degraded gate is
1342
+ * reached only when the live read failed, so there is no case here where
1343
+ * absence was proved.
1344
+ */
1345
+ export type DegradedPushDecision = {
1346
+ action: "skip";
1347
+ } | {
1348
+ action: "send";
1349
+ } | {
1350
+ action: "decline";
1351
+ reason: "no-baseline";
1352
+ };
1353
+ export declare function decideDegradedPush(input: {
1354
+ force?: boolean;
1355
+ storedContentHash?: string;
1356
+ currentFileHash?: string;
1357
+ }): DegradedPushDecision;
1223
1358
  export declare function decidePushForConfig(input: {
1224
1359
  spec: ConfigDiffSpec;
1225
1360
  maps: ConfigDiffMaps;
@@ -1234,7 +1369,30 @@ export declare function decidePushForConfig(input: {
1234
1369
  entry?: PushBaselineEntry | null;
1235
1370
  /** Byte hash of the local file — the legacy direction signal. */
1236
1371
  localFileHash?: string;
1372
+ /**
1373
+ * Per-type context for the LOCAL projection (#2880) — a test case's id→name
1374
+ * lookups. Separate from `live.extra`, which carries remote-only rows (a
1375
+ * database type's operations) the local parse must never see.
1376
+ */
1377
+ localExtra?: any;
1237
1378
  force?: boolean;
1379
+ /**
1380
+ * Whether this type's push path ADOPTS an existing object by key (#2909).
1381
+ *
1382
+ * Opt-in, and it changes exactly one outcome: a live entity the manifest has
1383
+ * no row for at all. Prompts (#1006), and the other types whose create path
1384
+ * recovers a 409 through `adoptByKeyOnCreate409`, treat that as the adopt it
1385
+ * has always been — an object pushed from another slot, created out of band,
1386
+ * or orphaned by a push that aborted before recording it, which the local
1387
+ * file is the declared intent for. Without this the gate reads that same
1388
+ * shape as a difference it cannot attribute, refuses, and the create the
1389
+ * adopt guard recovers from is never even sent (the #2909 regression).
1390
+ *
1391
+ * The types #2731 converted leave it off: for them a never-synced file
1392
+ * differing from a same-named live entity IS the conflict they report, and
1393
+ * that contract is unchanged.
1394
+ */
1395
+ adoptsUntrackedByKey?: boolean;
1238
1396
  }): PushGateOutcome;
1239
1397
  /**
1240
1398
  * The identity a file may leave to its name, written back into the parsed
@@ -1818,6 +1976,23 @@ export declare function pushTestCasesForBlock(params: {
1818
1976
  }>;
1819
1977
  /** Where a rejected create/update/upload goes — it fails the push (#2731 B2). */
1820
1978
  failures: ApplyFailure[];
1979
+ /**
1980
+ * What push DECLINED to apply (#2880): server drift, a conflict, a live read
1981
+ * that failed with no baseline. Reported exactly as every other converted
1982
+ * type reports it — the caller passes its `recordDeclined`.
1983
+ */
1984
+ declined?: (type: string, key: string, outcome: Extract<PushGateOutcome, {
1985
+ action: "drift" | "conflict" | "immutable" | "live-unavailable";
1986
+ }>, storedModifiedAt?: string) => void;
1987
+ /** The id→name lookups the comparison reads a reference through (#2880). */
1988
+ lookupMaps?: TestCaseLookupMaps;
1989
+ /**
1990
+ * Re-derive `lookupMaps` from the caller's name→id maps (#2880). Called after
1991
+ * this case's references are resolved and before it is compared: resolution
1992
+ * is what LOADS a block's configs, so the comparison would otherwise read a
1993
+ * reference the same push just learned how to read.
1994
+ */
1995
+ refreshLookupMaps?: () => void;
1821
1996
  resolutionMaps?: PushResolutionMaps;
1822
1997
  options?: {
1823
1998
  force?: boolean;
@@ -1826,25 +2001,53 @@ export declare function pushTestCasesForBlock(params: {
1826
2001
  skipped: number;
1827
2002
  }>;
1828
2003
  /**
1829
- * Classify one block's sidecar against the server's test-case names (#2769).
2004
+ * Which live test case each sidecar manages (#2880 behavior 16).
1830
2005
  *
1831
- * Remote slugs are derived exactly the way pull writes them — same slugify,
1832
- * same collision suffixes, same order — because a `<slug>-2.toml` on disk IS
1833
- * the second colliding name and must not read as an unpushed local file.
2006
+ * The manifest id comes FIRST. A test case's slug is derived from its name, so
2007
+ * matching on the slug alone made a rename — in either direction — read as a
2008
+ * new local file plus a removable remote case: diff showed two rows for one
2009
+ * case, and push created a duplicate. `entities.testCases[stateKey].id` is
2010
+ * stable across every rename, so it is the identity, and the slug is only how
2011
+ * a sidecar the manifest has never seen is ADOPTED.
2012
+ *
2013
+ * Adoption derives its slugs exactly the way pull writes them — same slugify,
2014
+ * same collision suffixes, same listing order — so a `<slug>-2.toml` on disk IS
2015
+ * the second colliding name and is paired with it.
2016
+ *
2017
+ * One function for both commands, because a diff row and a push decision that
2018
+ * pair differently are two answers to the same question.
1834
2019
  */
1835
- export declare function classifyTestCaseSidecarDiff(params: {
1836
- remoteNames: string[];
2020
+ export declare function pairTestCases(input: {
2021
+ blockType: string;
2022
+ blockKey: string;
1837
2023
  localSlugs: string[];
1838
- }): Array<{
1839
- slug: string;
1840
- status: string;
1841
- }>;
2024
+ liveCases: any[];
2025
+ managed: Record<string, {
2026
+ id?: string;
2027
+ }> | undefined;
2028
+ }): {
2029
+ bySlug: Map<string, any>;
2030
+ localOnly: string[];
2031
+ remoteOnly: any[];
2032
+ };
1842
2033
  /**
1843
2034
  * `config diff` for one block's test-case sidecar (#2769). A failed listing is
1844
2035
  * an OUTCOME the caller reports as "not compared" — the old helper returned
1845
2036
  * silently, so a block whose tests could not be fetched simply disappeared from
1846
2037
  * a report that still read as exhaustive.
1847
2038
  */
2039
+ export interface TestCaseDiffRow {
2040
+ blockType: string;
2041
+ blockKey: string;
2042
+ slug: string;
2043
+ status: string;
2044
+ /** What a validation error or a degraded comparison has to say. */
2045
+ hint?: string;
2046
+ /** Attachments push would upload from this sidecar (#2880 behavior 19). */
2047
+ attachmentUploads?: string[];
2048
+ /** Managed attachments `push --prune` would delete (never a plain push). */
2049
+ attachmentDeletions?: string[];
2050
+ }
1848
2051
  export declare function compareTestCasesForBlock(params: {
1849
2052
  client: ApiClient;
1850
2053
  appId: string;
@@ -1852,14 +2055,12 @@ export declare function compareTestCasesForBlock(params: {
1852
2055
  blockId: string;
1853
2056
  blockKey: string;
1854
2057
  configDir: string;
2058
+ /** The manifest — identity, and the attachment hashes the plan reads. */
2059
+ syncState?: SyncState | null;
2060
+ lookupMaps?: TestCaseLookupMaps;
1855
2061
  }): Promise<{
1856
2062
  ok: true;
1857
- rows: Array<{
1858
- blockType: string;
1859
- blockKey: string;
1860
- slug: string;
1861
- status: string;
1862
- }>;
2063
+ rows: TestCaseDiffRow[];
1863
2064
  } | {
1864
2065
  ok: false;
1865
2066
  }>;
@@ -1871,6 +2072,17 @@ export declare function compareTestCasesForBlock(params: {
1871
2072
  * on the way to it, pushing a test case that tested something else.
1872
2073
  */
1873
2074
  export declare function testCaseSidecarErrors(filePath: string, tomlData: any): string[];
2075
+ /**
2076
+ * EVERY reason `config push` refuses one test-case sidecar (#2880 criterion 1).
2077
+ *
2078
+ * The four checks the push preflight ran inline: the document's shape, the keys
2079
+ * the definition recognizes, the declared type of each value, and the sidecar's
2080
+ * own authoring rules (#2769). Collected in one place because `config diff`
2081
+ * runs the identical list — it used to run only the last of the four, so a
2082
+ * sidecar with an unknown key or a mistyped ordinary field was reported Synced
2083
+ * by the command whose whole promise is that a Synced file pushes.
2084
+ */
2085
+ export declare function testCaseSidecarPreflightErrors(filePath: string, tomlData: any): string[];
1874
2086
  /** A managed test case whose authored file is gone (#2769). */
1875
2087
  export interface TestCaseDeletionCandidate {
1876
2088
  stateKey: string;