@tenonhq/dovetail-servicenow 0.0.31 → 0.0.33

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/dist/cli.js CHANGED
@@ -895,10 +895,19 @@ function printHelp() {
895
895
  " Shorten or clear those values first, then re-run.\n" +
896
896
  " --element / --internal-type are REFUSED with an explanation:\n" +
897
897
  " ServiceNow silently ignores both on an existing column.\n" +
898
+ " set-table Update an EXISTING TABLE's own dictionary row (the collection row),\n" +
899
+ " into an update set, then verify against the instance\n" +
900
+ " (--table <t> --update-set <sys_id> [--audit true|false]\n" +
901
+ " [--dry-run] [--json])\n" +
902
+ " --audit turns RECORD AUDITING on/off for the whole table: with it\n" +
903
+ " true ServiceNow writes a sys_audit row per changed field on every\n" +
904
+ " insert and update — a real cost on a high-write table.\n" +
905
+ " Column attributes belong to set-column, record values to set-field.\n" +
898
906
  " invoke-rest Invoke an arbitrary authenticated REST operation (Scripted REST incl.)\n" +
899
907
  " DRY-RUN BY DEFAULT — nothing is sent without --confirm\n" +
900
908
  " (--method <GET|POST|PUT|DELETE> --path /api/<scope>/<service>/<resource>\n" +
901
- " [--body '<json>' | --body-json <path>] [--confirm] [--dry-run] [--json])\n" +
909
+ " [--body '<json>' | --body-json <path>] [--confirm] [--dry-run] [--json]\n" +
910
+ " [--out <file> full JSON result to a file (atomic; overwrites); for large bodies])\n" +
902
911
  " set-field Set field value(s) on an EXISTING record, into an update set, then verify\n" +
903
912
  " (--table <t> --sys-id <id>|--query <q> --update-set <sys_id>\n" +
904
913
  ' (--fields "k=v,k2=v2" | --from-json <path>) [--dry-run] [--json])\n' +
@@ -1118,12 +1127,12 @@ async function runAddColumn(flags) {
1118
1127
  }
1119
1128
  /** Parse a CLI boolean flag. Bare `--mandatory` means true; `--mandatory false` means
1120
1129
  * false. Anything else is rejected rather than quietly coerced to `true`. */
1121
- function parseBoolFlag(name, raw) {
1130
+ function parseBoolFlag(name, raw, verb = "set-column") {
1122
1131
  if (raw === "true")
1123
1132
  return true;
1124
1133
  if (raw === "false")
1125
1134
  return false;
1126
- throw new Error("set-column: --" + name + " must be true or false (got '" + raw + "').");
1135
+ throw new Error(verb + ": --" + name + " must be true or false (got '" + raw + "').");
1127
1136
  }
1128
1137
  /**
1129
1138
  * dove-sn set-column:
@@ -1221,6 +1230,71 @@ async function runSetColumn(flags, bare) {
1221
1230
  return 2;
1222
1231
  return 0;
1223
1232
  }
1233
+ /**
1234
+ * dove-sn set-table:
1235
+ * --table x_cadso_core_setting --audit true --update-set <sys_id>
1236
+ * [--dry-run] [--json]
1237
+ *
1238
+ * Updates the TABLE's own dictionary row (the `internal_type=collection` row, whose
1239
+ * `element` is empty) — not a column's. Column attributes belong to set-column; a
1240
+ * record's values belong to set-field.
1241
+ */
1242
+ async function runSetTable(flags, bare) {
1243
+ // Guard the string flags AND the updateSetSysId alias: a value-less string flag
1244
+ // arrives as the literal "true", so --update-set (or its alias) with nothing after
1245
+ // it would silently become the sys_id "true" and later fail as "not found".
1246
+ var stringFlags = ["table", "update-set", "updateSetSysId"];
1247
+ for (var f = 0; f < stringFlags.length; f += 1) {
1248
+ if (bare[stringFlags[f]]) {
1249
+ process.stderr.write("set-table: --" + stringFlags[f] + " needs a value (it was given none).\n");
1250
+ return 1;
1251
+ }
1252
+ }
1253
+ var table = flags.table;
1254
+ if (!table) {
1255
+ process.stderr.write("set-table: --table is required " +
1256
+ "(--update-set is required too, unless --dry-run)\n");
1257
+ return 1;
1258
+ }
1259
+ var attributes = {};
1260
+ if (flags.audit !== undefined) {
1261
+ attributes.audit = parseBoolFlag("audit", flags.audit, "set-table");
1262
+ }
1263
+ var params = {
1264
+ client: (0, client_1.createClient)({}),
1265
+ table: table,
1266
+ attributes: attributes,
1267
+ };
1268
+ var setTableUs = flags["update-set"] || flags.updateSetSysId;
1269
+ if (setTableUs)
1270
+ params.updateSetSysId = setTableUs;
1271
+ if (flags["dry-run"] === "true")
1272
+ params.dryRun = true;
1273
+ var result = await (0, table_1.setTable)(params);
1274
+ if (flags.json === "true") {
1275
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
1276
+ }
1277
+ else {
1278
+ process.stdout.write("[" +
1279
+ result.status +
1280
+ "] " +
1281
+ result.table +
1282
+ (result.verified && result.status === "applied" ? " — verified" : "") +
1283
+ (result.status === "applied" && !result.capturedInUpdateSet
1284
+ ? " — NOT CAPTURED"
1285
+ : "") +
1286
+ "\n" +
1287
+ result.note +
1288
+ "\n");
1289
+ }
1290
+ // 2 = the write landed but the instance does not reflect it (or it was not
1291
+ // captured), which must not read as success to a script.
1292
+ if (result.status === "failed")
1293
+ return 2;
1294
+ if (result.status === "applied" && !result.capturedInUpdateSet)
1295
+ return 2;
1296
+ return 0;
1297
+ }
1224
1298
  /** Parse inline `--fields "k=v, k2=v2"` into a field map. */
1225
1299
  function parseFieldsInline(input) {
1226
1300
  var out = {};
@@ -1443,6 +1517,11 @@ async function runHostAssets(flags) {
1443
1517
  * --confirm Send for real. WITHOUT it the command is a DRY-RUN.
1444
1518
  * --dry-run Force a dry-run even with --confirm.
1445
1519
  * --json Emit the structured InvokeRestResult.
1520
+ * --out <file> Also write the full structured result to a file
1521
+ * (pretty JSON, atomic temp+rename, OVERWRITES an
1522
+ * existing file; parent dir must exist). The reliable
1523
+ * channel for large response bodies - piped stdout is
1524
+ * flush-guarded but a file needs no downstream reader.
1446
1525
  *
1447
1526
  * Invoke an arbitrary authenticated REST operation (Scripted REST included).
1448
1527
  * Dry-run by default; --confirm sends and returns { httpStatus, ok, body } with
@@ -1489,6 +1568,17 @@ async function runInvokeRest(flags) {
1489
1568
  params.body = body;
1490
1569
  }
1491
1570
  var result = await (0, invokeRest_1.invokeRest)(params);
1571
+ if (flags.out) {
1572
+ try {
1573
+ (0, invokeRest_1.writeInvokeRestResultFile)(flags.out, result);
1574
+ }
1575
+ catch (err) {
1576
+ process.stderr.write("invoke-rest: --out write failed: " +
1577
+ (err instanceof Error ? err.message : String(err)) +
1578
+ "\n");
1579
+ return 1;
1580
+ }
1581
+ }
1492
1582
  if (flags.json === "true") {
1493
1583
  process.stdout.write(JSON.stringify(result, null, 2) + "\n");
1494
1584
  }
@@ -1659,6 +1749,9 @@ async function main() {
1659
1749
  if (parsed.command === "set-column") {
1660
1750
  return await runSetColumn(parsed.flags, parsed.bare);
1661
1751
  }
1752
+ if (parsed.command === "set-table") {
1753
+ return await runSetTable(parsed.flags, parsed.bare);
1754
+ }
1662
1755
  if (parsed.command === "set-field") {
1663
1756
  return await runSetField(parsed.flags);
1664
1757
  }
@@ -1707,13 +1800,57 @@ async function main() {
1707
1800
  }
1708
1801
  throw new Error("Unknown command: " + parsed.command);
1709
1802
  }
1803
+ // A closed downstream pipe (e.g. `dove-sn ... --json | head`) surfaces as an
1804
+ // EPIPE on stdout. Exit quietly with the code already set instead of crashing
1805
+ // with an unhandled stream error.
1806
+ process.stdout.on("error", function (err) {
1807
+ if (err && err.code === "EPIPE") {
1808
+ process.exit(typeof process.exitCode === "number" ? process.exitCode : 0);
1809
+ }
1810
+ throw err;
1811
+ });
1812
+ /**
1813
+ * process.exit() discards buffered stdout/stderr - when stdout is a PIPE,
1814
+ * anything past the OS pipe buffer (~64KB) is silently dropped, which is how
1815
+ * `invoke-rest --json` used to truncate large bodies mid-string. Queue an
1816
+ * empty chunk behind any pending data on each stream and exit only when both
1817
+ * callbacks confirm the flush. The barrier is queued UNCONDITIONALLY (not
1818
+ * gated on writableLength) so the exit never races stream internals about
1819
+ * whether a prior write is still in flight.
1820
+ */
1821
+ function exitAfterFlush(code) {
1822
+ process.exitCode = code;
1823
+ var pending = 0;
1824
+ var finish = function () {
1825
+ pending -= 1;
1826
+ if (pending <= 0) {
1827
+ process.exit(code);
1828
+ }
1829
+ };
1830
+ [process.stdout, process.stderr].forEach(function (stream) {
1831
+ if (stream.destroyed || !stream.writable) {
1832
+ return;
1833
+ }
1834
+ pending += 1;
1835
+ try {
1836
+ stream.write("", finish);
1837
+ }
1838
+ catch (writeErr) {
1839
+ // A stream that rejects the barrier write has nothing left to flush.
1840
+ pending -= 1;
1841
+ }
1842
+ });
1843
+ if (pending === 0) {
1844
+ process.exit(code);
1845
+ }
1846
+ }
1710
1847
  main()
1711
1848
  .then(function (code) {
1712
- process.exit(code);
1849
+ exitAfterFlush(code);
1713
1850
  })
1714
1851
  .catch(function (err) {
1715
1852
  process.stderr.write("dove-sn error: " +
1716
1853
  (err && err.message ? err.message : String(err)) +
1717
1854
  "\n");
1718
- process.exit(1);
1855
+ exitAfterFlush(1);
1719
1856
  });
package/dist/index.d.ts CHANGED
@@ -19,8 +19,8 @@ export { sincPlugin } from "./plugin";
19
19
  export { listTemplates, verifyArtifact, cloneSubflow, cloneActionType, triggerPublication, publishActionType, editActionType, applyStepOps, verifySteps, summarizeSteps, formatStepPill, readFlow, readActionType, publishFlow, copyFlow, createFlow, buildPublishModel, editFlow, testFlow, DEFAULT_RUN_FLOW_PATH, generateSysId, topoSort, executeWritePlan, WriteOrderError, } from "./flowDesigner";
20
20
  export type { TemplateRef, ListTemplatesParams, FlowKind, VerifyExpect, VerifyFound, VerifyFailure, VerifyReport, VerifyArtifactParams, CloneSubflowParams, CloneSubflowResult, CloneActionTypeParams, CloneActionTypeResult, TriggerPublicationParams, TriggerPublicationResult, PublishActionTypeParams, PublishActionTypeResult, EditActionTypeParams, EditActionTypeResult, EditActionTypeOps, StepOps, StepRecord, StepSummary, StepIoSummary, PatchStepScriptOp, 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";
21
21
  export type { ServiceNowClientConfig, ChoiceValue, ChoiceType, AddChoicesParams, AddChoicesResult, ChoiceActionResult, DictionaryRecord, UpdateSetRecord, LayoutAction, LayoutRecordResult, LayoutResult, CreateViewParams, CreateViewResult, FormSectionSpec, SetFormLayoutParams, SetListLayoutParams, SetRelatedListsParams, ChunkRole, ChunkInfo, ChunkResult, PrunedResult, HostAssetsParams, HostAssetsResult, } from "./types";
22
- export { createTable, projectTableGraph, buildColumnXml, normalizeColumns, resolveType, applyTableSaveOverlay, defaultAccessFlags, TYPE_MAP, DEFAULT_SUPER_CLASS, DEFAULT_SAVE_ACTION, addColumn, deriveElement, setColumn, resolveAttributes, toStoredValue, } from "./table";
23
- export type { CreateTableParams, CreateTableResult, AddColumnParams, AddColumnResult, SetColumnParams, SetColumnResult, ColumnAttributes, AttributeChange, TableGraph, NormalizedColumn, ColumnSpec, AccessFlags, OverlaySpec, } from "./table";
22
+ export { createTable, projectTableGraph, buildColumnXml, normalizeColumns, resolveType, applyTableSaveOverlay, defaultAccessFlags, TYPE_MAP, DEFAULT_SUPER_CLASS, DEFAULT_SAVE_ACTION, addColumn, deriveElement, setColumn, resolveAttributes, toStoredValue, setTable, resolveTableAttributes, } from "./table";
23
+ export type { CreateTableParams, CreateTableResult, AddColumnParams, AddColumnResult, SetColumnParams, SetColumnResult, ColumnAttributes, AttributeChange, SetTableParams, SetTableResult, TableAttributes, TableGraph, NormalizedColumn, ColumnSpec, AccessFlags, OverlaySpec, } from "./table";
24
24
  export { setField } from "./setField";
25
25
  export { createRecord } from "./createRecord";
26
26
  export type { RecordWriteResult, SetFieldParams, SetFieldResult, } from "./setField";
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  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 = exports.editFlow = exports.buildPublishModel = exports.createFlow = exports.copyFlow = exports.publishFlow = exports.readActionType = exports.readFlow = exports.formatStepPill = exports.summarizeSteps = exports.verifySteps = exports.applyStepOps = exports.editActionType = exports.publishActionType = exports.triggerPublication = exports.cloneActionType = exports.cloneSubflow = exports.verifyArtifact = exports.listTemplates = exports.sincPlugin = exports.formatCreateViewResult = exports.formatLayoutResult = exports.setRelatedLists = exports.setFormLayout = exports.setListLayout = exports.createView = exports.formatAddChoicesResult = exports.formatHostAssetsResult = exports.classifyChunks = exports.hostAssets = exports.addChoicesToField = exports.resolveConfigFromEnvFile = exports.createClientFromEnvFile = exports.createClient = void 0;
10
- 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.buildStartFields = exports.publishApp = exports.INVOKE_REST_METHODS = exports.invokeRest = exports.createRecord = exports.setField = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.deriveElement = void 0;
10
+ 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.buildStartFields = exports.publishApp = exports.INVOKE_REST_METHODS = exports.invokeRest = exports.createRecord = exports.setField = exports.resolveTableAttributes = exports.setTable = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.deriveElement = void 0;
11
11
  var client_1 = require("./client");
12
12
  Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } });
13
13
  var createClientFromEnvFile_1 = require("./createClientFromEnvFile");
@@ -75,6 +75,8 @@ Object.defineProperty(exports, "deriveElement", { enumerable: true, get: functio
75
75
  Object.defineProperty(exports, "setColumn", { enumerable: true, get: function () { return table_1.setColumn; } });
76
76
  Object.defineProperty(exports, "resolveAttributes", { enumerable: true, get: function () { return table_1.resolveAttributes; } });
77
77
  Object.defineProperty(exports, "toStoredValue", { enumerable: true, get: function () { return table_1.toStoredValue; } });
78
+ Object.defineProperty(exports, "setTable", { enumerable: true, get: function () { return table_1.setTable; } });
79
+ Object.defineProperty(exports, "resolveTableAttributes", { enumerable: true, get: function () { return table_1.resolveTableAttributes; } });
78
80
  var setField_1 = require("./setField");
79
81
  Object.defineProperty(exports, "setField", { enumerable: true, get: function () { return setField_1.setField; } });
80
82
  var createRecord_1 = require("./createRecord");
@@ -48,3 +48,15 @@ export interface InvokeRestResult {
48
48
  note: string;
49
49
  }
50
50
  export declare function invokeRest(params: InvokeRestParams): Promise<InvokeRestResult>;
51
+ /**
52
+ * Write an invoke-rest result to a file as pretty-printed JSON — the
53
+ * first-class landing spot for large responses (piped stdout truncates at the
54
+ * OS pipe buffer if the process exits before draining; see the CLI's
55
+ * flush-aware exit). ATOMIC: writes to a same-directory temp file then
56
+ * renames, so a killed process leaves either the old file or the new one —
57
+ * never a silently-truncated hybrid. Overwrites an existing file by design
58
+ * (documented in --help). The parent directory must already exist; a missing
59
+ * or unwritable directory throws with the underlying error so the CLI can
60
+ * exit 1 loudly instead of "succeeding" with no file.
61
+ */
62
+ export declare function writeInvokeRestResultFile(outPath: string, result: unknown): void;
@@ -17,9 +17,46 @@
17
17
  * only in the returned result. Any log line about an invocation elsewhere must
18
18
  * carry method, path and status ONLY.
19
19
  */
20
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ var desc = Object.getOwnPropertyDescriptor(m, k);
23
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
24
+ desc = { enumerable: true, get: function() { return m[k]; } };
25
+ }
26
+ Object.defineProperty(o, k2, desc);
27
+ }) : (function(o, m, k, k2) {
28
+ if (k2 === undefined) k2 = k;
29
+ o[k2] = m[k];
30
+ }));
31
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
32
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
33
+ }) : function(o, v) {
34
+ o["default"] = v;
35
+ });
36
+ var __importStar = (this && this.__importStar) || (function () {
37
+ var ownKeys = function(o) {
38
+ ownKeys = Object.getOwnPropertyNames || function (o) {
39
+ var ar = [];
40
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
41
+ return ar;
42
+ };
43
+ return ownKeys(o);
44
+ };
45
+ return function (mod) {
46
+ if (mod && mod.__esModule) return mod;
47
+ var result = {};
48
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
49
+ __setModuleDefault(result, mod);
50
+ return result;
51
+ };
52
+ })();
20
53
  Object.defineProperty(exports, "__esModule", { value: true });
21
54
  exports.INVOKE_REST_METHODS = void 0;
22
55
  exports.invokeRest = invokeRest;
56
+ exports.writeInvokeRestResultFile = writeInvokeRestResultFile;
57
+ const fs = __importStar(require("fs"));
58
+ // Aliased: `path` is the instance-relative REST path throughout this module.
59
+ const nodePath = __importStar(require("path"));
23
60
  const client_1 = require("./client");
24
61
  exports.INVOKE_REST_METHODS = ["GET", "POST", "PUT", "DELETE"];
25
62
  function normalizeMethod(method) {
@@ -91,3 +128,33 @@ async function invokeRest(params) {
91
128
  }
92
129
  return result;
93
130
  }
131
+ /**
132
+ * Write an invoke-rest result to a file as pretty-printed JSON — the
133
+ * first-class landing spot for large responses (piped stdout truncates at the
134
+ * OS pipe buffer if the process exits before draining; see the CLI's
135
+ * flush-aware exit). ATOMIC: writes to a same-directory temp file then
136
+ * renames, so a killed process leaves either the old file or the new one —
137
+ * never a silently-truncated hybrid. Overwrites an existing file by design
138
+ * (documented in --help). The parent directory must already exist; a missing
139
+ * or unwritable directory throws with the underlying error so the CLI can
140
+ * exit 1 loudly instead of "succeeding" with no file.
141
+ */
142
+ function writeInvokeRestResultFile(outPath, result) {
143
+ var dir = nodePath.dirname(outPath);
144
+ var tempPath = nodePath.join(dir, "." + nodePath.basename(outPath) + ".tmp-" + process.pid + "-" + Date.now());
145
+ var payload = JSON.stringify(result, null, 2) + "\n";
146
+ try {
147
+ fs.writeFileSync(tempPath, payload, { encoding: "utf8" });
148
+ fs.renameSync(tempPath, outPath);
149
+ }
150
+ catch (err) {
151
+ // Best-effort temp cleanup so a failed write leaves no residue.
152
+ try {
153
+ fs.unlinkSync(tempPath);
154
+ }
155
+ catch (cleanupErr) {
156
+ // The temp file never landed — nothing to clean.
157
+ }
158
+ throw err;
159
+ }
160
+ }
@@ -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", "flow_view", "action_view", "action_edit", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "set_column", "set_field", "create_record", "host_assets", "invoke_rest", "app_publish"];
13
+ export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "flow_view", "action_view", "action_edit", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "set_column", "set_table", "set_field", "create_record", "host_assets", "invoke_rest", "app_publish"];
14
14
  export type ToolName = (typeof TOOL_NAMES)[number];
15
15
  export interface RegistryDeps {
16
16
  /** Optional client injection for tests; defaults to createClient({}). */
@@ -50,6 +50,7 @@ exports.TOOL_NAMES = [
50
50
  "create_table",
51
51
  "add_column",
52
52
  "set_column",
53
+ "set_table",
53
54
  "set_field",
54
55
  "create_record",
55
56
  "host_assets",
@@ -393,6 +394,42 @@ function buildDescriptors(deps = {}) {
393
394
  });
394
395
  },
395
396
  },
397
+ {
398
+ name: "set_table",
399
+ annotations: dovetail_mcp_kit_1.WRITE_OVERWRITE,
400
+ description: "Update an EXISTING ServiceNow TABLE's own dictionary attributes — the row where " +
401
+ "internal_type=collection and element is EMPTY — captured into a named update set, then " +
402
+ "READ BACK from the instance to verify. Completes the trio: set_field changes a RECORD's " +
403
+ "value, set_column changes a COLUMN's definition, set_table changes the TABLE's own. " +
404
+ "Currently settable: audit. audit=true turns RECORD AUDITING on for the whole table — " +
405
+ "ServiceNow then writes a sys_audit row per changed field on every insert and update, " +
406
+ "which is a real storage and write cost on a high-volume table, so weigh it before " +
407
+ "enabling. Attributes are a closed set, never an open field map; a COLUMN attribute " +
408
+ "(label/mandatory/default/maxLength/readOnly/element/internalType) is refused by name and " +
409
+ "redirected to set_column. When the requested value already matches, nothing is written " +
410
+ "and the status is 'unchanged' — note that an identical-value write also captures " +
411
+ "NOTHING, so there is no update-set row to promote. dryRun:true diffs against the " +
412
+ "instance and writes nothing.",
413
+ shape: schemas_1.setTableSchema.shape,
414
+ handler: async function (args) {
415
+ var p = schemas_1.setTableSchema.parse(args);
416
+ // Same live-path guard as set_column: the schema leaves updateSetSysId
417
+ // optional so dry-run works without one.
418
+ if (p.dryRun !== true &&
419
+ (!p.updateSetSysId || !p.updateSetSysId.trim())) {
420
+ throw new Error("set_table: updateSetSysId is required on the live path so the schema " +
421
+ "change is captured in a known update set — set dryRun:true to plan " +
422
+ "without one.");
423
+ }
424
+ return (0, table_1.setTable)({
425
+ client: client(),
426
+ table: p.table,
427
+ attributes: p.attributes,
428
+ updateSetSysId: p.updateSetSysId,
429
+ dryRun: p.dryRun,
430
+ });
431
+ },
432
+ },
396
433
  {
397
434
  name: "set_field",
398
435
  annotations: dovetail_mcp_kit_1.WRITE_OVERWRITE,
@@ -914,6 +914,43 @@ export declare var setColumnSchema: z.ZodObject<{
914
914
  dryRun?: boolean | undefined;
915
915
  updateSetSysId?: string | undefined;
916
916
  }>;
917
+ export declare var tableAttributesSchema: z.ZodObject<{
918
+ audit: z.ZodOptional<z.ZodBoolean>;
919
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
920
+ audit: z.ZodOptional<z.ZodBoolean>;
921
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
922
+ audit: z.ZodOptional<z.ZodBoolean>;
923
+ }, z.ZodTypeAny, "passthrough">>;
924
+ export declare var setTableSchema: z.ZodObject<{
925
+ table: z.ZodString;
926
+ attributes: z.ZodObject<{
927
+ audit: z.ZodOptional<z.ZodBoolean>;
928
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
929
+ audit: z.ZodOptional<z.ZodBoolean>;
930
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
931
+ audit: z.ZodOptional<z.ZodBoolean>;
932
+ }, z.ZodTypeAny, "passthrough">>;
933
+ updateSetSysId: z.ZodOptional<z.ZodString>;
934
+ dryRun: z.ZodOptional<z.ZodBoolean>;
935
+ }, "strip", z.ZodTypeAny, {
936
+ table: string;
937
+ attributes: {
938
+ audit?: boolean | undefined;
939
+ } & {
940
+ [k: string]: unknown;
941
+ };
942
+ dryRun?: boolean | undefined;
943
+ updateSetSysId?: string | undefined;
944
+ }, {
945
+ table: string;
946
+ attributes: {
947
+ audit?: boolean | undefined;
948
+ } & {
949
+ [k: string]: unknown;
950
+ };
951
+ dryRun?: boolean | undefined;
952
+ updateSetSysId?: string | undefined;
953
+ }>;
917
954
  export declare var setFieldSchema: z.ZodObject<{
918
955
  table: z.ZodString;
919
956
  sysId: z.ZodOptional<z.ZodString>;
@@ -4,7 +4,7 @@
4
4
  * own file so registry.ts stays focused on wiring.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.invokeRestSchema = exports.publishAppSchema = exports.createRecordSchema = exports.setFieldSchema = exports.setColumnSchema = exports.columnAttributesSchema = exports.addColumnSchema = exports.createTableSchema = exports.columnSpecSchema = exports.hostAssetsSchema = exports.editFlowSchema = exports.editActionSchema = exports.stepInputPatchSchema = exports.testFlowSchema = exports.createFlowSchema = exports.copyFlowSchema = exports.publishFlowSchema = exports.viewActionSchema = exports.viewFlowSchema = exports.addChoicesToFieldSchema = exports.choiceValueSchema = exports.setRelatedListsSchema = exports.setFormLayoutSchema = exports.formSectionSchema = exports.setListLayoutSchema = exports.createViewSchema = void 0;
7
+ exports.invokeRestSchema = exports.publishAppSchema = exports.createRecordSchema = exports.setFieldSchema = exports.setTableSchema = exports.tableAttributesSchema = exports.setColumnSchema = exports.columnAttributesSchema = exports.addColumnSchema = exports.createTableSchema = exports.columnSpecSchema = exports.hostAssetsSchema = exports.editFlowSchema = exports.editActionSchema = exports.stepInputPatchSchema = exports.testFlowSchema = exports.createFlowSchema = exports.copyFlowSchema = exports.publishFlowSchema = exports.viewActionSchema = exports.viewFlowSchema = exports.addChoicesToFieldSchema = exports.choiceValueSchema = exports.setRelatedListsSchema = exports.setFormLayoutSchema = exports.formSectionSchema = exports.setListLayoutSchema = exports.createViewSchema = void 0;
8
8
  const zod_1 = require("zod");
9
9
  exports.createViewSchema = zod_1.z.object({
10
10
  name: zod_1.z.string().min(1),
@@ -241,6 +241,28 @@ exports.setColumnSchema = zod_1.z.object({
241
241
  updateSetSysId: zod_1.z.string().min(1).optional(),
242
242
  dryRun: zod_1.z.boolean().optional(),
243
243
  });
244
+ // set-table takes a CLOSED attribute set for the same reason set-column does: an
245
+ // unbounded write to sys_dictionary lets a caller silently corrupt schema. These are
246
+ // the TABLE's own attributes (the internal_type=collection row), not a column's.
247
+ //
248
+ // .passthrough() is deliberate: z.object() strips unknown keys by default, which would
249
+ // silently drop a column attribute (or a typo) BEFORE resolveTableAttributes could see
250
+ // it — so { audit:true, label:"x" } would succeed, quietly discarding label. Passing
251
+ // unknown keys through lets the core allowlist reject them (a column attribute earns the
252
+ // "use set-column" redirect; anything else, "not a settable table attribute").
253
+ exports.tableAttributesSchema = zod_1.z
254
+ .object({
255
+ audit: zod_1.z.boolean().optional(),
256
+ })
257
+ .passthrough();
258
+ exports.setTableSchema = zod_1.z.object({
259
+ table: zod_1.z.string().min(1),
260
+ attributes: exports.tableAttributesSchema,
261
+ // Optional because dryRun works without one; the live-path requirement is
262
+ // enforced at the tool boundary (registry.ts), matching set_column.
263
+ updateSetSysId: zod_1.z.string().min(1).optional(),
264
+ dryRun: zod_1.z.boolean().optional(),
265
+ });
244
266
  // Data-record write verbs. Kept as plain z.object (no .refine wrapper) so
245
267
  // registry.ts can read `.shape`; the deeper rules — one of sysId/query, at
246
268
  // least one field, the schema-table refusal — are enforced by the core
@@ -15,3 +15,5 @@ export { addColumn, deriveElement } from "./addColumn";
15
15
  export type { AddColumnParams, AddColumnResult } from "./addColumn";
16
16
  export { setColumn, resolveAttributes, toStoredValue, findTruncationRisk, } from "./setColumn";
17
17
  export type { SetColumnParams, SetColumnResult, ColumnAttributes, AttributeChange, TruncationRisk, } from "./setColumn";
18
+ export { setTable, resolveTableAttributes } from "./setTable";
19
+ export type { SetTableParams, SetTableResult, TableAttributes, } from "./setTable";
@@ -5,7 +5,7 @@
5
5
  * live-validation caveat.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.findTruncationRisk = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.deriveElement = exports.addColumn = exports.decodeHtmlEntities = exports.scrapeCk = exports.postForm = exports.parseFormInputs = exports.getNewRecordForm = exports.getRecordForm = exports.setCurrentApplication = exports.openFormSession = exports.resolveFormAuth = exports.TYPE_MAP = exports.listEditKey = exports.showInMenuKey = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.xmlEscape = exports.buildColumnXml = exports.DEFAULT_COLUMNS_REL_ID = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.parseSysIdFromLocation = exports.projectTableGraph = exports.createTable = void 0;
8
+ exports.resolveTableAttributes = exports.setTable = exports.findTruncationRisk = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.deriveElement = exports.addColumn = exports.decodeHtmlEntities = exports.scrapeCk = exports.postForm = exports.parseFormInputs = exports.getNewRecordForm = exports.getRecordForm = exports.setCurrentApplication = exports.openFormSession = exports.resolveFormAuth = exports.TYPE_MAP = exports.listEditKey = exports.showInMenuKey = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.xmlEscape = exports.buildColumnXml = exports.DEFAULT_COLUMNS_REL_ID = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.parseSysIdFromLocation = exports.projectTableGraph = exports.createTable = void 0;
9
9
  var createTable_1 = require("./createTable");
10
10
  Object.defineProperty(exports, "createTable", { enumerable: true, get: function () { return createTable_1.createTable; } });
11
11
  Object.defineProperty(exports, "projectTableGraph", { enumerable: true, get: function () { return createTable_1.projectTableGraph; } });
@@ -42,3 +42,6 @@ Object.defineProperty(exports, "setColumn", { enumerable: true, get: function ()
42
42
  Object.defineProperty(exports, "resolveAttributes", { enumerable: true, get: function () { return setColumn_1.resolveAttributes; } });
43
43
  Object.defineProperty(exports, "toStoredValue", { enumerable: true, get: function () { return setColumn_1.toStoredValue; } });
44
44
  Object.defineProperty(exports, "findTruncationRisk", { enumerable: true, get: function () { return setColumn_1.findTruncationRisk; } });
45
+ var setTable_1 = require("./setTable");
46
+ Object.defineProperty(exports, "setTable", { enumerable: true, get: function () { return setTable_1.setTable; } });
47
+ Object.defineProperty(exports, "resolveTableAttributes", { enumerable: true, get: function () { return setTable_1.resolveTableAttributes; } });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * dove-sn set-table — update the dictionary attributes of an EXISTING ServiceNow
3
+ * TABLE, captured into a named update set, then read back and verified against the
4
+ * instance.
5
+ *
6
+ * WHY THIS IS NOT set-column. Every table carries one sys_dictionary row that
7
+ * describes the TABLE rather than any column: `internal_type=collection`, with
8
+ * `element` empty. Table-wide settings live there. `set-column` targets
9
+ * `name=<table>^element=<column>` and its whole apparatus — inheritance walking,
10
+ * the max_length ALTER, the shrink guard — is column-shaped; none of it applies to
11
+ * a collection row. And `set-field` is a DATA verb that refuses sys_dictionary
12
+ * outright. So the collection row had no owner. This is it.
13
+ *
14
+ * The split that settled in practice, extended one step:
15
+ * "field" = a record's value -> set-field
16
+ * "column" = a table's column schema -> set-column
17
+ * "table" = the table's own schema -> set-table (this verb)
18
+ *
19
+ * AUDIT IS A TABLE-LEVEL FLAG. `sys_dictionary.audit` on the collection row is what
20
+ * turns record auditing on for a table: with it true, ServiceNow writes a sys_audit
21
+ * row per changed field on every insert and update. That is a genuine cost on a
22
+ * high-write table, so this verb reports the before -> after honestly and never
23
+ * writes a value over itself.
24
+ *
25
+ * THE ALLOWLIST IS DELIBERATELY SMALL. An unbounded write to sys_dictionary lets a
26
+ * caller silently corrupt schema, so — exactly as set-column does — only named
27
+ * attributes are settable and everything else is refused by name. Widen WRITABLE
28
+ * only for an attribute whose behaviour on a collection row has been verified live.
29
+ */
30
+ import type { ServiceNowClient } from "../client";
31
+ import type { AttributeChange } from "./setColumn";
32
+ export interface TableAttributes {
33
+ /**
34
+ * Record auditing for the whole table (sys_dictionary.audit on the collection
35
+ * row). True makes ServiceNow write a sys_audit row per changed field on every
36
+ * insert and update.
37
+ */
38
+ audit?: boolean;
39
+ }
40
+ export interface SetTableParams {
41
+ client: ServiceNowClient;
42
+ /** The table, by name (e.g. "x_cadso_core_setting"). */
43
+ table: string;
44
+ /** The attributes to set. At least one is required. */
45
+ attributes: TableAttributes;
46
+ /** Update set to capture the change into. REQUIRED on the live path. */
47
+ updateSetSysId?: string;
48
+ /** Plan only — resolves and diffs, writes nothing. */
49
+ dryRun?: boolean;
50
+ }
51
+ export interface SetTableResult {
52
+ status: "dry-run" | "applied" | "unchanged" | "failed";
53
+ table: string;
54
+ /** sys_id of the table's collection sys_dictionary row. */
55
+ tableSysId: string;
56
+ updateSetSysId: string;
57
+ /** The attributes that differed and were written (empty when nothing changed). */
58
+ changes: Array<AttributeChange>;
59
+ /** True only when every requested value was READ BACK from the instance and matched. */
60
+ verified: boolean;
61
+ /** True when the sys_update_xml row for this table was found in the named update
62
+ * set. A write that lands but is not captured cannot be promoted — it dies on this
63
+ * instance, so a 200 alone is not success. */
64
+ capturedInUpdateSet: boolean;
65
+ note: string;
66
+ }
67
+ /**
68
+ * Translate the caller's friendly attributes into the sys_dictionary columns to
69
+ * write. Throws on anything not on the allowlist, and redirects column attributes
70
+ * to set-column by name rather than reporting them as merely unknown.
71
+ */
72
+ export declare function resolveTableAttributes(attributes: TableAttributes): Record<string, string>;
73
+ export declare function setTable(params: SetTableParams): Promise<SetTableResult>;
@@ -0,0 +1,294 @@
1
+ "use strict";
2
+ /**
3
+ * dove-sn set-table — update the dictionary attributes of an EXISTING ServiceNow
4
+ * TABLE, captured into a named update set, then read back and verified against the
5
+ * instance.
6
+ *
7
+ * WHY THIS IS NOT set-column. Every table carries one sys_dictionary row that
8
+ * describes the TABLE rather than any column: `internal_type=collection`, with
9
+ * `element` empty. Table-wide settings live there. `set-column` targets
10
+ * `name=<table>^element=<column>` and its whole apparatus — inheritance walking,
11
+ * the max_length ALTER, the shrink guard — is column-shaped; none of it applies to
12
+ * a collection row. And `set-field` is a DATA verb that refuses sys_dictionary
13
+ * outright. So the collection row had no owner. This is it.
14
+ *
15
+ * The split that settled in practice, extended one step:
16
+ * "field" = a record's value -> set-field
17
+ * "column" = a table's column schema -> set-column
18
+ * "table" = the table's own schema -> set-table (this verb)
19
+ *
20
+ * AUDIT IS A TABLE-LEVEL FLAG. `sys_dictionary.audit` on the collection row is what
21
+ * turns record auditing on for a table: with it true, ServiceNow writes a sys_audit
22
+ * row per changed field on every insert and update. That is a genuine cost on a
23
+ * high-write table, so this verb reports the before -> after honestly and never
24
+ * writes a value over itself.
25
+ *
26
+ * THE ALLOWLIST IS DELIBERATELY SMALL. An unbounded write to sys_dictionary lets a
27
+ * caller silently corrupt schema, so — exactly as set-column does — only named
28
+ * attributes are settable and everything else is refused by name. Widen WRITABLE
29
+ * only for an attribute whose behaviour on a collection row has been verified live.
30
+ */
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.resolveTableAttributes = resolveTableAttributes;
33
+ exports.setTable = setTable;
34
+ const setField_1 = require("../setField");
35
+ const choices_1 = require("../choices");
36
+ const setColumn_1 = require("./setColumn");
37
+ /** The table-level dictionary attributes set-table will write, keyed by the friendly
38
+ * name a caller uses. Everything absent from this map is refused. */
39
+ var WRITABLE = {
40
+ audit: "audit",
41
+ };
42
+ /**
43
+ * Attributes that belong to a COLUMN, not the table. Refused by name so that asking
44
+ * for one earns a redirect to set-column instead of a confusing "not settable".
45
+ */
46
+ var COLUMN_ATTRIBUTES = [
47
+ "label",
48
+ "mandatory",
49
+ "default",
50
+ "readOnly",
51
+ "maxLength",
52
+ "internalType",
53
+ "element",
54
+ ];
55
+ /**
56
+ * Translate the caller's friendly attributes into the sys_dictionary columns to
57
+ * write. Throws on anything not on the allowlist, and redirects column attributes
58
+ * to set-column by name rather than reporting them as merely unknown.
59
+ */
60
+ function resolveTableAttributes(attributes) {
61
+ if (!attributes || typeof attributes !== "object") {
62
+ throw new Error("set-table: attributes object is required.");
63
+ }
64
+ var out = {};
65
+ var keys = Object.keys(attributes);
66
+ for (var i = 0; i < keys.length; i += 1) {
67
+ var key = keys[i];
68
+ var value = attributes[key];
69
+ if (value === undefined || value === null)
70
+ continue;
71
+ if (COLUMN_ATTRIBUTES.indexOf(key) !== -1) {
72
+ throw new Error("set-table: '" +
73
+ key +
74
+ "' is a COLUMN attribute, not a table one — it lives on a column's " +
75
+ "dictionary row, not the table's collection row. Use set-column.");
76
+ }
77
+ var target = WRITABLE[key];
78
+ if (!target) {
79
+ throw new Error("set-table: '" +
80
+ key +
81
+ "' is not a settable table attribute. Settable: " +
82
+ Object.keys(WRITABLE).join(", ") +
83
+ ".");
84
+ }
85
+ out[target] = (0, setColumn_1.toStoredValue)(value);
86
+ }
87
+ if (Object.keys(out).length === 0) {
88
+ throw new Error("set-table: at least one attribute is required.");
89
+ }
90
+ return out;
91
+ }
92
+ /**
93
+ * Fetch the table's collection dictionary row — `element` empty, which is what
94
+ * distinguishes the table's own row from every column row sharing its `name`.
95
+ */
96
+ async function fetchCollectionRow(client, table, fields) {
97
+ var rows = await client.table.query("sys_dictionary", "name=" + (0, choices_1.encodeQueryValue)(table) + "^elementISEMPTY", { limit: 1, fields: fields });
98
+ if (rows.length > 0)
99
+ return rows[0];
100
+ throw new Error("set-table: no collection dictionary row found for '" +
101
+ table +
102
+ "'. Check the table name — every real table has exactly one row with " +
103
+ "internal_type=collection and an empty element.");
104
+ }
105
+ /**
106
+ * Require the update set to be open. A write into a Complete/Ignored set is accepted
107
+ * and then silently lost — the change lands on the instance and is captured nowhere,
108
+ * so it can never be promoted.
109
+ */
110
+ async function assertUpdateSetOpen(client, updateSetSysId) {
111
+ var rows = await client.table.query("sys_update_set", "sys_id=" + (0, choices_1.encodeQueryValue)(updateSetSysId), { limit: 1, fields: ["sys_id", "name", "state"] });
112
+ if (rows.length === 0) {
113
+ throw new Error("set-table: update set " + updateSetSysId + " not found on this instance.");
114
+ }
115
+ var state = (0, setField_1.fieldToString)(rows[0].state);
116
+ if (state !== "in progress" && state !== "in_progress") {
117
+ throw new Error("set-table: update set '" +
118
+ (0, setField_1.fieldToString)(rows[0].name) +
119
+ "' is '" +
120
+ state +
121
+ "', not 'in progress'. A change written into a closed set is captured " +
122
+ "nowhere and can never be promoted.");
123
+ }
124
+ }
125
+ /**
126
+ * Assert the change was captured. The collection row's update name is deterministic
127
+ * and ends in `_null` — the element is empty, so ServiceNow names the row for "no
128
+ * column" rather than for a column.
129
+ */
130
+ async function assertCaptured(client, table, updateSetSysId) {
131
+ var name = "sys_dictionary_" + table + "_null";
132
+ var rows = await client.table.query("sys_update_xml", "update_set=" +
133
+ (0, choices_1.encodeQueryValue)(updateSetSysId) +
134
+ "^name=" +
135
+ (0, choices_1.encodeQueryValue)(name), { limit: 1, fields: ["sys_id", "name"] });
136
+ return rows.length > 0;
137
+ }
138
+ function describeChanges(changes) {
139
+ return changes
140
+ .map(function (c) {
141
+ return c.attribute + " " + (c.from === "" ? "(empty)" : c.from) + " -> " + c.to;
142
+ })
143
+ .join(", ");
144
+ }
145
+ async function setTable(params) {
146
+ if (!params || !params.client) {
147
+ throw new Error("set-table: client is required.");
148
+ }
149
+ if (!params.table || !String(params.table).trim()) {
150
+ throw new Error("set-table: --table is required.");
151
+ }
152
+ // The allowlist is enforced BEFORE anything touches the instance, so an
153
+ // unsettable attribute fails on the dry-run too.
154
+ var writes = resolveTableAttributes(params.attributes);
155
+ var targets = Object.keys(writes);
156
+ var table = String(params.table).trim();
157
+ var readFields = ["sys_id", "name", "element", "internal_type"].concat(targets);
158
+ var row = await fetchCollectionRow(params.client, table, readFields);
159
+ var tableSysId = (0, setField_1.fieldToString)(row.sys_id);
160
+ // Diff against what the instance actually stores, so a same-value request is
161
+ // reported as unchanged rather than written over itself and called applied.
162
+ var changes = [];
163
+ for (var i = 0; i < targets.length; i += 1) {
164
+ var field = targets[i];
165
+ var from = (0, setField_1.fieldToString)(row[field]);
166
+ var to = writes[field];
167
+ if (from !== to) {
168
+ changes.push({ attribute: field, from: from, to: to });
169
+ }
170
+ }
171
+ if (params.dryRun) {
172
+ return {
173
+ status: "dry-run",
174
+ table: table,
175
+ tableSysId: tableSysId,
176
+ updateSetSysId: params.updateSetSysId ? String(params.updateSetSysId).trim() : "",
177
+ changes: changes,
178
+ verified: false,
179
+ capturedInUpdateSet: false,
180
+ note: changes.length === 0
181
+ ? "dry-run: no write. " +
182
+ table +
183
+ " already holds every requested value — nothing would change."
184
+ : "dry-run: no write. Would set " +
185
+ describeChanges(changes) +
186
+ " on " +
187
+ table +
188
+ ", captured into update set " +
189
+ (params.updateSetSysId ? params.updateSetSysId : "(none provided)") +
190
+ ".",
191
+ };
192
+ }
193
+ if (!params.updateSetSysId || !String(params.updateSetSysId).trim()) {
194
+ throw new Error("set-table: --update-set <sys_id> is required so the schema change is " +
195
+ "captured and can be promoted. An uncaptured change exists only on this " +
196
+ "instance.");
197
+ }
198
+ var updateSetSysId = String(params.updateSetSysId).trim();
199
+ await assertUpdateSetOpen(params.client, updateSetSysId);
200
+ if (changes.length === 0) {
201
+ return {
202
+ status: "unchanged",
203
+ table: table,
204
+ tableSysId: tableSysId,
205
+ updateSetSysId: updateSetSysId,
206
+ changes: [],
207
+ verified: true,
208
+ capturedInUpdateSet: false,
209
+ note: table +
210
+ " already holds every requested value — nothing written, and nothing " +
211
+ "captured: an identical-value write produces no sys_update_xml row.",
212
+ };
213
+ }
214
+ var fieldsToWrite = {};
215
+ for (var k = 0; k < changes.length; k += 1) {
216
+ fieldsToWrite[changes[k].attribute] = changes[k].to;
217
+ }
218
+ try {
219
+ await params.client.claude.pushWithUpdateSet({
220
+ update_set_sys_id: updateSetSysId,
221
+ table: "sys_dictionary",
222
+ record_sys_id: tableSysId,
223
+ fields: fieldsToWrite,
224
+ });
225
+ }
226
+ catch (e) {
227
+ // The write may or may not have landed — the transport failed, not necessarily
228
+ // the change. Say exactly that rather than leaving the caller to guess.
229
+ return {
230
+ status: "failed",
231
+ table: table,
232
+ tableSysId: tableSysId,
233
+ updateSetSysId: updateSetSysId,
234
+ changes: changes,
235
+ verified: false,
236
+ capturedInUpdateSet: false,
237
+ note: "the write to " +
238
+ table +
239
+ " failed in transport: " +
240
+ (e instanceof Error ? e.message : String(e)) +
241
+ ". Re-read the dictionary row before retrying — the change may or may not " +
242
+ "have landed.",
243
+ };
244
+ }
245
+ // Read back from the instance. A 200 is not proof the value stuck.
246
+ var afterRow = await fetchCollectionRow(params.client, table, readFields);
247
+ var verified = true;
248
+ for (var m = 0; m < targets.length; m += 1) {
249
+ if ((0, setField_1.fieldToString)(afterRow[targets[m]]) !== writes[targets[m]]) {
250
+ verified = false;
251
+ }
252
+ }
253
+ var captured = await assertCaptured(params.client, table, updateSetSysId);
254
+ if (!verified) {
255
+ return {
256
+ status: "failed",
257
+ table: table,
258
+ tableSysId: tableSysId,
259
+ updateSetSysId: updateSetSysId,
260
+ changes: changes,
261
+ verified: false,
262
+ capturedInUpdateSet: captured,
263
+ note: "the write to " +
264
+ table +
265
+ " landed but the read-back does not match what was requested — check " +
266
+ "dictionary ACLs on this instance.",
267
+ };
268
+ }
269
+ return {
270
+ status: "applied",
271
+ table: table,
272
+ tableSysId: tableSysId,
273
+ updateSetSysId: updateSetSysId,
274
+ changes: changes,
275
+ verified: true,
276
+ capturedInUpdateSet: captured,
277
+ note: captured
278
+ ? "Set " +
279
+ describeChanges(changes) +
280
+ " on " +
281
+ table +
282
+ ", verified by read-back and captured in update set " +
283
+ updateSetSysId +
284
+ "."
285
+ : "Set " +
286
+ describeChanges(changes) +
287
+ " on " +
288
+ table +
289
+ " and verified by read-back, but NO sys_update_xml row was found in " +
290
+ "update set " +
291
+ updateSetSysId +
292
+ " — the change cannot be promoted off this instance until it is captured.",
293
+ };
294
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-servicenow",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },