@tenonhq/dovetail-servicenow 0.0.49 → 0.0.51

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 CHANGED
@@ -736,7 +736,7 @@ admin, and `sys_index_column` does not exist at all (HTTP 400 `Invalid table`),
736
736
  is no two-table index model to join and nothing to cross-check against.
737
737
 
738
738
  Each row comes back as `{ name, columns, type, rawColumns }`. `columns` is the view's
739
- bracketed `column_names` cell (`"[phone]"`, `"[a,b]"`) **parsed** into a list - never
739
+ bracketed `column_names` cell (`"[phone]"`, `"[a;b]"` - semicolon-separated when composite) **parsed** into a list - never
740
740
  substring-matched, because `"[owner_id]"` contains `"owner"`. `type` is `access_method`.
741
741
 
742
742
  **Uniqueness is not readable.** `v_db_index` has no uniqueness field, so a unique index
@@ -747,49 +747,73 @@ is wrong than that the table is unindexed - every physical table has a `PRIMARY`
747
747
 
748
748
  ### Create an index (composite and non-unique included)
749
749
 
750
- > **A DATABASE INDEX IS A PHYSICAL, PER-INSTANCE CHANGE. IT IS NOT CAPTURED IN AN UPDATE
751
- > SET AND DOES NOT TRAVEL WITH A PROMOTION.** Re-run `index-create` against every
752
- > environment that needs the index (dev, test, uat, staging, prod). There is deliberately
753
- > no `--update-set` - passing one is an error, not a silent no-op.
750
+ > **An index IS captured in an update set.** The platform's build job writes a
751
+ > `sys_update_xml` row (`type=Indexes`, name `sys_index_<table>_<col>_<col>…`) into the
752
+ > session user's **current** update set - the Database Indexes dialog says so itself, and
753
+ > it was read back live on tenonworkstudio 2026-10-08. `--update-set` is therefore
754
+ > **required** on the live path: the verb pins that set as current first and reads the
755
+ > capture row back from it afterwards. The physical index is still built per instance;
756
+ > committing the set elsewhere rebuilds it there.
754
757
 
755
758
  ```bash
756
- # Dry-run (the DEFAULT) - sends nothing and reads nothing
759
+ # Dry-run (the DEFAULT) - sends nothing and reads nothing; no update set needed
757
760
  npx dove-sn index-create --table x_cadso_journey_instance --columns state,created_on
758
761
 
759
762
  # Send it
760
763
  npx dove-sn index-create \
761
- --table x_cadso_journey_instance --columns state,created_on --confirm --json
764
+ --table x_cadso_journey_instance --columns state,created_on \
765
+ --update-set <sys_id> --confirm --json
762
766
  ```
763
767
 
764
768
  This is what `add-index` cannot do. `sys_dictionary.unique` - the only record-shaped lever
765
769
  - is **per-column and unique-only**, so composite and plain indexes have no record path at
766
- all. `index-create` instead replays the platform's own index-creator form
767
- (`sys_action=create_index`, `sysparm_index_table`, `sysparm_fields`,
768
- `sysparm_unique_index_SKIP`) over a form-login session. That contract is lifted from the
769
- instance's shipped `index_creator_information` UI macro, not from a guess, and the POST
770
- target is taken from the rendered page's own `<form action>`.
770
+ all (`sys_index` is API-level-ACL 403, `sys_index_column` does not exist). `index-create`
771
+ instead replays the two GlideAjax calls the platform's own Database Indexes dialog makes
772
+ on `xmlhttp.do` over a form-login session - taken from a HAR of the Studio dialog plus the
773
+ `dialog_index_create` UI page's client script, both read live:
774
+
775
+ 1. `IndexCreatorErrorChecker` / `canCreate` - the dialog's pre-flight (`sysparm_table_name`,
776
+ `sysparm_field_names`, `sysparm_unique`, `sysparm_access_method`). A `canCreate:false`
777
+ verdict is returned verbatim with its `errorCode` and nothing is scheduled.
778
+ 2. `ScheduleCreator` / `createSchedule` - schedules the build (`sysparm_table`,
779
+ `sysparm_fields`, `sysparm_access_method` - default `btree`, exactly as the dialog's
780
+ client JS does - `sysparm_unique` `true|false`, empty `sysparm_email` = no
781
+ notification, empty `sysparm_schedule_name`).
782
+
783
+ (The `index_creator_dialog` page's `sys_action=create_index` form is inert - the browser
784
+ never submits it, and the UI page has no processing script. The earlier form-POST replay
785
+ was a silent no-op for that reason.)
771
786
 
772
787
  - **Dry-run by default.** Without `--confirm` nothing is sent *and nothing is read*;
773
788
  `--dry-run` forces a plan even with `--confirm`.
789
+ - **Target checks before anything else.** The table must exist; the update set must exist,
790
+ be `in progress`, and belong to the table's application scope - a set in another scope
791
+ is refused, because the capture row would land in the wrong scope.
774
792
  - **Idempotent.** On the live path `v_db_index` is read first, and an index over *exactly*
775
- these columns short-circuits to `already-exists` with no form session and no write.
776
- Column **order** is part of an index's identity - `[a,b]` is not `[b,a]`.
777
- - **`--name` is refused.** The platform's form has no name input; ServiceNow names the
778
- index itself. Reporting a name the instance does not carry would be a lie, so the
779
- created index's *real* name is returned in `name` instead.
780
- - **The read-back is the proof.** After the POST the index is polled for in `v_db_index`
781
- (default 10 checks, 3 s apart - a build on a populated table is asynchronous). If it
782
- never appears the status is `failed`: a form processor returning a page is not evidence
783
- an ALTER ran, and a unique index cannot build over duplicate values (EMPTY counts).
793
+ these columns short-circuits to `already-exists` with no pin, no form session and no
794
+ write. Column **order** is part of an index's identity - `[a;b]` is not `[b;a]`.
795
+ - **The pin is read back.** The set is pinned with Dovetail's own `changeUpdateSet` and
796
+ `currentUpdateSet` is read; a pin that did not take stops the run before the session
797
+ opens.
798
+ - **`--name` is refused.** The dialog has no name input; ServiceNow names the index after
799
+ its leading column (live: `[sys_created_on;status;version_step;version]` → index
800
+ `sys_created_on`). The created index's *real* name is returned in `name`.
801
+ - **The read-back is the proof.** After scheduling, the index is polled for in `v_db_index`
802
+ (default 10 checks, 3 s apart). If it never appears the status is `failed`: an accepted
803
+ schedule is not evidence an ALTER ran, and a unique index cannot build over duplicate
804
+ values (EMPTY counts). Then the capture row is looked for in the pinned set:
805
+ `captured:true` only when it was read back; an index that exists but was not captured
806
+ is `created:true, captured:false` with `update-set-capture` in `unverified` - and
807
+ exit code 2, because it will not travel.
784
808
  - **Uniqueness is still never claimed.** `uniqueness-enforced` stays in `unverified` on
785
809
  every status.
786
810
 
787
- **Requires a username+password identity that can form-log-in.** An instance on
788
- API-key-only auth, SSO or MFA rejects the form login however valid the API key is; the
789
- verb fails at the session with that diagnosis rather than a mystery 302, and no `.do`
790
- replay (including `create-table`'s) can work in that state.
811
+ **Requires a username+password identity that can form-log-in.** `xmlhttp.do` ignores
812
+ Basic auth and API keys, so an API-key-only, SSO or MFA identity fails at the session with
813
+ that diagnosis (TenonHQ/Dovetail#292).
791
814
 
792
- Exit codes: `0` created / already-exists / dry-run, `1` bad args, `2` failed.
815
+ Exit codes: `0` created / already-exists / dry-run, `1` bad args, `2` failed - and `2`
816
+ when the index was created but its capture row was not found in the pinned set.
793
817
 
794
818
  ### Set a field on a record
795
819
 
@@ -1154,15 +1178,22 @@ Claude Code and agents: `create_view`, `set_list_layout`, `set_form_layout`,
1154
1178
  `add_column` (same-scope by default; `crossScope: true` with `scope` adds a column OWNED
1155
1179
  by another app — a Journey field on an Automate table, element `<scope>_<name>` —
1156
1180
  guarded by `sys_db_object.alter_access` and the update set's scope, with the stored
1157
- element and `sys_scope` read back; the CLI flag is `--cross-scope`) /
1181
+ element and `sys_scope` read back; the CLI flag is `--cross-scope`. A cross-scope
1182
+ result also carries `designAccess`: the platform UI refuses cross-scope authoring
1183
+ without a `sys_scope_design_access` record (column scope -> table scope), so a missing
1184
+ one is FLAGGED — not blocking, the headless insert works without it — and
1185
+ `ensureDesignAccess: true` / `--ensure-design-access` creates it first in the same
1186
+ update set) / `design_access` (ensure that record on its own — idempotent, created in
1187
+ the SOURCE app's update set and read back; CLI `dove-sn design-access --source <app>
1188
+ --target <app> --update-set <sys_id>`, `--dry-run` only reports exists/missing) /
1158
1189
  `add_index` (a single-column unique index via `sys_dictionary.unique`,
1159
1190
  read back from the `v_db_index` view - uniqueness enforcement is always reported
1160
1191
  unverified) / `index_list` (read-only: a table's database indexes from `v_db_index`,
1161
1192
  the only index read surface - `sys_index` is API-level-ACL 403 and `sys_index_column`
1162
1193
  does not exist) / `index_create` (create an index, composite and non-unique included, by
1163
- replaying the platform index-creator form; dry-run by default, idempotent, read back from
1164
- `v_db_index` - and **not** captured in an update set, because a database index is a
1165
- physical per-instance change), the record-write verbs `set_field` (update scalar fields on an
1194
+ replaying the Database Indexes dialog's own processor calls; dry-run by default, idempotent,
1195
+ pinned to a required update set, read back from `v_db_index` and the capture row read back
1196
+ from `sys_update_xml`), the record-write verbs `set_field` (update scalar fields on an
1166
1197
  existing record), `create_record` (insert one record) and `delete_record` (delete one
1167
1198
  record — dry-run by default, `confirm:true` to apply, `updateSetSysId` required, the
1168
1199
  record read back before AND after so success is only reported once it is confirmed
package/dist/cli.js CHANGED
@@ -1340,15 +1340,60 @@ async function runCreateTable(flags) {
1340
1340
  return 2;
1341
1341
  return 0;
1342
1342
  }
1343
+ /**
1344
+ * dove-sn design-access:
1345
+ * --source x_cadso_journey --target x_cadso_automate
1346
+ * [--update-set <sys_id in source>] [--dry-run] [--json]
1347
+ * Ensures the sys_scope_design_access record that lets the SOURCE app author in the
1348
+ * TARGET app's tables (required by the platform UI for cross-scope columns). Idempotent;
1349
+ * --dry-run only reports whether it exists. --update-set is required unless --dry-run.
1350
+ */
1351
+ async function runDesignAccess(flags) {
1352
+ if (!flags.source || !flags.target) {
1353
+ process.stderr.write("design-access: --source (authoring app) and --target (app that owns the tables) are required\n");
1354
+ return 1;
1355
+ }
1356
+ var isDryRun = flags["dry-run"] === "true";
1357
+ if (!isDryRun && !flags["update-set"]) {
1358
+ process.stderr.write("design-access: --update-set (an update set in --source) is required on the live path (only --dry-run works without one)\n");
1359
+ return 1;
1360
+ }
1361
+ var result = await (0, table_1.ensureDesignAccess)({
1362
+ client: (0, client_1.createClient)({}),
1363
+ sourceScope: flags.source,
1364
+ targetScope: flags.target,
1365
+ updateSetSysId: flags["update-set"],
1366
+ dryRun: isDryRun,
1367
+ });
1368
+ if (flags.json === "true") {
1369
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
1370
+ }
1371
+ else {
1372
+ process.stdout.write("[" +
1373
+ result.status +
1374
+ "] " +
1375
+ result.sourceScope +
1376
+ " -> " +
1377
+ result.targetScope +
1378
+ (result.sysId ? " (" + result.sysId + ")" : "") +
1379
+ "\n" +
1380
+ result.note +
1381
+ "\n");
1382
+ }
1383
+ if (result.status === "failed")
1384
+ return 2;
1385
+ return 0;
1386
+ }
1343
1387
  /**
1344
1388
  * dove-sn add-column:
1345
1389
  * --table x_cadso_journey --label URL --type url
1346
1390
  * [--name url] [--max-length 1024] [--reference <table>]
1347
1391
  * [--mandatory] [--default <value>] [--dependent-on-field <element>]
1348
- * [--scope x_cadso_journey] [--cross-scope] [--update-set <sys_id>]
1349
- * [--from-json <spec.json>] [--dry-run] [--debug] [--json]
1392
+ * [--scope x_cadso_journey] [--cross-scope] [--ensure-design-access]
1393
+ * [--update-set <sys_id>] [--from-json <spec.json>] [--dry-run] [--debug] [--json]
1350
1394
  * --update-set is required unless --dry-run. --cross-scope opts in to a column
1351
- * OWNED by --scope when that differs from the table's scope.
1395
+ * OWNED by --scope when that differs from the table's scope; a missing Design Access
1396
+ * record is flagged, and --ensure-design-access creates it first.
1352
1397
  */
1353
1398
  async function runAddColumn(flags) {
1354
1399
  var spec = {};
@@ -1388,6 +1433,10 @@ async function runAddColumn(flags) {
1388
1433
  if (flags["cross-scope"] === "true" || spec.crossScope === true) {
1389
1434
  params.crossScope = true;
1390
1435
  }
1436
+ if (flags["ensure-design-access"] === "true" ||
1437
+ spec.ensureDesignAccess === true) {
1438
+ params.ensureDesignAccess = true;
1439
+ }
1391
1440
  var us = flags["update-set"] || spec.updateSetSysId;
1392
1441
  if (us)
1393
1442
  params.updateSetSysId = us;
@@ -1539,16 +1588,19 @@ async function runIndexList(flags) {
1539
1588
  }
1540
1589
  /**
1541
1590
  * dove-sn index-create:
1542
- * --table x_cadso_core_u_smoke --columns a,b [--unique] [--access-method <m>]
1543
- * [--confirm] [--dry-run] [--poll-attempts <n>] [--poll-interval-ms <n>]
1544
- * [--debug] [--json]
1591
+ * --table x_cadso_core_u_smoke --columns a,b --update-set <sys_id> [--unique]
1592
+ * [--access-method <m>] [--confirm] [--dry-run] [--poll-attempts <n>]
1593
+ * [--poll-interval-ms <n>] [--debug] [--json]
1545
1594
  *
1546
1595
  * DRY-RUN BY DEFAULT — nothing is sent (and nothing is even READ) without --confirm;
1547
- * --dry-run forces a plan even with it. There is deliberately NO --update-set: a
1548
- * database index is physical and is not captured in one.
1596
+ * --dry-run forces a plan even with it. --update-set is REQUIRED on the live path:
1597
+ * the build job captures the index definition (sys_update_xml type=Indexes) into the
1598
+ * user's CURRENT update set, so the verb pins that set first and reads the capture
1599
+ * row back from it afterwards.
1549
1600
  *
1550
1601
  * Exit codes: 0 created / already-exists / dry-run, 1 bad args, 2 failed (which
1551
- * includes "the form was posted but no index was read back").
1602
+ * includes "scheduled but no index was read back") — and 2 when the index was read
1603
+ * back but its capture row was NOT found in the pinned set.
1552
1604
  */
1553
1605
  async function runIndexCreate(flags) {
1554
1606
  var columns = splitList(flags.columns || "");
@@ -1556,10 +1608,12 @@ async function runIndexCreate(flags) {
1556
1608
  process.stderr.write("index-create: --table <name> and --columns <a[,b,...]> are required\n");
1557
1609
  return 1;
1558
1610
  }
1559
- if (flags["update-set"]) {
1560
- process.stderr.write("index-create: --update-set is not accepted. A database index is a PHYSICAL, " +
1561
- "PER-INSTANCE change — it is NOT captured in an update set and does not " +
1562
- "travel with a promotion. Run index-create against each environment.\n");
1611
+ // DRY-RUN BY DEFAULT: --confirm is what sends; --dry-run forces a plan even with it.
1612
+ var indexDryRun = flags["dry-run"] === "true" || flags.confirm !== "true";
1613
+ if (!indexDryRun && !flags["update-set"]) {
1614
+ process.stderr.write("index-create: --update-set is required on the live path — the index " +
1615
+ "definition is captured into the user's CURRENT update set, so it must be " +
1616
+ "pinned first (only a dry-run works without one)\n");
1563
1617
  return 1;
1564
1618
  }
1565
1619
  var params = {
@@ -1574,8 +1628,8 @@ async function runIndexCreate(flags) {
1574
1628
  params.name = flags.name;
1575
1629
  if (flags["access-method"])
1576
1630
  params.accessMethod = flags["access-method"];
1577
- if (flags["form-path"])
1578
- params.formPath = flags["form-path"];
1631
+ if (flags["update-set"])
1632
+ params.updateSetSysId = flags["update-set"];
1579
1633
  if (flags.debug === "true")
1580
1634
  params.debug = true;
1581
1635
  // A non-integer poll setting would make the bounded wait unbounded (or zero).
@@ -1614,6 +1668,12 @@ async function runIndexCreate(flags) {
1614
1668
  (result.name ? " -> " + result.name : "") +
1615
1669
  (result.instance ? " on " + result.instance : "") +
1616
1670
  (result.verified ? " — verified" : "") +
1671
+ (result.updateSet
1672
+ ? " [update set " +
1673
+ (result.updateSet.name || result.updateSet.sysId) +
1674
+ (result.captured ? " — captured" : " — capture NOT verified") +
1675
+ "]"
1676
+ : "") +
1617
1677
  "\n" +
1618
1678
  result.note +
1619
1679
  "\nUNVERIFIED: " +
@@ -1622,6 +1682,10 @@ async function runIndexCreate(flags) {
1622
1682
  }
1623
1683
  if (result.status === "failed")
1624
1684
  return 2;
1685
+ // An index that exists but whose definition was not captured will not travel —
1686
+ // surface that as a non-zero exit, the same way an unread-back index is.
1687
+ if (result.status === "created" && !result.captured)
1688
+ return 2;
1625
1689
  return 0;
1626
1690
  }
1627
1691
  /** Parse a CLI boolean flag. Bare `--mandatory` means true; `--mandatory false` means
@@ -2616,6 +2680,9 @@ async function dispatch(parsed) {
2616
2680
  if (parsed.command === "add-column") {
2617
2681
  return await runAddColumn(parsed.flags);
2618
2682
  }
2683
+ if (parsed.command === "design-access") {
2684
+ return await runDesignAccess(parsed.flags);
2685
+ }
2619
2686
  if (parsed.command === "add-index") {
2620
2687
  return await runAddIndex(parsed.flags);
2621
2688
  }
package/dist/cliUsage.js CHANGED
@@ -209,6 +209,10 @@ exports.VERB_USAGE = {
209
209
  flag: "cross-scope",
210
210
  note: "Opt in to a column OWNED by --scope, which differs from the table's scope (element becomes <scope>_<name>; the table must allow new fields; --update-set must be in --scope).",
211
211
  },
212
+ {
213
+ flag: "ensure-design-access",
214
+ note: "Cross-scope only: create the missing sys_scope_design_access record (--scope -> table's scope) first, in the same update set. Without it a missing record is only flagged.",
215
+ },
212
216
  { flag: "update-set", value: "<sys_id>", note: "REQUIRED on the live path (only --dry-run works without one)." },
213
217
  DRY_RUN_FLAG,
214
218
  DEBUG_FLAG,
@@ -222,6 +226,26 @@ exports.VERB_USAGE = {
222
226
  "Cross-scope: dove-sn add-column --table x_cadso_automate_email_batch --label 'Instance Step' --name instance_step --type reference --reference x_cadso_journey_instance_step --scope x_cadso_journey --cross-scope --update-set <journey set>",
223
227
  ],
224
228
  },
229
+ "design-access": {
230
+ summary: "Ensure the sys_scope_design_access record that lets one app author in another app's tables (required by the UI for cross-scope columns)",
231
+ required: [
232
+ { flag: "source", value: "<x_scope>", note: "The AUTHORING app (owns the record and its update set)." },
233
+ { flag: "target", value: "<x_scope>", note: "The app that OWNS the tables." },
234
+ ],
235
+ optional: [
236
+ { flag: "update-set", value: "<sys_id>", note: "Update set in --source. REQUIRED on the live path." },
237
+ DRY_RUN_FLAG,
238
+ JSON_FLAG,
239
+ ],
240
+ gate: "dry-run-flag",
241
+ gateNote: "--update-set is required unless --dry-run; --dry-run reports exists/missing only.",
242
+ example: "dove-sn design-access --source x_cadso_journey --target x_cadso_automate --update-set <journey set>",
243
+ notes: [
244
+ "Idempotent: an existing record is reported and nothing is written.",
245
+ "Symptom it fixes: \"Invalid 'Table' selected on the Dictionary Entry record ... can only select '<app>' tables with read access enabled\" — the table's own access flags are NOT the gate.",
246
+ "Exit 2 when the record could not be created or verified on read-back.",
247
+ ],
248
+ },
225
249
  "set-column": {
226
250
  summary: "Update an EXISTING column's schema (label/mandatory/default/read-only/max-length/dependent-on-field), then verify",
227
251
  required: [
@@ -308,14 +332,15 @@ exports.VERB_USAGE = {
308
332
  ],
309
333
  },
310
334
  "index-create": {
311
- summary: "Create a DATABASE index (composite and non-unique included) by replaying the platform index-creator form, then read it back",
335
+ summary: "Create a DATABASE index (composite and non-unique included) by replaying the platform Database Indexes dialog's processor calls, pinned to an update set, then read it back",
312
336
  required: [
313
337
  { flag: "table", value: "<name>" },
314
338
  { flag: "columns", value: "<a[,b,...]>", note: "Comma-separated column elements, in index order." },
339
+ { flag: "update-set", value: "<sys_id>", note: "Required on the live path; the index definition is captured into it. Must be in the table's application scope." },
315
340
  ],
316
341
  optional: [
317
342
  { flag: "unique" },
318
- { flag: "access-method", value: "<m>", note: "Platform access method (default: the form's default)." },
343
+ { flag: "access-method", value: "<m>", note: "Platform access method (default: btree, as the dialog does)." },
319
344
  CONFIRM_FLAG,
320
345
  FORCE_DRY_RUN_FLAG,
321
346
  { flag: "poll-attempts", value: "<n>", note: "Positive integer." },
@@ -325,11 +350,12 @@ exports.VERB_USAGE = {
325
350
  ],
326
351
  gate: "confirm",
327
352
  gateNote: "Nothing is sent OR read without --confirm.",
328
- example: "dove-sn index-create --table x_cadso_journey_instance --columns state,created_on --confirm",
353
+ example: "dove-sn index-create --table x_cadso_journey_instance --columns state,created_on --update-set <sys_id> --confirm",
329
354
  notes: [
330
- "A DATABASE INDEX IS PHYSICAL AND PER-INSTANCE: not captured in an update set, does not travel with a promotion — re-run it against every environment. --update-set is therefore REFUSED.",
331
- "Idempotent: an index over exactly those columns returns already-exists with no write. --name is refused (the form has no name input; the real name is returned).",
332
- "Needs a username+password identity that can form-log-in.",
355
+ "An index IS captured in an update set: the build job writes a sys_update_xml row (type=Indexes) into the user's CURRENT set, so --update-set is pinned first and the capture row is read back from it. The physical index is still built per instance; committing the set elsewhere rebuilds it.",
356
+ "Replays the dialog's own xmlhttp.do calls: IndexCreatorErrorChecker.canCreate (pre-flight) then ScheduleCreator.createSchedule. Idempotent: an index over exactly those columns returns already-exists with no write. --name is refused (the dialog has no name input; the real name is returned).",
357
+ "Needs a username+password identity that can form-log-in (xmlhttp.do ignores Basic auth / API keys).",
358
+ "Exit 2 when the index was scheduled but never read back from v_db_index, and when it was read back but its capture row was not found in the pinned set.",
333
359
  ],
334
360
  },
335
361
  "set-field": {
package/dist/index.d.ts CHANGED
@@ -21,8 +21,8 @@ export { sincPlugin } from "./plugin";
21
21
  export { listTemplates, verifyArtifact, cloneSubflow, cloneActionType, resolveScope, slugInternalName, fetchActionSteps, findStepInput, triggerPublication, publishActionType, editActionType, defineActionType, planActionDefinition, validateDefineSpec, applyStepOps, verifySteps, summarizeSteps, formatStepPill, readFlow, readActionType, publishFlow, copyFlow, createFlow, buildPublishModel, editFlow, testFlow, DEFAULT_RUN_FLOW_PATH, generateSysId, topoSort, executeWritePlan, WriteOrderError, } from "./flowDesigner";
22
22
  export type { TemplateRef, ListTemplatesParams, FlowKind, VerifyExpect, VerifyFound, VerifyFailure, VerifyReport, VerifyArtifactParams, CloneSubflowParams, CloneSubflowResult, CloneActionTypeParams, CloneActionTypeResult, CloneActionTypePlan, CloneActionTypeStepReport, TriggerPublicationParams, TriggerPublicationResult, PublishActionTypeParams, PublishActionTypeResult, EditActionTypeParams, EditActionTypeResult, EditActionTypeOps, DefineActionSpec, DefineActionInputSpec, DefineActionOutputSpec, DefineStepSpec, DefineStepVarSpec, DefineStepOutputSpec, DefineStepValue, DefineActionTypeParams, DefineActionTypeResult, DefineActionDiff, DefineActionPlan, PlanActionDefinitionParams, ActionView, StepView, StepOps, StepRecord, StepSummary, StepIoSummary, PatchStepScriptOp, SetStepInputOp, AddStepOutputOp, AddStepInputOp, ApplyStepOpsResult, VerifyStepsResult, ReadFlowParams, ReadFlowResult, FlowStep, FlowVariable, ReadActionTypeParams, ReadActionTypeResult, ActionIo, PublishFlowParams, PublishFlowResult, CopyFlowParams, CopyFlowResult, CreateFlowParams, CreateFlowResult, EditFlowParams, EditFlowResult, EditFlowOps, StepInputPatch, TestFlowParams, TestFlowResult, WriteOp, WriteOpResult, } from "./flowDesigner";
23
23
  export type { ServiceNowClientConfig, ChoiceValue, ChoiceType, AddChoicesParams, AddChoicesResult, ChoiceActionResult, ChoiceFieldRef, RemoveChoicesParams, RemoveChoicesResult, ChoiceRemovalResult, DictionaryRecord, UpdateSetRecord, LayoutAction, LayoutRecordResult, LayoutResult, CreateViewParams, CreateViewResult, FormSectionSpec, SetFormLayoutParams, SetListLayoutParams, SetRelatedListsParams, ChunkRole, ChunkInfo, ChunkResult, PrunedResult, HostAssetsParams, HostAssetsResult, } from "./types";
24
- export { createTable, projectTableGraph, buildColumnXml, normalizeColumns, resolveType, applyTableSaveOverlay, defaultAccessFlags, TYPE_MAP, DEFAULT_SUPER_CLASS, DEFAULT_SAVE_ACTION, addColumn, deriveElement, addIndex, parseIndexColumns, indexMatchesColumns, listIndexes, createIndex, validateCreateIndex, DEFAULT_INDEX_FORM_PATH, NOT_IN_UPDATE_SET, setColumn, resolveAttributes, toStoredValue, setTable, resolveTableAttributes, } from "./table";
25
- export type { CreateTableParams, CreateTableResult, AddColumnParams, AddColumnResult, AddIndexParams, AddIndexResult, AddIndexVerification, ListIndexesParams, ListIndexesResult, TableIndex, CreateIndexParams, CreateIndexResult, SetColumnParams, SetColumnResult, ColumnAttributes, AttributeChange, SetTableParams, SetTableResult, TableAttributes, TableGraph, NormalizedColumn, ColumnSpec, AccessFlags, OverlaySpec, } from "./table";
24
+ export { createTable, projectTableGraph, buildColumnXml, normalizeColumns, resolveType, applyTableSaveOverlay, defaultAccessFlags, TYPE_MAP, DEFAULT_SUPER_CLASS, DEFAULT_SAVE_ACTION, addColumn, deriveElement, ensureDesignAccess, findDesignAccess, DESIGN_ACCESS_TABLE, addIndex, parseIndexColumns, indexMatchesColumns, listIndexes, createIndex, validateCreateIndex, captureRowName, parseAjaxAnswer, parseCanCreate, buildCanCreateFields, buildCreateScheduleFields, XMLHTTP_PATH, DEFAULT_ACCESS_METHOD, CAPTURE_NOTE, setColumn, resolveAttributes, toStoredValue, setTable, resolveTableAttributes, } from "./table";
25
+ export type { CreateTableParams, CreateTableResult, AddColumnParams, AddColumnResult, DesignAccessFlag, EnsureDesignAccessParams, EnsureDesignAccessResult, AddIndexParams, AddIndexResult, AddIndexVerification, ListIndexesParams, ListIndexesResult, TableIndex, CreateIndexParams, CreateIndexResult, SetColumnParams, SetColumnResult, ColumnAttributes, AttributeChange, SetTableParams, SetTableResult, TableAttributes, TableGraph, NormalizedColumn, ColumnSpec, AccessFlags, OverlaySpec, } from "./table";
26
26
  export { setField } from "./setField";
27
27
  export { createRecord } from "./createRecord";
28
28
  export type { RecordWriteResult, SetFieldParams, SetFieldResult, } from "./setField";
package/dist/index.js CHANGED
@@ -7,8 +7,9 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.editFlow = exports.buildPublishModel = exports.createFlow = exports.copyFlow = exports.publishFlow = exports.readActionType = exports.readFlow = exports.formatStepPill = exports.summarizeSteps = exports.verifySteps = exports.applyStepOps = exports.validateDefineSpec = exports.planActionDefinition = exports.defineActionType = exports.editActionType = exports.publishActionType = exports.triggerPublication = exports.findStepInput = exports.fetchActionSteps = exports.slugInternalName = exports.resolveScope = exports.cloneActionType = exports.cloneSubflow = exports.verifyArtifact = exports.listTemplates = exports.sincPlugin = exports.formatCreateViewResult = exports.formatLayoutResult = exports.setRelatedLists = exports.setFormLayout = exports.setListLayout = exports.createView = exports.formatRemoveChoicesResult = exports.formatAddChoicesResult = exports.formatHostAssetsResult = exports.classifyChunks = exports.hostAssets = exports.ChoiceWriteError = exports.removeChoicesFromField = exports.addChoicesToField = exports.resolveConfigFromEnvFile = exports.createClientFromEnvFile = exports.assertWriteAllowed = exports.evaluateWriteGate = exports.isCiEnvironment = exports.resolveExecutionContext = exports.PROCESSFLOW_PATH_PREFIX = exports.isProcessflowPath = exports.resolveFlowAuth = exports.createClient = void 0;
10
- exports.parseProgressTree = exports.parseXmlAnswer = exports.PUBLISH_TARGETS = exports.parsePublishTargets = exports.describeTarget = exports.resolveUpdateSetNaming = exports.buildPublishToUpdateSetFields = exports.buildCreateUpdateSetFields = exports.buildStartFields = exports.publishApp = exports.INVOKE_REST_METHODS = exports.invokeRest = exports.verifyReadBack = exports.readBackMatches = exports.hasHtmlEntity = exports.decodeHtmlEntities = exports.deleteRecord = exports.createRecord = exports.setField = exports.resolveTableAttributes = exports.setTable = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.NOT_IN_UPDATE_SET = exports.DEFAULT_INDEX_FORM_PATH = exports.validateCreateIndex = exports.createIndex = exports.listIndexes = exports.indexMatchesColumns = exports.parseIndexColumns = exports.addIndex = exports.deriveElement = exports.addColumn = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.TYPE_MAP = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.buildColumnXml = exports.projectTableGraph = exports.createTable = exports.WriteOrderError = exports.executeWritePlan = exports.topoSort = exports.generateSysId = exports.DEFAULT_RUN_FLOW_PATH = exports.testFlow = void 0;
11
- exports.SECRET_INTERNAL_TYPES = exports.SENTINEL = exports.isCapturable = exports.secretFieldsFromDictionary = exports.mergeSecretRules = exports.loadSecretRules = exports.defaultSecretRules = exports.escapeRegExp = exports.encodeXmlText = exports.encodeXmlEntities = exports.stripJsonValue = exports.stripField = exports.plannedStrips = exports.recordFieldNames = exports.readRecordTable = exports.readField = exports.verifyStripped = exports.stripSecrets = exports.DEFAULT_EXPORT_APP_TIMEOUT_MS = exports.buildPublishFields = exports.buildCreateSetFields = exports.exportApp = exports.DEFAULT_MAX_ROWS = exports.MAX_PAGE_SIZE = exports.DEFAULT_PAGE_SIZE = exports.UPDATE_XML_FIELDS = exports.xmlEscape = exports.formatUnloadDate = exports.parseStatsCount = exports.refreshTypeFields = exports.fetchUpdateXmlRows = exports.countUpdateXml = exports.countUnloadRecords = exports.renderUnload = exports.renderRemoteUpdateSet = exports.renderUpdateXmlRow = exports.exportUpdateSet = exports.PUBLISH_POLL_DELAYS_MS = exports.DEFAULT_PUBLISH_TIMEOUT_MS = exports.parseCicdProgress = exports.parseCicdPublishResponse = exports.harvestProgressResults = exports.flattenSteps = exports.classifyProgress = void 0;
10
+ exports.publishApp = exports.INVOKE_REST_METHODS = exports.invokeRest = exports.verifyReadBack = exports.readBackMatches = exports.hasHtmlEntity = exports.decodeHtmlEntities = exports.deleteRecord = exports.createRecord = exports.setField = exports.resolveTableAttributes = exports.setTable = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.CAPTURE_NOTE = exports.DEFAULT_ACCESS_METHOD = exports.XMLHTTP_PATH = exports.buildCreateScheduleFields = exports.buildCanCreateFields = exports.parseCanCreate = exports.parseAjaxAnswer = exports.captureRowName = exports.validateCreateIndex = exports.createIndex = exports.listIndexes = exports.indexMatchesColumns = exports.parseIndexColumns = exports.addIndex = exports.DESIGN_ACCESS_TABLE = exports.findDesignAccess = exports.ensureDesignAccess = exports.deriveElement = exports.addColumn = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.TYPE_MAP = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.buildColumnXml = exports.projectTableGraph = exports.createTable = exports.WriteOrderError = exports.executeWritePlan = exports.topoSort = exports.generateSysId = exports.DEFAULT_RUN_FLOW_PATH = exports.testFlow = void 0;
11
+ exports.secretFieldsFromDictionary = exports.mergeSecretRules = exports.loadSecretRules = exports.defaultSecretRules = exports.escapeRegExp = exports.encodeXmlText = exports.encodeXmlEntities = exports.stripJsonValue = exports.stripField = exports.plannedStrips = exports.recordFieldNames = exports.readRecordTable = exports.readField = exports.verifyStripped = exports.stripSecrets = exports.DEFAULT_EXPORT_APP_TIMEOUT_MS = exports.buildPublishFields = exports.buildCreateSetFields = exports.exportApp = exports.DEFAULT_MAX_ROWS = exports.MAX_PAGE_SIZE = exports.DEFAULT_PAGE_SIZE = exports.UPDATE_XML_FIELDS = exports.xmlEscape = exports.formatUnloadDate = exports.parseStatsCount = exports.refreshTypeFields = exports.fetchUpdateXmlRows = exports.countUpdateXml = exports.countUnloadRecords = exports.renderUnload = exports.renderRemoteUpdateSet = exports.renderUpdateXmlRow = exports.exportUpdateSet = exports.PUBLISH_POLL_DELAYS_MS = exports.DEFAULT_PUBLISH_TIMEOUT_MS = exports.parseCicdProgress = exports.parseCicdPublishResponse = exports.harvestProgressResults = exports.flattenSteps = exports.classifyProgress = exports.parseProgressTree = exports.parseXmlAnswer = exports.PUBLISH_TARGETS = exports.parsePublishTargets = exports.describeTarget = exports.resolveUpdateSetNaming = exports.buildPublishToUpdateSetFields = exports.buildCreateUpdateSetFields = exports.buildStartFields = void 0;
12
+ exports.SECRET_INTERNAL_TYPES = exports.SENTINEL = exports.isCapturable = void 0;
12
13
  var client_1 = require("./client");
13
14
  Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } });
14
15
  Object.defineProperty(exports, "resolveFlowAuth", { enumerable: true, get: function () { return client_1.resolveFlowAuth; } });
@@ -91,14 +92,23 @@ Object.defineProperty(exports, "DEFAULT_SUPER_CLASS", { enumerable: true, get: f
91
92
  Object.defineProperty(exports, "DEFAULT_SAVE_ACTION", { enumerable: true, get: function () { return table_1.DEFAULT_SAVE_ACTION; } });
92
93
  Object.defineProperty(exports, "addColumn", { enumerable: true, get: function () { return table_1.addColumn; } });
93
94
  Object.defineProperty(exports, "deriveElement", { enumerable: true, get: function () { return table_1.deriveElement; } });
95
+ Object.defineProperty(exports, "ensureDesignAccess", { enumerable: true, get: function () { return table_1.ensureDesignAccess; } });
96
+ Object.defineProperty(exports, "findDesignAccess", { enumerable: true, get: function () { return table_1.findDesignAccess; } });
97
+ Object.defineProperty(exports, "DESIGN_ACCESS_TABLE", { enumerable: true, get: function () { return table_1.DESIGN_ACCESS_TABLE; } });
94
98
  Object.defineProperty(exports, "addIndex", { enumerable: true, get: function () { return table_1.addIndex; } });
95
99
  Object.defineProperty(exports, "parseIndexColumns", { enumerable: true, get: function () { return table_1.parseIndexColumns; } });
96
100
  Object.defineProperty(exports, "indexMatchesColumns", { enumerable: true, get: function () { return table_1.indexMatchesColumns; } });
97
101
  Object.defineProperty(exports, "listIndexes", { enumerable: true, get: function () { return table_1.listIndexes; } });
98
102
  Object.defineProperty(exports, "createIndex", { enumerable: true, get: function () { return table_1.createIndex; } });
99
103
  Object.defineProperty(exports, "validateCreateIndex", { enumerable: true, get: function () { return table_1.validateCreateIndex; } });
100
- Object.defineProperty(exports, "DEFAULT_INDEX_FORM_PATH", { enumerable: true, get: function () { return table_1.DEFAULT_INDEX_FORM_PATH; } });
101
- Object.defineProperty(exports, "NOT_IN_UPDATE_SET", { enumerable: true, get: function () { return table_1.NOT_IN_UPDATE_SET; } });
104
+ Object.defineProperty(exports, "captureRowName", { enumerable: true, get: function () { return table_1.captureRowName; } });
105
+ Object.defineProperty(exports, "parseAjaxAnswer", { enumerable: true, get: function () { return table_1.parseAjaxAnswer; } });
106
+ Object.defineProperty(exports, "parseCanCreate", { enumerable: true, get: function () { return table_1.parseCanCreate; } });
107
+ Object.defineProperty(exports, "buildCanCreateFields", { enumerable: true, get: function () { return table_1.buildCanCreateFields; } });
108
+ Object.defineProperty(exports, "buildCreateScheduleFields", { enumerable: true, get: function () { return table_1.buildCreateScheduleFields; } });
109
+ Object.defineProperty(exports, "XMLHTTP_PATH", { enumerable: true, get: function () { return table_1.XMLHTTP_PATH; } });
110
+ Object.defineProperty(exports, "DEFAULT_ACCESS_METHOD", { enumerable: true, get: function () { return table_1.DEFAULT_ACCESS_METHOD; } });
111
+ Object.defineProperty(exports, "CAPTURE_NOTE", { enumerable: true, get: function () { return table_1.CAPTURE_NOTE; } });
102
112
  Object.defineProperty(exports, "setColumn", { enumerable: true, get: function () { return table_1.setColumn; } });
103
113
  Object.defineProperty(exports, "resolveAttributes", { enumerable: true, get: function () { return table_1.resolveAttributes; } });
104
114
  Object.defineProperty(exports, "toStoredValue", { enumerable: true, get: function () { return table_1.toStoredValue; } });
@@ -10,7 +10,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
10
  import { z } from "zod";
11
11
  import type { ToolAnnotations } from "@tenonhq/dovetail-mcp-kit";
12
12
  import type { ServiceNowClient } from "../client";
13
- export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "remove_choices_from_field", "flow_view", "action_view", "action_edit", "action_clone", "action_define", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "add_index", "index_list", "index_create", "set_column", "set_table", "set_field", "create_record", "delete_record", "host_assets", "invoke_rest", "app_publish", "update_set_export", "app_export"];
13
+ export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "remove_choices_from_field", "flow_view", "action_view", "action_edit", "action_clone", "action_define", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "design_access", "add_index", "index_list", "index_create", "set_column", "set_table", "set_field", "create_record", "delete_record", "host_assets", "invoke_rest", "app_publish", "update_set_export", "app_export"];
14
14
  export type ToolName = (typeof TOOL_NAMES)[number];
15
15
  export interface RegistryDeps {
16
16
  /** Optional client injection for tests; defaults to createClient({}). */
@@ -57,6 +57,7 @@ exports.TOOL_NAMES = [
57
57
  "flow_edit",
58
58
  "create_table",
59
59
  "add_column",
60
+ "design_access",
60
61
  "add_index",
61
62
  "index_list",
62
63
  "index_create",
@@ -445,7 +446,11 @@ function buildDescriptors(deps = {}) {
445
446
  "<scope>_<name>, the dictionary row and its update-set capture land in scope); the table " +
446
447
  "must allow new fields from other scopes (sys_db_object.alter_access) and updateSetSysId " +
447
448
  "must belong to scope — both checked on dryRun too, and the stored element + sys_scope are " +
448
- "read back. updateSetSysId is required on the live path. dryRun:true returns the plan with " +
449
+ "read back. A cross-scope result carries designAccess { present, sysId, created }: the " +
450
+ "platform UI refuses cross-scope authoring without a sys_scope_design_access record " +
451
+ "(scope -> the table's scope), so a missing one is FLAGGED (not blocking — the headless " +
452
+ "insert works without it); ensureDesignAccess:true creates it first in the same update " +
453
+ "set. updateSetSysId is required on the live path. dryRun:true returns the plan with " +
449
454
  "no writes.",
450
455
  shape: schemas_1.addColumnSchema.shape,
451
456
  handler: async function (args) {
@@ -465,12 +470,43 @@ function buildDescriptors(deps = {}) {
465
470
  column: p.column,
466
471
  scope: p.scope,
467
472
  crossScope: p.crossScope,
473
+ ensureDesignAccess: p.ensureDesignAccess,
468
474
  updateSetSysId: p.updateSetSysId,
469
475
  dryRun: p.dryRun,
470
476
  debug: p.debug,
471
477
  });
472
478
  },
473
479
  },
480
+ {
481
+ name: "design_access",
482
+ annotations: dovetail_mcp_kit_1.WRITE_CREATE,
483
+ description: "Ensure the sys_scope_design_access record that lets sourceScope (the AUTHORING app) " +
484
+ "design in targetScope's tables (the app that OWNS them). The platform UI requires it " +
485
+ "for cross-scope columns — the symptom is \"Invalid 'Table' selected on the Dictionary " +
486
+ "Entry record ... can only select '<app>' tables with read access enabled\", and the " +
487
+ "table's own access flags are NOT the gate. Scopes are names or sys_scope sys_ids. " +
488
+ "Idempotent: an existing record returns status 'exists' and nothing is written. Live " +
489
+ "creates it via the scope-aware createRecord op switched to sourceScope, captured in " +
490
+ "updateSetSysId (must belong to sourceScope), then reads it back and asserts " +
491
+ "source/target. updateSetSysId is required on the live path; dryRun:true only reports " +
492
+ "exists / missing.",
493
+ shape: schemas_1.designAccessSchema.shape,
494
+ handler: async function (args) {
495
+ var p = schemas_1.designAccessSchema.parse(args);
496
+ if (p.dryRun !== true &&
497
+ (!p.updateSetSysId || !p.updateSetSysId.trim())) {
498
+ throw new Error("design_access: updateSetSysId (an update set in sourceScope) is required on " +
499
+ "the live path — set dryRun:true to only check whether the record exists.");
500
+ }
501
+ return (0, table_1.ensureDesignAccess)({
502
+ client: client(),
503
+ sourceScope: p.sourceScope,
504
+ targetScope: p.targetScope,
505
+ updateSetSysId: p.updateSetSysId,
506
+ dryRun: p.dryRun,
507
+ });
508
+ },
509
+ },
474
510
  {
475
511
  name: "add_index",
476
512
  annotations: dovetail_mcp_kit_1.WRITE_OVERWRITE,
@@ -539,7 +575,7 @@ function buildDescriptors(deps = {}) {
539
575
  "admin (an ACL that refuses GET refuses POST), and sys_index_column does not exist at " +
540
576
  "all (HTTP 400 'Invalid table') — there is no two-table index model to join. Each row " +
541
577
  "returns { name, columns, type, rawColumns }: `columns` is v_db_index's bracketed " +
542
- "`column_names` cell ('[phone]', '[a,b]') PARSED into a list, never substring-matched, " +
578
+ "`column_names` cell ('[phone]', '[a;b]' — semicolon-separated when composite) PARSED into a list, never substring-matched, " +
543
579
  "and `type` is access_method (btree for essentially everything). UNIQUENESS IS NOT " +
544
580
  "READABLE: v_db_index carries no uniqueness field, so a unique index and an ordinary " +
545
581
  "one are indistinguishable in it — `unique` is therefore left ABSENT rather than " +
@@ -556,28 +592,38 @@ function buildDescriptors(deps = {}) {
556
592
  name: "index_create",
557
593
  annotations: dovetail_mcp_kit_1.WRITE_ADDITIVE_IDEMPOTENT,
558
594
  description: "Create a DATABASE INDEX on an EXISTING ServiceNow table — including the COMPOSITE and " +
559
- "NON-UNIQUE indexes add_index cannot build. A DATABASE INDEX IS A PHYSICAL, PER-INSTANCE " +
560
- "CHANGE: IT IS NOT CAPTURED IN AN UPDATE SET AND DOES NOT TRAVEL WITH A PROMOTION, so it " +
561
- "must be re-run against every environment that needs it — which is why this tool takes " +
562
- "no updateSetSysId. There is no record path to an index (sys_index is API-level-ACL 403, " +
563
- "sys_index_column does not exist, and sys_dictionary.unique — the add_index lever — is " +
564
- "per-column and unique-only), so this replays the platform's own index-creator form " +
565
- "(sys_action=create_index) over a form-login session. DRY-RUN BY DEFAULT: without " +
566
- "confirm:true NOTHING is sent and nothing is read; dryRun:true forces a dry-run even " +
567
- "with confirm. IDEMPOTENT: on the live path v_db_index is read first, and an index over " +
568
- "exactly these columns returns 'already-exists' with no write. `columns` is ORDERED — " +
569
- "order is part of an index's identity and is preserved verbatim. `name` is REFUSED: the " +
570
- "platform's form has no name input (ServiceNow names the index itself), so accepting one " +
571
- "would mean reporting a name the instance does not carry — the created index's real name " +
572
- "comes back in `name`. After the POST the index is polled for in v_db_index (default 10 " +
573
- "checks, 3s apart, since a build on a populated table is asynchronous); if it never " +
574
- "appears the result is 'failed', because a form processor returning a page is not " +
575
- "evidence an ALTER ran — and a unique index cannot build over duplicate values, EMPTY " +
576
- "included. verified:true means a matching row was READ BACK. 'uniqueness-enforced' is " +
577
- "ALWAYS in unverified: v_db_index has no uniqueness field, so enforcement is provable " +
578
- "only by a duplicate-insert test. Requires a username+password identity that can form-" +
579
- "log-in; an instance on API-key-only auth or SSO/MFA will fail at the session with a " +
580
- "diagnosis, because no .do replay can work there.",
595
+ "NON-UNIQUE indexes add_index cannot build. AN INDEX IS CAPTURED IN AN UPDATE SET: the " +
596
+ "platform's build job writes a sys_update_xml row (type=Indexes, name " +
597
+ "sys_index_<table>_<col>_…) into the session user's CURRENT update set, so " +
598
+ "updateSetSysId is REQUIRED on the live path — the tool pins that set as current " +
599
+ "(Dovetail changeUpdateSet, read back) BEFORE scheduling, refuses a set outside the " +
600
+ "table's application scope or not 'in progress', and reads the capture row back " +
601
+ "from that set afterwards. The physical index is still built per instance; committing " +
602
+ "the set elsewhere rebuilds it. There is no record path to an index (sys_index is " +
603
+ "API-level-ACL 403, sys_index_column does not exist, and sys_dictionary.unique — the " +
604
+ "add_index lever — is per-column and unique-only), so this replays the two GlideAjax " +
605
+ "calls the platform's own Database Indexes dialog makes on xmlhttp.do over a " +
606
+ "form-login session: IndexCreatorErrorChecker.canCreate (the dialog's pre-flight; a " +
607
+ "canCreate:false verdict is returned verbatim with its errorCode and nothing is " +
608
+ "scheduled) then ScheduleCreator.createSchedule (sysparm_table, sysparm_fields, " +
609
+ "sysparm_access_method — default btree — sysparm_unique true|false, no email, no " +
610
+ "name). DRY-RUN BY DEFAULT: without confirm:true NOTHING is sent and nothing is read; " +
611
+ "dryRun:true forces a dry-run even with confirm. IDEMPOTENT: on the live path " +
612
+ "v_db_index is read first, and an index over exactly these columns returns " +
613
+ "'already-exists' with no write. `columns` is ORDERED — order is part of an " +
614
+ "index's identity and is preserved verbatim. `name` is REFUSED: the dialog has no " +
615
+ "name input (ServiceNow names the index after its leading column), so accepting one " +
616
+ "would mean reporting a name the instance does not carry — the real name comes back " +
617
+ "in `name`. After scheduling, the index is polled for in v_db_index (default 10 " +
618
+ "checks, 3s apart); if it never appears the result is 'failed', because an accepted " +
619
+ "schedule is not evidence an ALTER ran — and a unique index cannot build over " +
620
+ "duplicate values, EMPTY included. verified:true means a matching row was READ BACK; " +
621
+ "captured:true means the sys_update_xml row was READ BACK from the pinned set (an " +
622
+ "index that exists but was not captured is created:true, captured:false with " +
623
+ "'update-set-capture' in unverified). 'uniqueness-enforced' is ALWAYS in " +
624
+ "unverified: v_db_index has no uniqueness field. Requires a username+password identity " +
625
+ "that can form-log-in; xmlhttp.do ignores Basic auth and API keys, so an API-key-only " +
626
+ "or SSO/MFA identity fails at the session with a diagnosis (TenonHQ/Dovetail#292).",
581
627
  shape: schemas_1.createIndexSchema.shape,
582
628
  handler: async function (args) {
583
629
  var p = schemas_1.createIndexSchema.parse(args);
@@ -585,6 +631,7 @@ function buildDescriptors(deps = {}) {
585
631
  client: client(),
586
632
  table: p.table,
587
633
  columns: p.columns,
634
+ updateSetSysId: p.updateSetSysId,
588
635
  unique: p.unique,
589
636
  name: p.name,
590
637
  accessMethod: p.accessMethod,