@tenonhq/dovetail-servicenow 0.0.50 → 0.0.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -736,7 +736,7 @@ admin, and `sys_index_column` does not exist at all (HTTP 400 `Invalid table`),
736
736
  is no two-table index model to join and nothing to cross-check against.
737
737
 
738
738
  Each row comes back as `{ name, columns, type, rawColumns }`. `columns` is the view's
739
- bracketed `column_names` cell (`"[phone]"`, `"[a,b]"`) **parsed** into a list - never
739
+ bracketed `column_names` cell (`"[phone]"`, `"[a;b]"` - semicolon-separated when composite) **parsed** into a list - never
740
740
  substring-matched, because `"[owner_id]"` contains `"owner"`. `type` is `access_method`.
741
741
 
742
742
  **Uniqueness is not readable.** `v_db_index` has no uniqueness field, so a unique index
@@ -747,49 +747,73 @@ is wrong than that the table is unindexed - every physical table has a `PRIMARY`
747
747
 
748
748
  ### Create an index (composite and non-unique included)
749
749
 
750
- > **A DATABASE INDEX IS A PHYSICAL, PER-INSTANCE CHANGE. IT IS NOT CAPTURED IN AN UPDATE
751
- > SET AND DOES NOT TRAVEL WITH A PROMOTION.** Re-run `index-create` against every
752
- > environment that needs the index (dev, test, uat, staging, prod). There is deliberately
753
- > no `--update-set` - passing one is an error, not a silent no-op.
750
+ > **An index IS captured in an update set.** The platform's build job writes a
751
+ > `sys_update_xml` row (`type=Indexes`, name `sys_index_<table>_<col>_<col>…`) into the
752
+ > session user's **current** update set - the Database Indexes dialog says so itself, and
753
+ > it was read back live on tenonworkstudio 2026-10-08. `--update-set` is therefore
754
+ > **required** on the live path: the verb pins that set as current first and reads the
755
+ > capture row back from it afterwards. The physical index is still built per instance;
756
+ > committing the set elsewhere rebuilds it there.
754
757
 
755
758
  ```bash
756
- # Dry-run (the DEFAULT) - sends nothing and reads nothing
759
+ # Dry-run (the DEFAULT) - sends nothing and reads nothing; no update set needed
757
760
  npx dove-sn index-create --table x_cadso_journey_instance --columns state,created_on
758
761
 
759
762
  # Send it
760
763
  npx dove-sn index-create \
761
- --table x_cadso_journey_instance --columns state,created_on --confirm --json
764
+ --table x_cadso_journey_instance --columns state,created_on \
765
+ --update-set <sys_id> --confirm --json
762
766
  ```
763
767
 
764
768
  This is what `add-index` cannot do. `sys_dictionary.unique` - the only record-shaped lever
765
769
  - is **per-column and unique-only**, so composite and plain indexes have no record path at
766
- all. `index-create` instead replays the platform's own index-creator form
767
- (`sys_action=create_index`, `sysparm_index_table`, `sysparm_fields`,
768
- `sysparm_unique_index_SKIP`) over a form-login session. That contract is lifted from the
769
- instance's shipped `index_creator_information` UI macro, not from a guess, and the POST
770
- target is taken from the rendered page's own `<form action>`.
770
+ all (`sys_index` is API-level-ACL 403, `sys_index_column` does not exist). `index-create`
771
+ instead replays the two GlideAjax calls the platform's own Database Indexes dialog makes
772
+ on `xmlhttp.do` over a form-login session - taken from a HAR of the Studio dialog plus the
773
+ `dialog_index_create` UI page's client script, both read live:
774
+
775
+ 1. `IndexCreatorErrorChecker` / `canCreate` - the dialog's pre-flight (`sysparm_table_name`,
776
+ `sysparm_field_names`, `sysparm_unique`, `sysparm_access_method`). A `canCreate:false`
777
+ verdict is returned verbatim with its `errorCode` and nothing is scheduled.
778
+ 2. `ScheduleCreator` / `createSchedule` - schedules the build (`sysparm_table`,
779
+ `sysparm_fields`, `sysparm_access_method` - default `btree`, exactly as the dialog's
780
+ client JS does - `sysparm_unique` `true|false`, empty `sysparm_email` = no
781
+ notification, empty `sysparm_schedule_name`).
782
+
783
+ (The `index_creator_dialog` page's `sys_action=create_index` form is inert - the browser
784
+ never submits it, and the UI page has no processing script. The earlier form-POST replay
785
+ was a silent no-op for that reason.)
771
786
 
772
787
  - **Dry-run by default.** Without `--confirm` nothing is sent *and nothing is read*;
773
788
  `--dry-run` forces a plan even with `--confirm`.
789
+ - **Target checks before anything else.** The table must exist; the update set must exist,
790
+ be `in progress`, and belong to the table's application scope - a set in another scope
791
+ is refused, because the capture row would land in the wrong scope.
774
792
  - **Idempotent.** On the live path `v_db_index` is read first, and an index over *exactly*
775
- these columns short-circuits to `already-exists` with no form session and no write.
776
- Column **order** is part of an index's identity - `[a,b]` is not `[b,a]`.
777
- - **`--name` is refused.** The platform's form has no name input; ServiceNow names the
778
- index itself. Reporting a name the instance does not carry would be a lie, so the
779
- created index's *real* name is returned in `name` instead.
780
- - **The read-back is the proof.** After the POST the index is polled for in `v_db_index`
781
- (default 10 checks, 3 s apart - a build on a populated table is asynchronous). If it
782
- never appears the status is `failed`: a form processor returning a page is not evidence
783
- an ALTER ran, and a unique index cannot build over duplicate values (EMPTY counts).
793
+ these columns short-circuits to `already-exists` with no pin, no form session and no
794
+ write. Column **order** is part of an index's identity - `[a;b]` is not `[b;a]`.
795
+ - **The pin is read back.** The set is pinned with Dovetail's own `changeUpdateSet` and
796
+ `currentUpdateSet` is read; a pin that did not take stops the run before the session
797
+ opens.
798
+ - **`--name` is refused.** The dialog has no name input; ServiceNow names the index after
799
+ its leading column (live: `[sys_created_on;status;version_step;version]` → index
800
+ `sys_created_on`). The created index's *real* name is returned in `name`.
801
+ - **The read-back is the proof.** After scheduling, the index is polled for in `v_db_index`
802
+ (default 10 checks, 3 s apart). If it never appears the status is `failed`: an accepted
803
+ schedule is not evidence an ALTER ran, and a unique index cannot build over duplicate
804
+ values (EMPTY counts). Then the capture row is looked for in the pinned set:
805
+ `captured:true` only when it was read back; an index that exists but was not captured
806
+ is `created:true, captured:false` with `update-set-capture` in `unverified` - and
807
+ exit code 2, because it will not travel.
784
808
  - **Uniqueness is still never claimed.** `uniqueness-enforced` stays in `unverified` on
785
809
  every status.
786
810
 
787
- **Requires a username+password identity that can form-log-in.** An instance on
788
- API-key-only auth, SSO or MFA rejects the form login however valid the API key is; the
789
- verb fails at the session with that diagnosis rather than a mystery 302, and no `.do`
790
- replay (including `create-table`'s) can work in that state.
811
+ **Requires a username+password identity that can form-log-in.** `xmlhttp.do` ignores
812
+ Basic auth and API keys, so an API-key-only, SSO or MFA identity fails at the session with
813
+ that diagnosis (TenonHQ/Dovetail#292).
791
814
 
792
- Exit codes: `0` created / already-exists / dry-run, `1` bad args, `2` failed.
815
+ Exit codes: `0` created / already-exists / dry-run, `1` bad args, `2` failed - and `2`
816
+ when the index was created but its capture row was not found in the pinned set.
793
817
 
794
818
  ### Set a field on a record
795
819
 
@@ -1167,9 +1191,9 @@ read back from the `v_db_index` view - uniqueness enforcement is always reported
1167
1191
  unverified) / `index_list` (read-only: a table's database indexes from `v_db_index`,
1168
1192
  the only index read surface - `sys_index` is API-level-ACL 403 and `sys_index_column`
1169
1193
  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
1194
+ replaying the Database Indexes dialog's own processor calls; dry-run by default, idempotent,
1195
+ pinned to a required update set, read back from `v_db_index` and the capture row read back
1196
+ from `sys_update_xml`), the record-write verbs `set_field` (update scalar fields on an
1173
1197
  existing record), `create_record` (insert one record) and `delete_record` (delete one
1174
1198
  record — dry-run by default, `confirm:true` to apply, `updateSetSysId` required, the
1175
1199
  record read back before AND after so success is only reported once it is confirmed
package/dist/cli.js CHANGED
@@ -1588,16 +1588,19 @@ async function runIndexList(flags) {
1588
1588
  }
1589
1589
  /**
1590
1590
  * 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]
1591
+ * --table x_cadso_core_u_smoke --columns a,b --update-set <sys_id> [--unique]
1592
+ * [--access-method <m>] [--confirm] [--dry-run] [--poll-attempts <n>]
1593
+ * [--poll-interval-ms <n>] [--debug] [--json]
1594
1594
  *
1595
1595
  * 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.
1596
+ * --dry-run forces a plan even with it. --update-set is REQUIRED on the live path:
1597
+ * the build job captures the index definition (sys_update_xml type=Indexes) into the
1598
+ * user's CURRENT update set, so the verb pins that set first and reads the capture
1599
+ * row back from it afterwards.
1598
1600
  *
1599
1601
  * 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").
1602
+ * includes "scheduled but no index was read back") — and 2 when the index was read
1603
+ * back but its capture row was NOT found in the pinned set.
1601
1604
  */
1602
1605
  async function runIndexCreate(flags) {
1603
1606
  var columns = splitList(flags.columns || "");
@@ -1605,10 +1608,12 @@ async function runIndexCreate(flags) {
1605
1608
  process.stderr.write("index-create: --table <name> and --columns <a[,b,...]> are required\n");
1606
1609
  return 1;
1607
1610
  }
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");
1611
+ // DRY-RUN BY DEFAULT: --confirm is what sends; --dry-run forces a plan even with it.
1612
+ var indexDryRun = flags["dry-run"] === "true" || flags.confirm !== "true";
1613
+ if (!indexDryRun && !flags["update-set"]) {
1614
+ process.stderr.write("index-create: --update-set is required on the live path — the index " +
1615
+ "definition is captured into the user's CURRENT update set, so it must be " +
1616
+ "pinned first (only a dry-run works without one)\n");
1612
1617
  return 1;
1613
1618
  }
1614
1619
  var params = {
@@ -1623,8 +1628,8 @@ async function runIndexCreate(flags) {
1623
1628
  params.name = flags.name;
1624
1629
  if (flags["access-method"])
1625
1630
  params.accessMethod = flags["access-method"];
1626
- if (flags["form-path"])
1627
- params.formPath = flags["form-path"];
1631
+ if (flags["update-set"])
1632
+ params.updateSetSysId = flags["update-set"];
1628
1633
  if (flags.debug === "true")
1629
1634
  params.debug = true;
1630
1635
  // A non-integer poll setting would make the bounded wait unbounded (or zero).
@@ -1663,6 +1668,12 @@ async function runIndexCreate(flags) {
1663
1668
  (result.name ? " -> " + result.name : "") +
1664
1669
  (result.instance ? " on " + result.instance : "") +
1665
1670
  (result.verified ? " — verified" : "") +
1671
+ (result.updateSet
1672
+ ? " [update set " +
1673
+ (result.updateSet.name || result.updateSet.sysId) +
1674
+ (result.captured ? " — captured" : " — capture NOT verified") +
1675
+ "]"
1676
+ : "") +
1666
1677
  "\n" +
1667
1678
  result.note +
1668
1679
  "\nUNVERIFIED: " +
@@ -1671,6 +1682,10 @@ async function runIndexCreate(flags) {
1671
1682
  }
1672
1683
  if (result.status === "failed")
1673
1684
  return 2;
1685
+ // An index that exists but whose definition was not captured will not travel —
1686
+ // surface that as a non-zero exit, the same way an unread-back index is.
1687
+ if (result.status === "created" && !result.captured)
1688
+ return 2;
1674
1689
  return 0;
1675
1690
  }
1676
1691
  /** Parse a CLI boolean flag. Bare `--mandatory` means true; `--mandatory false` means
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": {
package/dist/index.d.ts CHANGED
@@ -21,7 +21,7 @@ export { sincPlugin } from "./plugin";
21
21
  export { listTemplates, verifyArtifact, cloneSubflow, cloneActionType, resolveScope, slugInternalName, fetchActionSteps, findStepInput, triggerPublication, publishActionType, editActionType, defineActionType, planActionDefinition, validateDefineSpec, applyStepOps, verifySteps, summarizeSteps, formatStepPill, readFlow, readActionType, publishFlow, copyFlow, createFlow, buildPublishModel, editFlow, testFlow, DEFAULT_RUN_FLOW_PATH, generateSysId, topoSort, executeWritePlan, WriteOrderError, } from "./flowDesigner";
22
22
  export type { TemplateRef, ListTemplatesParams, FlowKind, VerifyExpect, VerifyFound, VerifyFailure, VerifyReport, VerifyArtifactParams, CloneSubflowParams, CloneSubflowResult, CloneActionTypeParams, CloneActionTypeResult, CloneActionTypePlan, CloneActionTypeStepReport, TriggerPublicationParams, TriggerPublicationResult, PublishActionTypeParams, PublishActionTypeResult, EditActionTypeParams, EditActionTypeResult, EditActionTypeOps, DefineActionSpec, DefineActionInputSpec, DefineActionOutputSpec, DefineStepSpec, DefineStepVarSpec, DefineStepOutputSpec, DefineStepValue, DefineActionTypeParams, DefineActionTypeResult, DefineActionDiff, DefineActionPlan, PlanActionDefinitionParams, ActionView, StepView, StepOps, StepRecord, StepSummary, StepIoSummary, PatchStepScriptOp, SetStepInputOp, AddStepOutputOp, AddStepInputOp, ApplyStepOpsResult, VerifyStepsResult, ReadFlowParams, ReadFlowResult, FlowStep, FlowVariable, ReadActionTypeParams, ReadActionTypeResult, ActionIo, PublishFlowParams, PublishFlowResult, CopyFlowParams, CopyFlowResult, CreateFlowParams, CreateFlowResult, EditFlowParams, EditFlowResult, EditFlowOps, StepInputPatch, TestFlowParams, TestFlowResult, WriteOp, WriteOpResult, } from "./flowDesigner";
23
23
  export type { ServiceNowClientConfig, ChoiceValue, ChoiceType, AddChoicesParams, AddChoicesResult, ChoiceActionResult, ChoiceFieldRef, RemoveChoicesParams, RemoveChoicesResult, ChoiceRemovalResult, DictionaryRecord, UpdateSetRecord, LayoutAction, LayoutRecordResult, LayoutResult, CreateViewParams, CreateViewResult, FormSectionSpec, SetFormLayoutParams, SetListLayoutParams, SetRelatedListsParams, ChunkRole, ChunkInfo, ChunkResult, PrunedResult, HostAssetsParams, HostAssetsResult, } from "./types";
24
- export { createTable, projectTableGraph, buildColumnXml, normalizeColumns, resolveType, applyTableSaveOverlay, defaultAccessFlags, TYPE_MAP, DEFAULT_SUPER_CLASS, DEFAULT_SAVE_ACTION, addColumn, deriveElement, ensureDesignAccess, findDesignAccess, DESIGN_ACCESS_TABLE, addIndex, parseIndexColumns, indexMatchesColumns, listIndexes, createIndex, validateCreateIndex, DEFAULT_INDEX_FORM_PATH, NOT_IN_UPDATE_SET, setColumn, resolveAttributes, toStoredValue, setTable, resolveTableAttributes, } from "./table";
24
+ export { createTable, projectTableGraph, buildColumnXml, normalizeColumns, resolveType, applyTableSaveOverlay, defaultAccessFlags, TYPE_MAP, DEFAULT_SUPER_CLASS, DEFAULT_SAVE_ACTION, addColumn, deriveElement, ensureDesignAccess, findDesignAccess, DESIGN_ACCESS_TABLE, addIndex, parseIndexColumns, indexMatchesColumns, listIndexes, createIndex, validateCreateIndex, captureRowName, parseAjaxAnswer, parseCanCreate, buildCanCreateFields, buildCreateScheduleFields, XMLHTTP_PATH, DEFAULT_ACCESS_METHOD, CAPTURE_NOTE, setColumn, resolveAttributes, toStoredValue, setTable, resolveTableAttributes, } from "./table";
25
25
  export type { CreateTableParams, CreateTableResult, AddColumnParams, AddColumnResult, DesignAccessFlag, EnsureDesignAccessParams, EnsureDesignAccessResult, AddIndexParams, AddIndexResult, AddIndexVerification, ListIndexesParams, ListIndexesResult, TableIndex, CreateIndexParams, CreateIndexResult, SetColumnParams, SetColumnResult, ColumnAttributes, AttributeChange, SetTableParams, SetTableResult, TableAttributes, TableGraph, NormalizedColumn, ColumnSpec, AccessFlags, OverlaySpec, } from "./table";
26
26
  export { setField } from "./setField";
27
27
  export { createRecord } from "./createRecord";
package/dist/index.js CHANGED
@@ -7,8 +7,9 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.editFlow = exports.buildPublishModel = exports.createFlow = exports.copyFlow = exports.publishFlow = exports.readActionType = exports.readFlow = exports.formatStepPill = exports.summarizeSteps = exports.verifySteps = exports.applyStepOps = exports.validateDefineSpec = exports.planActionDefinition = exports.defineActionType = exports.editActionType = exports.publishActionType = exports.triggerPublication = exports.findStepInput = exports.fetchActionSteps = exports.slugInternalName = exports.resolveScope = exports.cloneActionType = exports.cloneSubflow = exports.verifyArtifact = exports.listTemplates = exports.sincPlugin = exports.formatCreateViewResult = exports.formatLayoutResult = exports.setRelatedLists = exports.setFormLayout = exports.setListLayout = exports.createView = exports.formatRemoveChoicesResult = exports.formatAddChoicesResult = exports.formatHostAssetsResult = exports.classifyChunks = exports.hostAssets = exports.ChoiceWriteError = exports.removeChoicesFromField = exports.addChoicesToField = exports.resolveConfigFromEnvFile = exports.createClientFromEnvFile = exports.assertWriteAllowed = exports.evaluateWriteGate = exports.isCiEnvironment = exports.resolveExecutionContext = exports.PROCESSFLOW_PATH_PREFIX = exports.isProcessflowPath = exports.resolveFlowAuth = exports.createClient = void 0;
10
- exports.parsePublishTargets = exports.describeTarget = exports.resolveUpdateSetNaming = exports.buildPublishToUpdateSetFields = exports.buildCreateUpdateSetFields = exports.buildStartFields = exports.publishApp = exports.INVOKE_REST_METHODS = exports.invokeRest = exports.verifyReadBack = exports.readBackMatches = exports.hasHtmlEntity = exports.decodeHtmlEntities = exports.deleteRecord = exports.createRecord = exports.setField = exports.resolveTableAttributes = exports.setTable = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.NOT_IN_UPDATE_SET = exports.DEFAULT_INDEX_FORM_PATH = exports.validateCreateIndex = exports.createIndex = exports.listIndexes = exports.indexMatchesColumns = exports.parseIndexColumns = exports.addIndex = exports.DESIGN_ACCESS_TABLE = exports.findDesignAccess = exports.ensureDesignAccess = exports.deriveElement = exports.addColumn = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.TYPE_MAP = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.buildColumnXml = exports.projectTableGraph = exports.createTable = exports.WriteOrderError = exports.executeWritePlan = exports.topoSort = exports.generateSysId = exports.DEFAULT_RUN_FLOW_PATH = exports.testFlow = void 0;
11
- exports.SECRET_INTERNAL_TYPES = exports.SENTINEL = exports.isCapturable = exports.secretFieldsFromDictionary = exports.mergeSecretRules = exports.loadSecretRules = exports.defaultSecretRules = exports.escapeRegExp = exports.encodeXmlText = exports.encodeXmlEntities = exports.stripJsonValue = exports.stripField = exports.plannedStrips = exports.recordFieldNames = exports.readRecordTable = exports.readField = exports.verifyStripped = exports.stripSecrets = exports.DEFAULT_EXPORT_APP_TIMEOUT_MS = exports.buildPublishFields = exports.buildCreateSetFields = exports.exportApp = exports.DEFAULT_MAX_ROWS = exports.MAX_PAGE_SIZE = exports.DEFAULT_PAGE_SIZE = exports.UPDATE_XML_FIELDS = exports.xmlEscape = exports.formatUnloadDate = exports.parseStatsCount = exports.refreshTypeFields = exports.fetchUpdateXmlRows = exports.countUpdateXml = exports.countUnloadRecords = exports.renderUnload = exports.renderRemoteUpdateSet = exports.renderUpdateXmlRow = exports.exportUpdateSet = exports.PUBLISH_POLL_DELAYS_MS = exports.DEFAULT_PUBLISH_TIMEOUT_MS = exports.parseCicdProgress = exports.parseCicdPublishResponse = exports.harvestProgressResults = exports.flattenSteps = exports.classifyProgress = exports.parseProgressTree = exports.parseXmlAnswer = exports.PUBLISH_TARGETS = void 0;
10
+ exports.publishApp = exports.INVOKE_REST_METHODS = exports.invokeRest = exports.verifyReadBack = exports.readBackMatches = exports.hasHtmlEntity = exports.decodeHtmlEntities = exports.deleteRecord = exports.createRecord = exports.setField = exports.resolveTableAttributes = exports.setTable = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.CAPTURE_NOTE = exports.DEFAULT_ACCESS_METHOD = exports.XMLHTTP_PATH = exports.buildCreateScheduleFields = exports.buildCanCreateFields = exports.parseCanCreate = exports.parseAjaxAnswer = exports.captureRowName = exports.validateCreateIndex = exports.createIndex = exports.listIndexes = exports.indexMatchesColumns = exports.parseIndexColumns = exports.addIndex = exports.DESIGN_ACCESS_TABLE = exports.findDesignAccess = exports.ensureDesignAccess = exports.deriveElement = exports.addColumn = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.TYPE_MAP = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.buildColumnXml = exports.projectTableGraph = exports.createTable = exports.WriteOrderError = exports.executeWritePlan = exports.topoSort = exports.generateSysId = exports.DEFAULT_RUN_FLOW_PATH = exports.testFlow = void 0;
11
+ exports.secretFieldsFromDictionary = exports.mergeSecretRules = exports.loadSecretRules = exports.defaultSecretRules = exports.escapeRegExp = exports.encodeXmlText = exports.encodeXmlEntities = exports.stripJsonValue = exports.stripField = exports.plannedStrips = exports.recordFieldNames = exports.readRecordTable = exports.readField = exports.verifyStripped = exports.stripSecrets = exports.DEFAULT_EXPORT_APP_TIMEOUT_MS = exports.buildPublishFields = exports.buildCreateSetFields = exports.exportApp = exports.DEFAULT_MAX_ROWS = exports.MAX_PAGE_SIZE = exports.DEFAULT_PAGE_SIZE = exports.UPDATE_XML_FIELDS = exports.xmlEscape = exports.formatUnloadDate = exports.parseStatsCount = exports.refreshTypeFields = exports.fetchUpdateXmlRows = exports.countUpdateXml = exports.countUnloadRecords = exports.renderUnload = exports.renderRemoteUpdateSet = exports.renderUpdateXmlRow = exports.exportUpdateSet = exports.PUBLISH_POLL_DELAYS_MS = exports.DEFAULT_PUBLISH_TIMEOUT_MS = exports.parseCicdProgress = exports.parseCicdPublishResponse = exports.harvestProgressResults = exports.flattenSteps = exports.classifyProgress = exports.parseProgressTree = exports.parseXmlAnswer = exports.PUBLISH_TARGETS = exports.parsePublishTargets = exports.describeTarget = exports.resolveUpdateSetNaming = exports.buildPublishToUpdateSetFields = exports.buildCreateUpdateSetFields = exports.buildStartFields = void 0;
12
+ exports.SECRET_INTERNAL_TYPES = exports.SENTINEL = exports.isCapturable = void 0;
12
13
  var client_1 = require("./client");
13
14
  Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } });
14
15
  Object.defineProperty(exports, "resolveFlowAuth", { enumerable: true, get: function () { return client_1.resolveFlowAuth; } });
@@ -100,8 +101,14 @@ Object.defineProperty(exports, "indexMatchesColumns", { enumerable: true, get: f
100
101
  Object.defineProperty(exports, "listIndexes", { enumerable: true, get: function () { return table_1.listIndexes; } });
101
102
  Object.defineProperty(exports, "createIndex", { enumerable: true, get: function () { return table_1.createIndex; } });
102
103
  Object.defineProperty(exports, "validateCreateIndex", { enumerable: true, get: function () { return table_1.validateCreateIndex; } });
103
- Object.defineProperty(exports, "DEFAULT_INDEX_FORM_PATH", { enumerable: true, get: function () { return table_1.DEFAULT_INDEX_FORM_PATH; } });
104
- Object.defineProperty(exports, "NOT_IN_UPDATE_SET", { enumerable: true, get: function () { return table_1.NOT_IN_UPDATE_SET; } });
104
+ Object.defineProperty(exports, "captureRowName", { enumerable: true, get: function () { return table_1.captureRowName; } });
105
+ Object.defineProperty(exports, "parseAjaxAnswer", { enumerable: true, get: function () { return table_1.parseAjaxAnswer; } });
106
+ Object.defineProperty(exports, "parseCanCreate", { enumerable: true, get: function () { return table_1.parseCanCreate; } });
107
+ Object.defineProperty(exports, "buildCanCreateFields", { enumerable: true, get: function () { return table_1.buildCanCreateFields; } });
108
+ Object.defineProperty(exports, "buildCreateScheduleFields", { enumerable: true, get: function () { return table_1.buildCreateScheduleFields; } });
109
+ Object.defineProperty(exports, "XMLHTTP_PATH", { enumerable: true, get: function () { return table_1.XMLHTTP_PATH; } });
110
+ Object.defineProperty(exports, "DEFAULT_ACCESS_METHOD", { enumerable: true, get: function () { return table_1.DEFAULT_ACCESS_METHOD; } });
111
+ Object.defineProperty(exports, "CAPTURE_NOTE", { enumerable: true, get: function () { return table_1.CAPTURE_NOTE; } });
105
112
  Object.defineProperty(exports, "setColumn", { enumerable: true, get: function () { return table_1.setColumn; } });
106
113
  Object.defineProperty(exports, "resolveAttributes", { enumerable: true, get: function () { return table_1.resolveAttributes; } });
107
114
  Object.defineProperty(exports, "toStoredValue", { enumerable: true, get: function () { return table_1.toStoredValue; } });
@@ -575,7 +575,7 @@ function buildDescriptors(deps = {}) {
575
575
  "admin (an ACL that refuses GET refuses POST), and sys_index_column does not exist at " +
576
576
  "all (HTTP 400 'Invalid table') — there is no two-table index model to join. Each row " +
577
577
  "returns { name, columns, type, rawColumns }: `columns` is v_db_index's bracketed " +
578
- "`column_names` cell ('[phone]', '[a,b]') PARSED into a list, never substring-matched, " +
578
+ "`column_names` cell ('[phone]', '[a;b]' — semicolon-separated when composite) PARSED into a list, never substring-matched, " +
579
579
  "and `type` is access_method (btree for essentially everything). UNIQUENESS IS NOT " +
580
580
  "READABLE: v_db_index carries no uniqueness field, so a unique index and an ordinary " +
581
581
  "one are indistinguishable in it — `unique` is therefore left ABSENT rather than " +
@@ -592,28 +592,38 @@ function buildDescriptors(deps = {}) {
592
592
  name: "index_create",
593
593
  annotations: dovetail_mcp_kit_1.WRITE_ADDITIVE_IDEMPOTENT,
594
594
  description: "Create a DATABASE INDEX on an EXISTING ServiceNow table — including the COMPOSITE and " +
595
- "NON-UNIQUE indexes add_index cannot build. A DATABASE INDEX IS A PHYSICAL, PER-INSTANCE " +
596
- "CHANGE: IT IS NOT CAPTURED IN AN UPDATE SET AND DOES NOT TRAVEL WITH A PROMOTION, so it " +
597
- "must be re-run against every environment that needs it — which is why this tool takes " +
598
- "no updateSetSysId. There is no record path to an index (sys_index is API-level-ACL 403, " +
599
- "sys_index_column does not exist, and sys_dictionary.unique — the add_index lever — is " +
600
- "per-column and unique-only), so this replays the platform's own index-creator form " +
601
- "(sys_action=create_index) over a form-login session. DRY-RUN BY DEFAULT: without " +
602
- "confirm:true NOTHING is sent and nothing is read; dryRun:true forces a dry-run even " +
603
- "with confirm. IDEMPOTENT: on the live path v_db_index is read first, and an index over " +
604
- "exactly these columns returns 'already-exists' with no write. `columns` is ORDERED — " +
605
- "order is part of an index's identity and is preserved verbatim. `name` is REFUSED: the " +
606
- "platform's form has no name input (ServiceNow names the index itself), so accepting one " +
607
- "would mean reporting a name the instance does not carry — the created index's real name " +
608
- "comes back in `name`. After the POST the index is polled for in v_db_index (default 10 " +
609
- "checks, 3s apart, since a build on a populated table is asynchronous); if it never " +
610
- "appears the result is 'failed', because a form processor returning a page is not " +
611
- "evidence an ALTER ran — and a unique index cannot build over duplicate values, EMPTY " +
612
- "included. verified:true means a matching row was READ BACK. 'uniqueness-enforced' is " +
613
- "ALWAYS in unverified: v_db_index has no uniqueness field, so enforcement is provable " +
614
- "only by a duplicate-insert test. Requires a username+password identity that can form-" +
615
- "log-in; an instance on API-key-only auth or SSO/MFA will fail at the session with a " +
616
- "diagnosis, because no .do replay can work there.",
595
+ "NON-UNIQUE indexes add_index cannot build. AN INDEX IS CAPTURED IN AN UPDATE SET: the " +
596
+ "platform's build job writes a sys_update_xml row (type=Indexes, name " +
597
+ "sys_index_<table>_<col>_…) into the session user's CURRENT update set, so " +
598
+ "updateSetSysId is REQUIRED on the live path — the tool pins that set as current " +
599
+ "(Dovetail changeUpdateSet, read back) BEFORE scheduling, refuses a set outside the " +
600
+ "table's application scope or not 'in progress', and reads the capture row back " +
601
+ "from that set afterwards. The physical index is still built per instance; committing " +
602
+ "the set elsewhere rebuilds it. There is no record path to an index (sys_index is " +
603
+ "API-level-ACL 403, sys_index_column does not exist, and sys_dictionary.unique — the " +
604
+ "add_index lever — is per-column and unique-only), so this replays the two GlideAjax " +
605
+ "calls the platform's own Database Indexes dialog makes on xmlhttp.do over a " +
606
+ "form-login session: IndexCreatorErrorChecker.canCreate (the dialog's pre-flight; a " +
607
+ "canCreate:false verdict is returned verbatim with its errorCode and nothing is " +
608
+ "scheduled) then ScheduleCreator.createSchedule (sysparm_table, sysparm_fields, " +
609
+ "sysparm_access_method — default btree — sysparm_unique true|false, no email, no " +
610
+ "name). DRY-RUN BY DEFAULT: without confirm:true NOTHING is sent and nothing is read; " +
611
+ "dryRun:true forces a dry-run even with confirm. IDEMPOTENT: on the live path " +
612
+ "v_db_index is read first, and an index over exactly these columns returns " +
613
+ "'already-exists' with no write. `columns` is ORDERED — order is part of an " +
614
+ "index's identity and is preserved verbatim. `name` is REFUSED: the dialog has no " +
615
+ "name input (ServiceNow names the index after its leading column), so accepting one " +
616
+ "would mean reporting a name the instance does not carry — the real name comes back " +
617
+ "in `name`. After scheduling, the index is polled for in v_db_index (default 10 " +
618
+ "checks, 3s apart); if it never appears the result is 'failed', because an accepted " +
619
+ "schedule is not evidence an ALTER ran — and a unique index cannot build over " +
620
+ "duplicate values, EMPTY included. verified:true means a matching row was READ BACK; " +
621
+ "captured:true means the sys_update_xml row was READ BACK from the pinned set (an " +
622
+ "index that exists but was not captured is created:true, captured:false with " +
623
+ "'update-set-capture' in unverified). 'uniqueness-enforced' is ALWAYS in " +
624
+ "unverified: v_db_index has no uniqueness field. Requires a username+password identity " +
625
+ "that can form-log-in; xmlhttp.do ignores Basic auth and API keys, so an API-key-only " +
626
+ "or SSO/MFA identity fails at the session with a diagnosis (TenonHQ/Dovetail#292).",
617
627
  shape: schemas_1.createIndexSchema.shape,
618
628
  handler: async function (args) {
619
629
  var p = schemas_1.createIndexSchema.parse(args);
@@ -621,6 +631,7 @@ function buildDescriptors(deps = {}) {
621
631
  client: client(),
622
632
  table: p.table,
623
633
  columns: p.columns,
634
+ updateSetSysId: p.updateSetSysId,
624
635
  unique: p.unique,
625
636
  name: p.name,
626
637
  accessMethod: p.accessMethod,
@@ -1983,6 +1983,7 @@ export declare var listIndexesSchema: z.ZodObject<{
1983
1983
  export declare var createIndexSchema: z.ZodObject<{
1984
1984
  table: z.ZodString;
1985
1985
  columns: z.ZodArray<z.ZodString, "many">;
1986
+ updateSetSysId: z.ZodOptional<z.ZodString>;
1986
1987
  unique: z.ZodOptional<z.ZodBoolean>;
1987
1988
  name: z.ZodOptional<z.ZodString>;
1988
1989
  accessMethod: z.ZodOptional<z.ZodString>;
@@ -1998,6 +1999,7 @@ export declare var createIndexSchema: z.ZodObject<{
1998
1999
  confirm?: boolean | undefined;
1999
2000
  dryRun?: boolean | undefined;
2000
2001
  debug?: boolean | undefined;
2002
+ updateSetSysId?: string | undefined;
2001
2003
  unique?: boolean | undefined;
2002
2004
  accessMethod?: string | undefined;
2003
2005
  pollAttempts?: number | undefined;
@@ -2009,6 +2011,7 @@ export declare var createIndexSchema: z.ZodObject<{
2009
2011
  confirm?: boolean | undefined;
2010
2012
  dryRun?: boolean | undefined;
2011
2013
  debug?: boolean | undefined;
2014
+ updateSetSysId?: string | undefined;
2012
2015
  unique?: boolean | undefined;
2013
2016
  accessMethod?: string | undefined;
2014
2017
  pollAttempts?: number | undefined;
@@ -413,11 +413,13 @@ exports.listIndexesSchema = zod_1.z.object({
413
413
  // earns the explanation that the platform's index-creator form has no name input (the
414
414
  // same contract set-column uses for internalType/element); createIndex refuses it.
415
415
  // confirm is the write gate: without confirm:true the tool is a pure dry-run that sends
416
- // nothing and reads nothing. There is deliberately NO updateSetSysId — a database index is
417
- // physical and is not captured in an update set.
416
+ // nothing and reads nothing. updateSetSysId is optional here because a dry-run needs none;
417
+ // the live path requires it (createIndex refuses without one) because the build job
418
+ // captures the index definition into the user's CURRENT update set.
418
419
  exports.createIndexSchema = zod_1.z.object({
419
420
  table: zod_1.z.string().min(1),
420
421
  columns: zod_1.z.array(zod_1.z.string().min(1)).min(1),
422
+ updateSetSysId: zod_1.z.string().min(1).optional(),
421
423
  unique: zod_1.z.boolean().optional(),
422
424
  name: zod_1.z.string().optional(),
423
425
  accessMethod: zod_1.z.string().min(1).optional(),
@@ -119,7 +119,13 @@ export interface DuplicateScan {
119
119
  * `client.table.query` exposes no sysparm_offset.
120
120
  */
121
121
  export declare function scanForDuplicates(client: ServiceNowClient, table: string, column: string): Promise<DuplicateScan>;
122
- /** Parse a v_db_index `column_names` cell ("[a]", "[a,b]") into its column list. */
122
+ /**
123
+ * Parse a v_db_index `column_names` cell into its column list. The live view
124
+ * separates a composite index's columns with a SEMICOLON — "[a;b;c]" (read live
125
+ * from tenonworkstudio 2026-10-08: "[sys_created_on;status;version_step;version]").
126
+ * A comma is tolerated too, so an older fixture or a differently-rendered cell
127
+ * still parses; a cell that is one bare name ("[a]") is a one-column list.
128
+ */
123
129
  export declare function parseIndexColumns(raw: string): Array<string>;
124
130
  /**
125
131
  * Does this row's column list match the requested one EXACTLY? Parsed, never
@@ -224,7 +224,13 @@ function describeDuplicates(scan) {
224
224
  shown.join(", ") +
225
225
  (scan.duplicates.length > shown.length ? ", ..." : ""));
226
226
  }
227
- /** Parse a v_db_index `column_names` cell ("[a]", "[a,b]") into its column list. */
227
+ /**
228
+ * Parse a v_db_index `column_names` cell into its column list. The live view
229
+ * separates a composite index's columns with a SEMICOLON — "[a;b;c]" (read live
230
+ * from tenonworkstudio 2026-10-08: "[sys_created_on;status;version_step;version]").
231
+ * A comma is tolerated too, so an older fixture or a differently-rendered cell
232
+ * still parses; a cell that is one bare name ("[a]") is a one-column list.
233
+ */
228
234
  function parseIndexColumns(raw) {
229
235
  var text = String(raw === undefined || raw === null ? "" : raw).trim();
230
236
  if (text.length > 1 &&
@@ -235,7 +241,7 @@ function parseIndexColumns(raw) {
235
241
  if (!text)
236
242
  return [];
237
243
  return text
238
- .split(",")
244
+ .split(/[;,]/)
239
245
  .map(function (part) {
240
246
  return part.trim();
241
247
  })