@tenonhq/dovetail-servicenow 0.0.51 → 0.0.53

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
@@ -349,6 +349,7 @@ npx dove-sn publish-flow --sys-id <sys_id> # scope defaults to the f
349
349
  # Test a flow: validate (default, read-only) or actually run it
350
350
  npx dove-sn test-flow --sys-id <sys_id> --inputs '{"phone":"+1555..."}'
351
351
  npx dove-sn test-flow --sys-id <sys_id> --execute --confirm --inputs '{...}' # runs it
352
+ npx dove-sn test-flow --sys-id <action_sys_id> --action --execute --confirm # runs an action
352
353
 
353
354
  # Edit a flow in place (rename / description / step inputs)
354
355
  echo '{"rename":{"name":"New Name"},"patchStepInputs":[{"step":"Calculate SMS Send At","input":"send_rate","value":"5"}]}' > ops.json
@@ -742,8 +743,16 @@ substring-matched, because `"[owner_id]"` contains `"owner"`. `type` is `access_
742
743
  **Uniqueness is not readable.** `v_db_index` has no uniqueness field, so a unique index
743
744
  and an ordinary one are indistinguishable in it: `unique` is left **absent** rather than
744
745
  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`.
746
+ duplicate-insert test proves enforcement. An empty result for a table that does not exist
747
+ means the table name is wrong - every physical table has a `PRIMARY`.
748
+
749
+ **Table-per-hierarchy children list their storage root's indexes.** `v_db_index` lists
750
+ indexes by *physical* table, and a table stored in an ancestor's physical table (anything
751
+ extending `task`, for example) has no rows under its own name. When the table exists but
752
+ has no rows, its `super_class` chain is walked to the first ancestor that has them, and
753
+ that root's indexes are listed - with `storageTable` and the note naming the root.
754
+ `index-create` and `add-index` **refuse** such a child on the live path, naming the root,
755
+ rather than build on it or report a false "NOT created".
747
756
 
748
757
  ### Create an index (composite and non-unique included)
749
758
 
@@ -792,6 +801,15 @@ was a silent no-op for that reason.)
792
801
  - **Idempotent.** On the live path `v_db_index` is read first, and an index over *exactly*
793
802
  these columns short-circuits to `already-exists` with no pin, no form session and no
794
803
  write. Column **order** is part of an index's identity - `[a;b]` is not `[b;a]`.
804
+ - **Table-per-hierarchy children are refused.** A table with no `v_db_index` rows of its
805
+ own whose ancestor has them is stored in that ancestor's physical table; the run stops
806
+ before any write and names the root (run against the root if that is what you want).
807
+ - **Identity check (warns, never refuses).** The pin runs through the REST client (an API
808
+ key when one is configured) but the build is scheduled by the form session (always
809
+ `SN_USER`), and the build captures into the *form* user's current set. The REST caller's
810
+ own `sys_user` row is read first; when it is not the form-login user (or cannot be read)
811
+ the run still goes ahead and every result note carries an `IDENTITY WARNING`. The capture
812
+ read-back then reports where the row actually landed (`captured` / `captureFoundIn`).
795
813
  - **The pin is read back.** The set is pinned with Dovetail's own `changeUpdateSet` and
796
814
  `currentUpdateSet` is read; a pin that did not take stops the run before the session
797
815
  opens.
@@ -804,7 +822,8 @@ was a silent no-op for that reason.)
804
822
  values (EMPTY counts). Then the capture row is looked for in the pinned set:
805
823
  `captured:true` only when it was read back; an index that exists but was not captured
806
824
  is `created:true, captured:false` with `update-set-capture` in `unverified` - and
807
- exit code 2, because it will not travel.
825
+ exit code 2, because it will not travel. In that case the capture name is searched
826
+ across every set and the set(s) it actually landed in come back in `captureFoundIn`.
808
827
  - **Uniqueness is still never claimed.** `uniqueness-enforced` stays in `unverified` on
809
828
  every status.
810
829
 
@@ -859,7 +878,8 @@ npx dove-sn create-record \
859
878
  switches the executing user's app scope + update set server-side, inserts, and
860
879
  restores both — so the record is owned by the right app and the insert is captured
861
880
  in the right update set. Like `set-field` it **refuses** schema tables and verifies
862
- via read-back. `--scope` and `--update-set` are required; `--if-absent
881
+ via read-back. It also **refuses** `sys_update_set` — the op cannot set an update
882
+ set's application, so create sets with `dove createUpdateSet` instead. `--scope` and `--update-set` are required; `--if-absent
863
883
  "<encoded-query>"` makes re-runs idempotent (the insert is skipped when the query
864
884
  already matches a row). Exit codes: `0` created / skipped-in-sync / dry-run, `1` bad
865
885
  args, `2` write landed unverified (or skipped with drift). To **update** an existing
@@ -916,14 +936,19 @@ npx dove-sn delete-record \
916
936
  `delete-record` wraps the core `deleteRecord` op. It is **dry-run by default** —
917
937
  nothing is deleted without `--apply` (`--dry-run` wins if both are given). `--sys-id`
918
938
  must be a 32-character lowercase hex id and `--table` a plain table name; both are
919
- validated before any network call. `--update-set` is **required** and sent with the
920
- delete, but **the capture is not pinned yet**: until
921
- [#297](https://github.com/TenonHQ/Dovetail/issues/297) ships server-side, the op ignores
922
- `update_set_sys_id` and captures the delete into the session's **current** update set —
923
- make that the set you want before `--apply`. Every result note repeats this caveat; the
924
- 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` /
925
- `sys_dictionary`). Exit codes: `0` deleted / dry-run, `1` bad args or no such record,
926
- `2` the delete returned but the record is **still present** on read-back.
939
+ validated before any network call. `--update-set` is **required**, must exist and be
940
+ **in progress** (checked on the dry-run too). Until
941
+ [#297](https://github.com/TenonHQ/Dovetail/issues/297) ships server-side the op ignores
942
+ `update_set_sys_id` and captures the delete into the session's **current** update set, so
943
+ the verb pins `--update-set` as current first (refusing, nothing deleted, if the pin does
944
+ not read back) and then reads the DELETE row back from `sys_update_xml`. The result carries
945
+ `captured`, `capturedInto` and `captureState` (`in-set` / `other-set` / `none` /
946
+ `unverified`). Like its siblings it **refuses** schema tables (`sys_db_object` /
947
+ `sys_dictionary`). A failed delete call never skips the read-back. Exit codes: `0` deleted
948
+ and captured in the set, a table that writes no capture at all (`none`, with a note), or a
949
+ dry-run; `1` bad args, no such record, or an unknown/closed update set; `2` the record is
950
+ **still present** on read-back (including a server-refused delete), its state is unknown,
951
+ the pin did not take, or the DELETE landed in a different set / could not be read back.
927
952
 
928
953
  All three verbs are exported for programmatic use:
929
954
 
@@ -941,6 +966,36 @@ var r = await setField({
941
966
  console.log(r.status, r.verified); // "applied" true
942
967
  ```
943
968
 
969
+ ### Register UI component events (sync-ux-events)
970
+
971
+ UI Builder can only map an event a component dispatches when it exists as a
972
+ `sys_ux_event` record **and** is listed in the component macroponent's
973
+ `dispatched_events`. A component deploy does not reliably create either, so
974
+ `sync-ux-events` reads the component's `now-ui.json` and reconciles every
975
+ `components.<tag>.actions[]` entry against the instance.
976
+
977
+ ```bash
978
+ # Dry-run (the default) — per action: ok / create / link / drift / ambiguous, plus orphans
979
+ npx dove-sn sync-ux-events --file path/to/now-ui.json
980
+
981
+ # Apply — creates the missing events and appends them to dispatched_events
982
+ npx dove-sn sync-ux-events --file path/to/now-ui.json \
983
+ --component cadso-journey-builder --update-set <sys_id> --apply
984
+ ```
985
+
986
+ The macroponent is the `sys_ux_macroponent` (category `component`) whose
987
+ `root_component` is the `sys_ux_lib_component` with that tag; events are created in
988
+ the macroponent's scope via the scope-aware `createRecord` op and the list edit goes
989
+ through `pushWithUpdateSet`, both captured into `--update-set`. The write is
990
+ **append-only** — existing `dispatched_events` entries are never dropped or reordered —
991
+ and is re-read to verify. Label/description **drift** and **orphans** (linked but no
992
+ longer declared) are reported, never changed; an event name carried by several records
993
+ is **ambiguous** and never written. Event names and tags are charset-validated before
994
+ they reach a query. Re-runs are no-ops. Exit codes: `0` in sync / dry-run / applied and
995
+ verified; `1` bad args or unreadable file; `2` an unresolved or ambiguous
996
+ component/event, or a write that did not verify. MCP: `sync_ux_events` (dry-run unless
997
+ `confirm:true`).
998
+
944
999
  ### Invoke an arbitrary REST operation
945
1000
 
946
1001
  Invoke any authenticated ServiceNow REST operation — an application's own
@@ -1106,7 +1161,14 @@ failed/timeout. Programmatic: `exportUpdateSet({ updateSet, mode })`,
1106
1161
 
1107
1162
  `test-flow` defaults to **validate** — a safe pre-flight (published? inputs match
1108
1163
  declared variables?) that never runs the flow; `--execute --confirm` runs it via
1109
- the server-side FlowAPI runner (deploy `resources/runFlow.md` first).
1164
+ the Dovetail Core `runFlow` op (`POST /api/cadso/dovetail_core/runFlow`), which
1165
+ ships with the Dovetail app — nothing to deploy. On a route-level 404 it falls
1166
+ back once to the legacy `/api/cadso/dovetail/runFlow`; `--runner <path>` is used
1167
+ as-is with no fallback. Add `--action` when `--sys-id` is a
1168
+ `sys_hub_action_type_definition` (sent as `actionSysId`). The caller needs the
1169
+ `admin` or `dovetail_user` role; a rejected run (400/403/404/422/500 with the
1170
+ op's `{ ok: false, error }`) reports `ok=false` and exits 2. Contract and source:
1171
+ [`resources/runFlow.md`](resources/runFlow.md).
1110
1172
 
1111
1173
  `edit-flow` defaults to a **dry-run** diff. With `--apply`: rename/description are
1112
1174
  written to `sys_hub_flow` through the update-set-aware API (so `--update-set` is
@@ -1198,8 +1260,8 @@ existing record), `create_record` (insert one record) and `delete_record` (delet
1198
1260
  record — dry-run by default, `confirm:true` to apply, `updateSetSysId` required, the
1199
1261
  record read back before AND after so success is only reported once it is confirmed
1200
1262
  gone) — all read-back-verified; `set_field` / `create_record` are captured in the
1201
- update set you pass, while `delete_record` captures into the session's current set
1202
- until [#297](https://github.com/TenonHQ/Dovetail/issues/297) ships — `host_assets` (deploy a built
1263
+ update set you pass, while `delete_record` pins the set as current and reads the DELETE
1264
+ capture back (`captureState`) until [#297](https://github.com/TenonHQ/Dovetail/issues/297) ships — `host_assets` (deploy a built
1203
1265
  dist/), plus the Flow Designer
1204
1266
  tools `flow_view` (read a flow/subflow's step graph), `action_view` (read an action
1205
1267
  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
@@ -663,15 +664,18 @@ async function runCreateFlow(flags) {
663
664
  }
664
665
  /**
665
666
  * dove-sn test-flow:
666
- * --sys-id <sys_id> Required. sys_hub_flow sys_id (flow or subflow).
667
+ * --sys-id <sys_id> Required. sys_hub_flow sys_id (flow or subflow), or a
668
+ * sys_hub_action_type_definition sys_id with --action.
669
+ * --action Optional. --sys-id is an action (sent as actionSysId).
667
670
  * --execute Optional. Actually run it (default is validate-only).
668
671
  * --confirm Required with --execute. A deliberate run-for-real gate.
669
672
  * --inputs <json> Optional. JSON object of inputs (or --inputs-json <path>).
670
673
  * --json Optional. Emit the structured TestFlowResult.
671
674
  *
672
675
  * Default (no --execute) is a safe pre-flight: published? readable? inputs match
673
- * declared variables? --execute POSTs the FlowAPI runner endpoint (see
674
- * resources/runFlow.md). Executing a flow can cause real side effects.
676
+ * declared variables? --execute POSTs the Dovetail Core runFlow op
677
+ * (/api/cadso/dovetail_core/runFlow, legacy fallback on 404; --runner overrides
678
+ * with no fallback — see resources/runFlow.md). Executing can cause real side effects.
675
679
  */
676
680
  async function runTestFlow(flags) {
677
681
  var sysId = flags["sys-id"] || flags.sysId;
@@ -689,6 +693,7 @@ async function runTestFlow(flags) {
689
693
  var params = {
690
694
  client: (0, client_1.createClient)({}),
691
695
  sysId: sysId,
696
+ target: flags.action === "true" ? "action" : "flow",
692
697
  mode: flags.execute === "true" ? "execute" : "validate",
693
698
  inputs: inputs,
694
699
  confirm: flags.confirm === "true",
@@ -1572,7 +1577,14 @@ async function runIndexList(flags) {
1572
1577
  process.stdout.write(JSON.stringify(result, null, 2) + "\n");
1573
1578
  return 0;
1574
1579
  }
1575
- process.stdout.write(result.table + " — " + result.indexes.length + " index(es)\n");
1580
+ process.stdout.write(result.table +
1581
+ " — " +
1582
+ result.indexes.length +
1583
+ " index(es)" +
1584
+ (result.storageTable && result.storageTable !== result.table
1585
+ ? " (stored in " + result.storageTable + "'s physical table — these are its indexes)"
1586
+ : "") +
1587
+ "\n");
1576
1588
  for (var i = 0; i < result.indexes.length; i += 1) {
1577
1589
  var idx = result.indexes[i];
1578
1590
  process.stdout.write(" " +
@@ -2006,18 +2018,76 @@ async function runCreateRecord(flags) {
2006
2018
  return 2;
2007
2019
  return 0;
2008
2020
  }
2021
+ /**
2022
+ * dove-sn sync-ux-events:
2023
+ * --file <path/to/now-ui.json>
2024
+ * [--component <tag>]
2025
+ * [--update-set <sys_id>] (required with --apply)
2026
+ * [--apply] [--dry-run] [--json]
2027
+ * DRY-RUN BY DEFAULT. Exit codes: 0 in sync / dry-run / applied+verified, 1 bad args or
2028
+ * unreadable file, 2 unresolved/ambiguous component or event, or an unverified write.
2029
+ */
2030
+ async function runSyncUxEvents(flags, bare) {
2031
+ var bareErr = bareStringFlagError("sync-ux-events", bare);
2032
+ if (bareErr) {
2033
+ process.stderr.write(bareErr);
2034
+ return 1;
2035
+ }
2036
+ var file = flags.file;
2037
+ if (!file) {
2038
+ process.stderr.write("sync-ux-events: --file <now-ui.json> is required\n");
2039
+ return 1;
2040
+ }
2041
+ var apply = flags.apply === "true" && flags["dry-run"] !== "true";
2042
+ var nowUi;
2043
+ try {
2044
+ nowUi = JSON.parse(fs.readFileSync(path.resolve(file), "utf8"));
2045
+ }
2046
+ catch (err) {
2047
+ process.stderr.write("sync-ux-events: cannot read " + file + ": " + (err instanceof Error ? err.message : String(err)) + "\n");
2048
+ return 1;
2049
+ }
2050
+ var result;
2051
+ try {
2052
+ result = await (0, uxEvents_1.syncUxEvents)({
2053
+ client: (0, client_1.createClient)({}),
2054
+ nowUi: nowUi,
2055
+ component: flags.component,
2056
+ updateSetSysId: flags["update-set"],
2057
+ apply: apply,
2058
+ });
2059
+ }
2060
+ catch (err) {
2061
+ var message = err instanceof Error ? err.message : String(err);
2062
+ if (message.indexOf("ux-events: ") === 0)
2063
+ message = message.slice("ux-events: ".length);
2064
+ process.stderr.write("sync-ux-events: " + message + "\n");
2065
+ return 1;
2066
+ }
2067
+ if (flags.json === "true") {
2068
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
2069
+ }
2070
+ else {
2071
+ process.stdout.write((0, uxEvents_1.formatUxEventSync)(result) + "\n");
2072
+ }
2073
+ return result.ok ? 0 : 2;
2074
+ }
2009
2075
  /**
2010
2076
  * dove-sn delete-record:
2011
2077
  * --table x_cadso_core_metric_point_type
2012
2078
  * --sys-id <32-hex sys_id> (the record to delete)
2013
- * --update-set <sys_id> (required — the delete is captured here, never the
2014
- * session default; server honours it once #297 ships)
2079
+ * --update-set <sys_id> (required — must exist and be in progress; pinned as
2080
+ * the session's current set before the delete, and the
2081
+ * DELETE capture row is read back from sys_update_xml)
2015
2082
  * [--apply] (DRY-RUN BY DEFAULT — nothing is deleted without it)
2016
2083
  * [--dry-run] [--json] (--dry-run wins over --apply)
2017
2084
  * Reads the record BEFORE (a missing record is an error, not a no-op delete) and AFTER
2018
2085
  * (success is only reported once the record is confirmed gone).
2019
- * Exit codes: 0 deleted/dry-run, 1 bad args or missing record, 2 delete returned but the
2020
- * record is STILL PRESENT on read-back.
2086
+ * Exit codes: 0 deleted (and captured in the requested set) / dry-run, 1 bad args, missing
2087
+ * record or unknown/closed update set, 2 the record is STILL PRESENT on read-back (including a
2088
+ * delete the server refused with an error), its state is unknown, the update-set pin did not
2089
+ * take (nothing deleted), or it was deleted but the DELETE capture landed in a different set or
2090
+ * could not be read back. A table with no update-set capture at all exits 0 with a note.
2021
2091
  */
2022
2092
  async function runDeleteRecord(flags) {
2023
2093
  var table = flags.table;
@@ -2063,7 +2133,15 @@ async function runDeleteRecord(flags) {
2063
2133
  result.sysId +
2064
2134
  " → update set " +
2065
2135
  result.updateSetSysId +
2136
+ (result.updateSetName ? " (" + result.updateSetName + ")" : "") +
2066
2137
  (result.verified ? " — verified gone" : "") +
2138
+ (result.status === "deleted"
2139
+ ? (result.captureState === "in-set"
2140
+ ? ", capture verified"
2141
+ : result.captureState === "none"
2142
+ ? ", no update-set capture (table not recorded?)"
2143
+ : ", NOT captured in that set")
2144
+ : "") +
2067
2145
  "\n" +
2068
2146
  result.note +
2069
2147
  "\n");
@@ -2073,6 +2151,12 @@ async function runDeleteRecord(flags) {
2073
2151
  }
2074
2152
  if (result.status === "failed")
2075
2153
  return 2;
2154
+ // Gone but the DELETE landed in ANOTHER set (the requested set would promote without it),
2155
+ // or the capture could not be read back: neither may read as success to a script. A table
2156
+ // that is not recorded in update sets at all ("none") is a plain data delete — exit 0, and
2157
+ // the note says the delete will not travel.
2158
+ if (result.status === "deleted" && (result.captureState === "other-set" || result.captureState === "unverified"))
2159
+ return 2;
2076
2160
  return 0;
2077
2161
  }
2078
2162
  /**
@@ -2704,6 +2788,9 @@ async function dispatch(parsed) {
2704
2788
  if (parsed.command === "create-record") {
2705
2789
  return await runCreateRecord(parsed.flags);
2706
2790
  }
2791
+ if (parsed.command === "sync-ux-events") {
2792
+ return await runSyncUxEvents(parsed.flags, parsed.bare);
2793
+ }
2707
2794
  if (parsed.command === "delete-record") {
2708
2795
  return await runDeleteRecord(parsed.flags);
2709
2796
  }
package/dist/cliUsage.js CHANGED
@@ -408,7 +408,7 @@ exports.VERB_USAGE = {
408
408
  {
409
409
  flag: "update-set",
410
410
  value: "<sys_id>",
411
- 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.",
412
412
  },
413
413
  ],
414
414
  optional: [
@@ -420,10 +420,36 @@ exports.VERB_USAGE = {
420
420
  gateNote: "The dry-run prints the record snapshot that would be deleted.",
421
421
  example: "dove-sn delete-record --table x_cadso_core_metric_point_type --sys-id <32-hex sys_id> --update-set <sys_id> --apply",
422
422
  notes: [
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).",
424
- "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.",
425
425
  ],
426
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
+ },
427
453
  "host-assets": {
428
454
  summary: "Deploy a built dist/ to ServiceNow (carrier sys_ui_script + attachment + m2m)",
429
455
  required: [
@@ -568,14 +594,15 @@ exports.VERB_USAGE = {
568
594
  notes: ["Exit 2 when the flow was created but the snapshot did not compile (not published)."],
569
595
  },
570
596
  "test-flow": {
571
- summary: "Validate (default) or run a flow/subflow",
572
- required: [{ flag: "sys-id", value: "<sys_id>", note: "sys_hub_flow sys_id (flow or subflow)." }],
597
+ summary: "Validate (default) or run a flow/subflow/action",
598
+ required: [{ flag: "sys-id", value: "<sys_id>", note: "sys_hub_flow sys_id (flow or subflow), or an action's with --action." }],
573
599
  optional: [
600
+ { flag: "action", note: "--sys-id is a sys_hub_action_type_definition (sent as actionSysId)." },
574
601
  { flag: "execute", note: "Actually run it (default is validate-only)." },
575
602
  { flag: "confirm", note: "Required with --execute — the deliberate run-for-real gate." },
576
603
  { flag: "inputs", value: "'<json>'", note: "JSON object of flow inputs." },
577
604
  { flag: "inputs-json", value: "<path>", note: "Same, from a file." },
578
- { flag: "runner", value: "<path>", note: "Override the FlowAPI runner endpoint path." },
605
+ { flag: "runner", value: "<path>", note: "Override the runner path (default /api/cadso/dovetail_core/runFlow; no legacy fallback when set)." },
579
606
  JSON_FLAG,
580
607
  ],
581
608
  gate: "execute-confirm",
package/dist/client.d.ts CHANGED
@@ -231,4 +231,15 @@ export interface ServiceNowClient {
231
231
  }) => Promise<void>;
232
232
  };
233
233
  }
234
+ /**
235
+ * Dovetail core Scripted REST API base (ships in the Dovetail app). Every
236
+ * Dovetail server op is tried here first.
237
+ */
238
+ export declare var DOVETAIL_CORE_API_BASE: string;
239
+ /**
240
+ * Legacy global-scope Dovetail Scripted REST API base. Clients fall back to it
241
+ * once, on a 404 from DOVETAIL_CORE_API_BASE, for instances that predate the
242
+ * Dovetail app.
243
+ */
244
+ export declare var DOVETAIL_LEGACY_API_BASE: string;
234
245
  export declare function createClient(config?: ServiceNowClientConfig): ServiceNowClient;
package/dist/client.js CHANGED
@@ -24,7 +24,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
24
24
  return (mod && mod.__esModule) ? mod : { "default": mod };
25
25
  };
26
26
  Object.defineProperty(exports, "__esModule", { value: true });
27
- exports.PROCESSFLOW_PATH_PREFIX = void 0;
27
+ exports.DOVETAIL_LEGACY_API_BASE = exports.DOVETAIL_CORE_API_BASE = exports.PROCESSFLOW_PATH_PREFIX = void 0;
28
28
  exports.resolveFlowAuth = resolveFlowAuth;
29
29
  exports.isProcessflowPath = isProcessflowPath;
30
30
  exports.createClient = createClient;
@@ -134,6 +134,17 @@ function sleep(ms) {
134
134
  setTimeout(resolve, ms);
135
135
  });
136
136
  }
137
+ /**
138
+ * Dovetail core Scripted REST API base (ships in the Dovetail app). Every
139
+ * Dovetail server op is tried here first.
140
+ */
141
+ exports.DOVETAIL_CORE_API_BASE = "/api/cadso/dovetail_core/";
142
+ /**
143
+ * Legacy global-scope Dovetail Scripted REST API base. Clients fall back to it
144
+ * once, on a 404 from DOVETAIL_CORE_API_BASE, for instances that predate the
145
+ * Dovetail app.
146
+ */
147
+ exports.DOVETAIL_LEGACY_API_BASE = "/api/cadso/dovetail/";
137
148
  /**
138
149
  * Match a thrown error message against the 403/404 patterns produced by request().
139
150
  * buildAgent.* uses this to decide when to fall back to the plain Table API.
@@ -303,8 +314,8 @@ function createClient(config = {}) {
303
314
  // fall back to the legacy /api/cadso/dovetail/<op> on 404 (one-time warning).
304
315
  async function dovetailRequest(method, op, body, params, ctx) {
305
316
  var url = useDovetailLegacyPath
306
- ? "/api/cadso/dovetail/" + op
307
- : "/api/cadso/dovetail_core/" + op;
317
+ ? exports.DOVETAIL_LEGACY_API_BASE + op
318
+ : exports.DOVETAIL_CORE_API_BASE + op;
308
319
  try {
309
320
  return await request({ method: method, url: url, data: body, params: params }, ctx);
310
321
  }
@@ -312,11 +323,11 @@ function createClient(config = {}) {
312
323
  var msg = e && e.message ? String(e.message) : "";
313
324
  if (!useDovetailLegacyPath && msg.indexOf("SN 404 on") === 0) {
314
325
  // eslint-disable-next-line no-console
315
- console.warn("[deprecation] /api/cadso/dovetail_core/" + op +
316
- " returned 404. Falling back to legacy /api/cadso/dovetail/" + op +
326
+ console.warn("[deprecation] " + exports.DOVETAIL_CORE_API_BASE + op +
327
+ " returned 404. Falling back to legacy " + exports.DOVETAIL_LEGACY_API_BASE + op +
317
328
  ". Install the Dovetail application's Scripted REST APIs to silence this warning.");
318
329
  useDovetailLegacyPath = true;
319
- var legacyUrl = "/api/cadso/dovetail/" + op;
330
+ var legacyUrl = exports.DOVETAIL_LEGACY_API_BASE + op;
320
331
  return await request({ method: method, url: legacyUrl, data: body, params: params }, ctx);
321
332
  }
322
333
  throw e;
@@ -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.");
@@ -7,12 +7,15 @@
7
7
  *
8
8
  * Wraps the Dovetail core Scripted REST `deleteRecord` op. DRY-RUN BY DEFAULT:
9
9
  * without confirm:true nothing is deleted; dryRun:true forces a dry-run even
10
- * with confirm. updateSetSysId is REQUIRED and sent with the delete, but the
11
- * capture is NOT pinned yet: until TenonHQ/Dovetail#297 ships server-side the
12
- * op ignores update_set_sys_id and captures into the session's current update
13
- * set. Every result note says so, so no caller reads "pinned" into a delete
14
- * that was not. The client keeps sending the field so it takes effect the
15
- * moment the server honours it.
10
+ * with confirm. updateSetSysId is REQUIRED and resolved up front (it must exist
11
+ * and be in progress — checked on the dry-run too). Until TenonHQ/Dovetail#297
12
+ * ships the server op ignores update_set_sys_id and captures into the session's
13
+ * current update set, so the verb does what every other deleteRecord caller does:
14
+ * it pins the set as current (changeUpdateSet) and reads the pin back, refusing
15
+ * to delete when it did not take. After the delete it reads the DELETE row back
16
+ * from sys_update_xml and reports where it actually landed (captured /
17
+ * capturedInto) — the pin alone is not proof (#297 saw it not stick cross-scope).
18
+ * The client keeps sending the field so it takes effect once the server honours it.
16
19
  *
17
20
  * NOT for schema tables (sys_db_object / sys_dictionary) — dropping a table or
18
21
  * column is a privileged lifecycle op, not a data delete.
@@ -24,9 +27,9 @@ export interface DeleteRecordParams {
24
27
  /** sys_id of the record to delete — 32 lowercase hex characters. */
25
28
  sysId: string;
26
29
  /**
27
- * Update set the delete should be captured into. Required and sent, but honoured only
28
- * once TenonHQ/Dovetail#297 ships — until then the capture lands in the session's
29
- * current update set (see UPDATE_SET_CAVEAT).
30
+ * Update set the delete should be captured into. Required; must exist and be in progress.
31
+ * Pinned as the session's current set before the delete (the server op ignores the field
32
+ * until TenonHQ/Dovetail#297 ships) and verified via the sys_update_xml capture row.
30
33
  */
31
34
  updateSetSysId?: string;
32
35
  /** The delete gate: the record is only deleted when confirm is exactly true. */
@@ -43,14 +46,34 @@ export interface DeleteRecordResult {
43
46
  before: Record<string, string>;
44
47
  /** True only when the post-delete read-back confirmed the record is gone. */
45
48
  verified: boolean;
49
+ /** Name of the requested update set, resolved before anything else is done. */
50
+ updateSetName: string;
51
+ /**
52
+ * True only when the DELETE's sys_update_xml capture row was read back IN the requested
53
+ * update set. A deleted record with captured:false will NOT travel with that set.
54
+ */
55
+ captured: boolean;
56
+ /** The update set the DELETE capture row actually landed in; null when none was found. */
57
+ capturedInto: {
58
+ sysId: string;
59
+ name: string;
60
+ } | null;
61
+ /**
62
+ * Where the DELETE capture stands: "in-set" (captured in the requested set), "other-set"
63
+ * (captured, but into a different set — the requested set promotes without it),
64
+ * "none" (no capture row anywhere — the table is likely not recorded in update sets),
65
+ * "unverified" (the capture read-back failed), or "n/a" (dry-run / nothing deleted).
66
+ */
67
+ captureState: "in-set" | "other-set" | "none" | "unverified" | "n/a";
46
68
  note: string;
47
69
  }
48
70
  export declare var TABLE_NAME_PATTERN: RegExp;
49
71
  export declare var SYS_ID_PATTERN: RegExp;
50
72
  /**
51
- * Appended to every result note while TenonHQ/Dovetail#297 is open: the server op does not
52
- * yet honour update_set_sys_id, so the capture lands in the session's current update set.
53
- * Delete this (and its test) when #297 ships.
73
+ * Appended to the dry-run and deleted notes while TenonHQ/Dovetail#297 is open: the server op
74
+ * does not yet honour update_set_sys_id, so the delete is captured into the session's current
75
+ * update set — which is why the verb pins it first and reads the capture row back. Revisit
76
+ * (and its test) when #297 ships.
54
77
  */
55
78
  export declare var UPDATE_SET_CAVEAT: string;
56
79
  export declare function validateDeleteTable(table: unknown): string;