@tenonhq/dovetail-servicenow 0.0.50 → 0.0.52

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,60 +736,102 @@ 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
743
743
  and an ordinary one are indistinguishable in it: `unique` is left **absent** rather than
744
744
  guessed, and `uniqueness-enforced` is reported in `unverified` on every result. Only a
745
- duplicate-insert test proves enforcement. An empty result more likely means the table name
746
- is wrong than that the table is unindexed - every physical table has a `PRIMARY`.
745
+ duplicate-insert test proves enforcement. An empty result for a table that does not exist
746
+ means the table name is wrong - every physical table has a `PRIMARY`.
747
+
748
+ **Table-per-hierarchy children list their storage root's indexes.** `v_db_index` lists
749
+ indexes by *physical* table, and a table stored in an ancestor's physical table (anything
750
+ extending `task`, for example) has no rows under its own name. When the table exists but
751
+ has no rows, its `super_class` chain is walked to the first ancestor that has them, and
752
+ that root's indexes are listed - with `storageTable` and the note naming the root.
753
+ `index-create` and `add-index` **refuse** such a child on the live path, naming the root,
754
+ rather than build on it or report a false "NOT created".
747
755
 
748
756
  ### Create an index (composite and non-unique included)
749
757
 
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.
758
+ > **An index IS captured in an update set.** The platform's build job writes a
759
+ > `sys_update_xml` row (`type=Indexes`, name `sys_index_<table>_<col>_<col>…`) into the
760
+ > session user's **current** update set - the Database Indexes dialog says so itself, and
761
+ > it was read back live on tenonworkstudio 2026-10-08. `--update-set` is therefore
762
+ > **required** on the live path: the verb pins that set as current first and reads the
763
+ > capture row back from it afterwards. The physical index is still built per instance;
764
+ > committing the set elsewhere rebuilds it there.
754
765
 
755
766
  ```bash
756
- # Dry-run (the DEFAULT) - sends nothing and reads nothing
767
+ # Dry-run (the DEFAULT) - sends nothing and reads nothing; no update set needed
757
768
  npx dove-sn index-create --table x_cadso_journey_instance --columns state,created_on
758
769
 
759
770
  # Send it
760
771
  npx dove-sn index-create \
761
- --table x_cadso_journey_instance --columns state,created_on --confirm --json
772
+ --table x_cadso_journey_instance --columns state,created_on \
773
+ --update-set <sys_id> --confirm --json
762
774
  ```
763
775
 
764
776
  This is what `add-index` cannot do. `sys_dictionary.unique` - the only record-shaped lever
765
777
  - 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>`.
778
+ all (`sys_index` is API-level-ACL 403, `sys_index_column` does not exist). `index-create`
779
+ instead replays the two GlideAjax calls the platform's own Database Indexes dialog makes
780
+ on `xmlhttp.do` over a form-login session - taken from a HAR of the Studio dialog plus the
781
+ `dialog_index_create` UI page's client script, both read live:
782
+
783
+ 1. `IndexCreatorErrorChecker` / `canCreate` - the dialog's pre-flight (`sysparm_table_name`,
784
+ `sysparm_field_names`, `sysparm_unique`, `sysparm_access_method`). A `canCreate:false`
785
+ verdict is returned verbatim with its `errorCode` and nothing is scheduled.
786
+ 2. `ScheduleCreator` / `createSchedule` - schedules the build (`sysparm_table`,
787
+ `sysparm_fields`, `sysparm_access_method` - default `btree`, exactly as the dialog's
788
+ client JS does - `sysparm_unique` `true|false`, empty `sysparm_email` = no
789
+ notification, empty `sysparm_schedule_name`).
790
+
791
+ (The `index_creator_dialog` page's `sys_action=create_index` form is inert - the browser
792
+ never submits it, and the UI page has no processing script. The earlier form-POST replay
793
+ was a silent no-op for that reason.)
771
794
 
772
795
  - **Dry-run by default.** Without `--confirm` nothing is sent *and nothing is read*;
773
796
  `--dry-run` forces a plan even with `--confirm`.
797
+ - **Target checks before anything else.** The table must exist; the update set must exist,
798
+ be `in progress`, and belong to the table's application scope - a set in another scope
799
+ is refused, because the capture row would land in the wrong scope.
774
800
  - **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).
801
+ these columns short-circuits to `already-exists` with no pin, no form session and no
802
+ write. Column **order** is part of an index's identity - `[a;b]` is not `[b;a]`.
803
+ - **Table-per-hierarchy children are refused.** A table with no `v_db_index` rows of its
804
+ own whose ancestor has them is stored in that ancestor's physical table; the run stops
805
+ before any write and names the root (run against the root if that is what you want).
806
+ - **Identity check (warns, never refuses).** The pin runs through the REST client (an API
807
+ key when one is configured) but the build is scheduled by the form session (always
808
+ `SN_USER`), and the build captures into the *form* user's current set. The REST caller's
809
+ own `sys_user` row is read first; when it is not the form-login user (or cannot be read)
810
+ the run still goes ahead and every result note carries an `IDENTITY WARNING`. The capture
811
+ read-back then reports where the row actually landed (`captured` / `captureFoundIn`).
812
+ - **The pin is read back.** The set is pinned with Dovetail's own `changeUpdateSet` and
813
+ `currentUpdateSet` is read; a pin that did not take stops the run before the session
814
+ opens.
815
+ - **`--name` is refused.** The dialog has no name input; ServiceNow names the index after
816
+ its leading column (live: `[sys_created_on;status;version_step;version]` → index
817
+ `sys_created_on`). The created index's *real* name is returned in `name`.
818
+ - **The read-back is the proof.** After scheduling, the index is polled for in `v_db_index`
819
+ (default 10 checks, 3 s apart). If it never appears the status is `failed`: an accepted
820
+ schedule is not evidence an ALTER ran, and a unique index cannot build over duplicate
821
+ values (EMPTY counts). Then the capture row is looked for in the pinned set:
822
+ `captured:true` only when it was read back; an index that exists but was not captured
823
+ is `created:true, captured:false` with `update-set-capture` in `unverified` - and
824
+ exit code 2, because it will not travel. In that case the capture name is searched
825
+ across every set and the set(s) it actually landed in come back in `captureFoundIn`.
784
826
  - **Uniqueness is still never claimed.** `uniqueness-enforced` stays in `unverified` on
785
827
  every status.
786
828
 
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.
829
+ **Requires a username+password identity that can form-log-in.** `xmlhttp.do` ignores
830
+ Basic auth and API keys, so an API-key-only, SSO or MFA identity fails at the session with
831
+ that diagnosis (TenonHQ/Dovetail#292).
791
832
 
792
- Exit codes: `0` created / already-exists / dry-run, `1` bad args, `2` failed.
833
+ Exit codes: `0` created / already-exists / dry-run, `1` bad args, `2` failed - and `2`
834
+ when the index was created but its capture row was not found in the pinned set.
793
835
 
794
836
  ### Set a field on a record
795
837
 
@@ -835,7 +877,8 @@ npx dove-sn create-record \
835
877
  switches the executing user's app scope + update set server-side, inserts, and
836
878
  restores both — so the record is owned by the right app and the insert is captured
837
879
  in the right update set. Like `set-field` it **refuses** schema tables and verifies
838
- via read-back. `--scope` and `--update-set` are required; `--if-absent
880
+ via read-back. It also **refuses** `sys_update_set` — the op cannot set an update
881
+ set's application, so create sets with `dove createUpdateSet` instead. `--scope` and `--update-set` are required; `--if-absent
839
882
  "<encoded-query>"` makes re-runs idempotent (the insert is skipped when the query
840
883
  already matches a row). Exit codes: `0` created / skipped-in-sync / dry-run, `1` bad
841
884
  args, `2` write landed unverified (or skipped with drift). To **update** an existing
@@ -892,14 +935,19 @@ npx dove-sn delete-record \
892
935
  `delete-record` wraps the core `deleteRecord` op. It is **dry-run by default** —
893
936
  nothing is deleted without `--apply` (`--dry-run` wins if both are given). `--sys-id`
894
937
  must be a 32-character lowercase hex id and `--table` a plain table name; both are
895
- validated before any network call. `--update-set` is **required** and sent with the
896
- delete, but **the capture is not pinned yet**: until
897
- [#297](https://github.com/TenonHQ/Dovetail/issues/297) ships server-side, the op ignores
898
- `update_set_sys_id` and captures the delete into the session's **current** update set —
899
- make that the set you want before `--apply`. Every result note repeats this caveat; the
900
- client keeps sending the field so it takes effect the moment the server honours it. Like its siblings it **refuses** schema tables (`sys_db_object` /
901
- `sys_dictionary`). Exit codes: `0` deleted / dry-run, `1` bad args or no such record,
902
- `2` the delete returned but the record is **still present** on read-back.
938
+ validated before any network call. `--update-set` is **required**, must exist and be
939
+ **in progress** (checked on the dry-run too). Until
940
+ [#297](https://github.com/TenonHQ/Dovetail/issues/297) ships server-side the op ignores
941
+ `update_set_sys_id` and captures the delete into the session's **current** update set, so
942
+ the verb pins `--update-set` as current first (refusing, nothing deleted, if the pin does
943
+ not read back) and then reads the DELETE row back from `sys_update_xml`. The result carries
944
+ `captured`, `capturedInto` and `captureState` (`in-set` / `other-set` / `none` /
945
+ `unverified`). Like its siblings it **refuses** schema tables (`sys_db_object` /
946
+ `sys_dictionary`). A failed delete call never skips the read-back. Exit codes: `0` deleted
947
+ and captured in the set, a table that writes no capture at all (`none`, with a note), or a
948
+ dry-run; `1` bad args, no such record, or an unknown/closed update set; `2` the record is
949
+ **still present** on read-back (including a server-refused delete), its state is unknown,
950
+ the pin did not take, or the DELETE landed in a different set / could not be read back.
903
951
 
904
952
  All three verbs are exported for programmatic use:
905
953
 
@@ -917,6 +965,36 @@ var r = await setField({
917
965
  console.log(r.status, r.verified); // "applied" true
918
966
  ```
919
967
 
968
+ ### Register UI component events (sync-ux-events)
969
+
970
+ UI Builder can only map an event a component dispatches when it exists as a
971
+ `sys_ux_event` record **and** is listed in the component macroponent's
972
+ `dispatched_events`. A component deploy does not reliably create either, so
973
+ `sync-ux-events` reads the component's `now-ui.json` and reconciles every
974
+ `components.<tag>.actions[]` entry against the instance.
975
+
976
+ ```bash
977
+ # Dry-run (the default) — per action: ok / create / link / drift / ambiguous, plus orphans
978
+ npx dove-sn sync-ux-events --file path/to/now-ui.json
979
+
980
+ # Apply — creates the missing events and appends them to dispatched_events
981
+ npx dove-sn sync-ux-events --file path/to/now-ui.json \
982
+ --component cadso-journey-builder --update-set <sys_id> --apply
983
+ ```
984
+
985
+ The macroponent is the `sys_ux_macroponent` (category `component`) whose
986
+ `root_component` is the `sys_ux_lib_component` with that tag; events are created in
987
+ the macroponent's scope via the scope-aware `createRecord` op and the list edit goes
988
+ through `pushWithUpdateSet`, both captured into `--update-set`. The write is
989
+ **append-only** — existing `dispatched_events` entries are never dropped or reordered —
990
+ and is re-read to verify. Label/description **drift** and **orphans** (linked but no
991
+ longer declared) are reported, never changed; an event name carried by several records
992
+ is **ambiguous** and never written. Event names and tags are charset-validated before
993
+ they reach a query. Re-runs are no-ops. Exit codes: `0` in sync / dry-run / applied and
994
+ verified; `1` bad args or unreadable file; `2` an unresolved or ambiguous
995
+ component/event, or a write that did not verify. MCP: `sync_ux_events` (dry-run unless
996
+ `confirm:true`).
997
+
920
998
  ### Invoke an arbitrary REST operation
921
999
 
922
1000
  Invoke any authenticated ServiceNow REST operation — an application's own
@@ -1167,15 +1245,15 @@ read back from the `v_db_index` view - uniqueness enforcement is always reported
1167
1245
  unverified) / `index_list` (read-only: a table's database indexes from `v_db_index`,
1168
1246
  the only index read surface - `sys_index` is API-level-ACL 403 and `sys_index_column`
1169
1247
  does not exist) / `index_create` (create an index, composite and non-unique included, by
1170
- replaying the platform index-creator form; dry-run by default, idempotent, read back from
1171
- `v_db_index` - and **not** captured in an update set, because a database index is a
1172
- physical per-instance change), the record-write verbs `set_field` (update scalar fields on an
1248
+ replaying the Database Indexes dialog's own processor calls; dry-run by default, idempotent,
1249
+ pinned to a required update set, read back from `v_db_index` and the capture row read back
1250
+ from `sys_update_xml`), the record-write verbs `set_field` (update scalar fields on an
1173
1251
  existing record), `create_record` (insert one record) and `delete_record` (delete one
1174
1252
  record — dry-run by default, `confirm:true` to apply, `updateSetSysId` required, the
1175
1253
  record read back before AND after so success is only reported once it is confirmed
1176
1254
  gone) — all read-back-verified; `set_field` / `create_record` are captured in the
1177
- update set you pass, while `delete_record` captures into the session's current set
1178
- until [#297](https://github.com/TenonHQ/Dovetail/issues/297) ships — `host_assets` (deploy a built
1255
+ update set you pass, while `delete_record` pins the set as current and reads the DELETE
1256
+ capture back (`captureState`) until [#297](https://github.com/TenonHQ/Dovetail/issues/297) ships — `host_assets` (deploy a built
1179
1257
  dist/), plus the Flow Designer
1180
1258
  tools `flow_view` (read a flow/subflow's step graph), `action_view` (read an action
1181
1259
  type's model), `action_edit` (structurally edit a published action type — per-step
package/dist/cli.js CHANGED
@@ -86,6 +86,7 @@ const deleteRecord_1 = require("./deleteRecord");
86
86
  const invokeRest_1 = require("./invokeRest");
87
87
  const publishApp_1 = require("./publishApp");
88
88
  const hostAssets_1 = require("./hostAssets");
89
+ const uxEvents_1 = require("./uxEvents");
89
90
  const flowDesigner_formatter_2 = require("./flowDesigner-formatter");
90
91
  function parseArgs(argv) {
91
92
  // `dove-sn --help` has no verb: a leading flag means an empty command and the
@@ -1572,7 +1573,14 @@ async function runIndexList(flags) {
1572
1573
  process.stdout.write(JSON.stringify(result, null, 2) + "\n");
1573
1574
  return 0;
1574
1575
  }
1575
- process.stdout.write(result.table + " — " + result.indexes.length + " index(es)\n");
1576
+ process.stdout.write(result.table +
1577
+ " — " +
1578
+ result.indexes.length +
1579
+ " index(es)" +
1580
+ (result.storageTable && result.storageTable !== result.table
1581
+ ? " (stored in " + result.storageTable + "'s physical table — these are its indexes)"
1582
+ : "") +
1583
+ "\n");
1576
1584
  for (var i = 0; i < result.indexes.length; i += 1) {
1577
1585
  var idx = result.indexes[i];
1578
1586
  process.stdout.write(" " +
@@ -1588,16 +1596,19 @@ async function runIndexList(flags) {
1588
1596
  }
1589
1597
  /**
1590
1598
  * dove-sn index-create:
1591
- * --table x_cadso_core_u_smoke --columns a,b [--unique] [--access-method <m>]
1592
- * [--confirm] [--dry-run] [--poll-attempts <n>] [--poll-interval-ms <n>]
1593
- * [--debug] [--json]
1599
+ * --table x_cadso_core_u_smoke --columns a,b --update-set <sys_id> [--unique]
1600
+ * [--access-method <m>] [--confirm] [--dry-run] [--poll-attempts <n>]
1601
+ * [--poll-interval-ms <n>] [--debug] [--json]
1594
1602
  *
1595
1603
  * DRY-RUN BY DEFAULT — nothing is sent (and nothing is even READ) without --confirm;
1596
- * --dry-run forces a plan even with it. There is deliberately NO --update-set: a
1597
- * database index is physical and is not captured in one.
1604
+ * --dry-run forces a plan even with it. --update-set is REQUIRED on the live path:
1605
+ * the build job captures the index definition (sys_update_xml type=Indexes) into the
1606
+ * user's CURRENT update set, so the verb pins that set first and reads the capture
1607
+ * row back from it afterwards.
1598
1608
  *
1599
1609
  * Exit codes: 0 created / already-exists / dry-run, 1 bad args, 2 failed (which
1600
- * includes "the form was posted but no index was read back").
1610
+ * includes "scheduled but no index was read back") — and 2 when the index was read
1611
+ * back but its capture row was NOT found in the pinned set.
1601
1612
  */
1602
1613
  async function runIndexCreate(flags) {
1603
1614
  var columns = splitList(flags.columns || "");
@@ -1605,10 +1616,12 @@ async function runIndexCreate(flags) {
1605
1616
  process.stderr.write("index-create: --table <name> and --columns <a[,b,...]> are required\n");
1606
1617
  return 1;
1607
1618
  }
1608
- if (flags["update-set"]) {
1609
- process.stderr.write("index-create: --update-set is not accepted. A database index is a PHYSICAL, " +
1610
- "PER-INSTANCE change — it is NOT captured in an update set and does not " +
1611
- "travel with a promotion. Run index-create against each environment.\n");
1619
+ // DRY-RUN BY DEFAULT: --confirm is what sends; --dry-run forces a plan even with it.
1620
+ var indexDryRun = flags["dry-run"] === "true" || flags.confirm !== "true";
1621
+ if (!indexDryRun && !flags["update-set"]) {
1622
+ process.stderr.write("index-create: --update-set is required on the live path — the index " +
1623
+ "definition is captured into the user's CURRENT update set, so it must be " +
1624
+ "pinned first (only a dry-run works without one)\n");
1612
1625
  return 1;
1613
1626
  }
1614
1627
  var params = {
@@ -1623,8 +1636,8 @@ async function runIndexCreate(flags) {
1623
1636
  params.name = flags.name;
1624
1637
  if (flags["access-method"])
1625
1638
  params.accessMethod = flags["access-method"];
1626
- if (flags["form-path"])
1627
- params.formPath = flags["form-path"];
1639
+ if (flags["update-set"])
1640
+ params.updateSetSysId = flags["update-set"];
1628
1641
  if (flags.debug === "true")
1629
1642
  params.debug = true;
1630
1643
  // A non-integer poll setting would make the bounded wait unbounded (or zero).
@@ -1663,6 +1676,12 @@ async function runIndexCreate(flags) {
1663
1676
  (result.name ? " -> " + result.name : "") +
1664
1677
  (result.instance ? " on " + result.instance : "") +
1665
1678
  (result.verified ? " — verified" : "") +
1679
+ (result.updateSet
1680
+ ? " [update set " +
1681
+ (result.updateSet.name || result.updateSet.sysId) +
1682
+ (result.captured ? " — captured" : " — capture NOT verified") +
1683
+ "]"
1684
+ : "") +
1666
1685
  "\n" +
1667
1686
  result.note +
1668
1687
  "\nUNVERIFIED: " +
@@ -1671,6 +1690,10 @@ async function runIndexCreate(flags) {
1671
1690
  }
1672
1691
  if (result.status === "failed")
1673
1692
  return 2;
1693
+ // An index that exists but whose definition was not captured will not travel —
1694
+ // surface that as a non-zero exit, the same way an unread-back index is.
1695
+ if (result.status === "created" && !result.captured)
1696
+ return 2;
1674
1697
  return 0;
1675
1698
  }
1676
1699
  /** Parse a CLI boolean flag. Bare `--mandatory` means true; `--mandatory false` means
@@ -1991,18 +2014,76 @@ async function runCreateRecord(flags) {
1991
2014
  return 2;
1992
2015
  return 0;
1993
2016
  }
2017
+ /**
2018
+ * dove-sn sync-ux-events:
2019
+ * --file <path/to/now-ui.json>
2020
+ * [--component <tag>]
2021
+ * [--update-set <sys_id>] (required with --apply)
2022
+ * [--apply] [--dry-run] [--json]
2023
+ * DRY-RUN BY DEFAULT. Exit codes: 0 in sync / dry-run / applied+verified, 1 bad args or
2024
+ * unreadable file, 2 unresolved/ambiguous component or event, or an unverified write.
2025
+ */
2026
+ async function runSyncUxEvents(flags, bare) {
2027
+ var bareErr = bareStringFlagError("sync-ux-events", bare);
2028
+ if (bareErr) {
2029
+ process.stderr.write(bareErr);
2030
+ return 1;
2031
+ }
2032
+ var file = flags.file;
2033
+ if (!file) {
2034
+ process.stderr.write("sync-ux-events: --file <now-ui.json> is required\n");
2035
+ return 1;
2036
+ }
2037
+ var apply = flags.apply === "true" && flags["dry-run"] !== "true";
2038
+ var nowUi;
2039
+ try {
2040
+ nowUi = JSON.parse(fs.readFileSync(path.resolve(file), "utf8"));
2041
+ }
2042
+ catch (err) {
2043
+ process.stderr.write("sync-ux-events: cannot read " + file + ": " + (err instanceof Error ? err.message : String(err)) + "\n");
2044
+ return 1;
2045
+ }
2046
+ var result;
2047
+ try {
2048
+ result = await (0, uxEvents_1.syncUxEvents)({
2049
+ client: (0, client_1.createClient)({}),
2050
+ nowUi: nowUi,
2051
+ component: flags.component,
2052
+ updateSetSysId: flags["update-set"],
2053
+ apply: apply,
2054
+ });
2055
+ }
2056
+ catch (err) {
2057
+ var message = err instanceof Error ? err.message : String(err);
2058
+ if (message.indexOf("ux-events: ") === 0)
2059
+ message = message.slice("ux-events: ".length);
2060
+ process.stderr.write("sync-ux-events: " + message + "\n");
2061
+ return 1;
2062
+ }
2063
+ if (flags.json === "true") {
2064
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
2065
+ }
2066
+ else {
2067
+ process.stdout.write((0, uxEvents_1.formatUxEventSync)(result) + "\n");
2068
+ }
2069
+ return result.ok ? 0 : 2;
2070
+ }
1994
2071
  /**
1995
2072
  * dove-sn delete-record:
1996
2073
  * --table x_cadso_core_metric_point_type
1997
2074
  * --sys-id <32-hex sys_id> (the record to delete)
1998
- * --update-set <sys_id> (required — the delete is captured here, never the
1999
- * session default; server honours it once #297 ships)
2075
+ * --update-set <sys_id> (required — must exist and be in progress; pinned as
2076
+ * the session's current set before the delete, and the
2077
+ * DELETE capture row is read back from sys_update_xml)
2000
2078
  * [--apply] (DRY-RUN BY DEFAULT — nothing is deleted without it)
2001
2079
  * [--dry-run] [--json] (--dry-run wins over --apply)
2002
2080
  * Reads the record BEFORE (a missing record is an error, not a no-op delete) and AFTER
2003
2081
  * (success is only reported once the record is confirmed gone).
2004
- * Exit codes: 0 deleted/dry-run, 1 bad args or missing record, 2 delete returned but the
2005
- * record is STILL PRESENT on read-back.
2082
+ * Exit codes: 0 deleted (and captured in the requested set) / dry-run, 1 bad args, missing
2083
+ * record or unknown/closed update set, 2 the record is STILL PRESENT on read-back (including a
2084
+ * delete the server refused with an error), its state is unknown, the update-set pin did not
2085
+ * take (nothing deleted), or it was deleted but the DELETE capture landed in a different set or
2086
+ * could not be read back. A table with no update-set capture at all exits 0 with a note.
2006
2087
  */
2007
2088
  async function runDeleteRecord(flags) {
2008
2089
  var table = flags.table;
@@ -2048,7 +2129,15 @@ async function runDeleteRecord(flags) {
2048
2129
  result.sysId +
2049
2130
  " → update set " +
2050
2131
  result.updateSetSysId +
2132
+ (result.updateSetName ? " (" + result.updateSetName + ")" : "") +
2051
2133
  (result.verified ? " — verified gone" : "") +
2134
+ (result.status === "deleted"
2135
+ ? (result.captureState === "in-set"
2136
+ ? ", capture verified"
2137
+ : result.captureState === "none"
2138
+ ? ", no update-set capture (table not recorded?)"
2139
+ : ", NOT captured in that set")
2140
+ : "") +
2052
2141
  "\n" +
2053
2142
  result.note +
2054
2143
  "\n");
@@ -2058,6 +2147,12 @@ async function runDeleteRecord(flags) {
2058
2147
  }
2059
2148
  if (result.status === "failed")
2060
2149
  return 2;
2150
+ // Gone but the DELETE landed in ANOTHER set (the requested set would promote without it),
2151
+ // or the capture could not be read back: neither may read as success to a script. A table
2152
+ // that is not recorded in update sets at all ("none") is a plain data delete — exit 0, and
2153
+ // the note says the delete will not travel.
2154
+ if (result.status === "deleted" && (result.captureState === "other-set" || result.captureState === "unverified"))
2155
+ return 2;
2061
2156
  return 0;
2062
2157
  }
2063
2158
  /**
@@ -2689,6 +2784,9 @@ async function dispatch(parsed) {
2689
2784
  if (parsed.command === "create-record") {
2690
2785
  return await runCreateRecord(parsed.flags);
2691
2786
  }
2787
+ if (parsed.command === "sync-ux-events") {
2788
+ return await runSyncUxEvents(parsed.flags, parsed.bare);
2789
+ }
2692
2790
  if (parsed.command === "delete-record") {
2693
2791
  return await runDeleteRecord(parsed.flags);
2694
2792
  }
package/dist/cliUsage.js CHANGED
@@ -332,14 +332,15 @@ exports.VERB_USAGE = {
332
332
  ],
333
333
  },
334
334
  "index-create": {
335
- 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",
336
336
  required: [
337
337
  { flag: "table", value: "<name>" },
338
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." },
339
340
  ],
340
341
  optional: [
341
342
  { flag: "unique" },
342
- { 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)." },
343
344
  CONFIRM_FLAG,
344
345
  FORCE_DRY_RUN_FLAG,
345
346
  { flag: "poll-attempts", value: "<n>", note: "Positive integer." },
@@ -349,11 +350,12 @@ exports.VERB_USAGE = {
349
350
  ],
350
351
  gate: "confirm",
351
352
  gateNote: "Nothing is sent OR read without --confirm.",
352
- 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",
353
354
  notes: [
354
- "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.",
355
- "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).",
356
- "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.",
357
359
  ],
358
360
  },
359
361
  "set-field": {
@@ -406,7 +408,7 @@ exports.VERB_USAGE = {
406
408
  {
407
409
  flag: "update-set",
408
410
  value: "<sys_id>",
409
- note: "Sent with the delete; see the #297 note below for where the capture lands today.",
411
+ note: "Must exist and be in progress (checked on the dry-run too); pinned as current before the delete.",
410
412
  },
411
413
  ],
412
414
  optional: [
@@ -418,10 +420,36 @@ exports.VERB_USAGE = {
418
420
  gateNote: "The dry-run prints the record snapshot that would be deleted.",
419
421
  example: "dove-sn delete-record --table x_cadso_core_metric_point_type --sys-id <32-hex sys_id> --update-set <sys_id> --apply",
420
422
  notes: [
421
- "Reads the record BEFORE (a missing record is an error, never a no-op delete) and AFTER (exit 2 if it is still present).",
422
- "Until TenonHQ/Dovetail#297 ships server-side the delete op IGNORES --update-set and captures into the session's current update set — make that the set you want before --apply.",
423
+ "Reads the record BEFORE (a missing record is an error, never a no-op delete) and AFTER (exit 2 if it is still present — including when the server refused the delete with an error).",
424
+ "Until TenonHQ/Dovetail#297 ships server-side the delete op IGNORES --update-set and captures into the session's current update set — so the verb pins --update-set as current first (refusing, nothing deleted, if the pin does not read back) and reads the DELETE row back from sys_update_xml: exit 2 when it landed in a different set or the read-back failed. A table that writes no update-set capture at all exits 0 with a note (captureState \"none\") — that delete will not travel.",
423
425
  ],
424
426
  },
427
+ "sync-ux-events": {
428
+ summary: "Register a UI component's now-ui.json actions as sys_ux_event records on its macroponent",
429
+ required: [
430
+ { flag: "file", value: "<now-ui.json>", note: "Path to the component's now-ui.json; every component with `actions` is synced." },
431
+ ],
432
+ optional: [
433
+ { flag: "component", value: "<tag>", note: "Limit to one component tag, e.g. cadso-journey-builder." },
434
+ {
435
+ flag: "update-set",
436
+ value: "<sys_id>",
437
+ note: "Required with --apply — the event creates and the macroponent edit are captured here.",
438
+ },
439
+ { flag: "apply", note: "Write (without it the run is a dry-run)." },
440
+ { flag: "dry-run", note: "Force a dry-run even with --apply." },
441
+ JSON_FLAG,
442
+ ],
443
+ gate: "apply",
444
+ gateNote: "The dry-run prints each action as ok / create / link / drift / ambiguous, plus orphans.",
445
+ example: "dove-sn sync-ux-events --file path/to/now-ui.json --component cadso-journey-builder --update-set <sys_id> --apply",
446
+ notes: [
447
+ "Macroponent = sys_ux_macroponent whose root_component is the sys_ux_lib_component with that tag (category component). Events are created in the macroponent's scope.",
448
+ "Append-only: missing events are created and their sys_ids appended to dispatched_events; existing entries are never dropped or reordered. Label/description drift and orphaned (linked but undeclared) events are reported, never changed.",
449
+ "Exit codes: 0 in sync / dry-run / applied and verified, 1 bad args or unreadable file, 2 unresolved or ambiguous component/event, or a write that did not verify.",
450
+ ],
451
+ stringFlags: ["file", "component", "update-set"],
452
+ },
425
453
  "host-assets": {
426
454
  summary: "Deploy a built dist/ to ServiceNow (carrier sys_ui_script + attachment + m2m)",
427
455
  required: [
@@ -9,7 +9,8 @@
9
9
  * The INSERT counterpart to `set-field`.
10
10
  *
11
11
  * NOT for schema tables (sys_db_object / sys_dictionary) — that's
12
- * create-table / add-column. To UPDATE an existing record, use `set-field`.
12
+ * create-table / add-column. NOT for sys_update_set — that's
13
+ * `dove createUpdateSet`. To UPDATE an existing record, use `set-field`.
13
14
  */
14
15
  import type { ServiceNowClient } from "./client";
15
16
  import type { RecordWriteResult } from "./setField";
@@ -10,7 +10,8 @@
10
10
  * The INSERT counterpart to `set-field`.
11
11
  *
12
12
  * NOT for schema tables (sys_db_object / sys_dictionary) — that's
13
- * create-table / add-column. To UPDATE an existing record, use `set-field`.
13
+ * create-table / add-column. NOT for sys_update_set — that's
14
+ * `dove createUpdateSet`. To UPDATE an existing record, use `set-field`.
14
15
  */
15
16
  Object.defineProperty(exports, "__esModule", { value: true });
16
17
  exports.createRecord = createRecord;
@@ -19,6 +20,11 @@ const setField_1 = require("./setField");
19
20
  // Same guard as set-field: schema tables are never written as data — routed to
20
21
  // the dedicated schema verbs instead so we never orphan or corrupt metadata.
21
22
  var REFUSED_TABLES = ["sys_db_object", "sys_dictionary"];
23
+ // The generic createRecord op never sets `application`, so an update set
24
+ // inserted through it lands in the session app while the read-back (name only)
25
+ // still reports success. Update sets go through the scope-correct
26
+ // createUpdateSet op instead.
27
+ var UPDATE_SET_TABLE = "sys_update_set";
22
28
  async function createRecord(params) {
23
29
  var client = params.client || (0, client_1.createClient)({});
24
30
  var table = params.table;
@@ -29,6 +35,11 @@ async function createRecord(params) {
29
35
  throw new Error("create-record: refusing to write " + table + " as data — it is a schema table. "
30
36
  + "Use add-column / create-table for schema changes.");
31
37
  }
38
+ if (table === UPDATE_SET_TABLE) {
39
+ throw new Error("create-record: refusing to create a sys_update_set record — the generic createRecord op "
40
+ + "does not set its application, so the set would land in the session app. "
41
+ + "Use `dove createUpdateSet --name <name> --scope <scope>` instead.");
42
+ }
32
43
  var fieldNames = params.fields ? Object.keys(params.fields) : [];
33
44
  if (fieldNames.length === 0) {
34
45
  throw new Error("create-record: at least one field (--fields key=value) is required.");