@tenonhq/dovetail-servicenow 0.0.49 → 0.0.50

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
@@ -1154,7 +1154,14 @@ Claude Code and agents: `create_view`, `set_list_layout`, `set_form_layout`,
1154
1154
  `add_column` (same-scope by default; `crossScope: true` with `scope` adds a column OWNED
1155
1155
  by another app — a Journey field on an Automate table, element `<scope>_<name>` —
1156
1156
  guarded by `sys_db_object.alter_access` and the update set's scope, with the stored
1157
- element and `sys_scope` read back; the CLI flag is `--cross-scope`) /
1157
+ element and `sys_scope` read back; the CLI flag is `--cross-scope`. A cross-scope
1158
+ result also carries `designAccess`: the platform UI refuses cross-scope authoring
1159
+ without a `sys_scope_design_access` record (column scope -> table scope), so a missing
1160
+ one is FLAGGED — not blocking, the headless insert works without it — and
1161
+ `ensureDesignAccess: true` / `--ensure-design-access` creates it first in the same
1162
+ update set) / `design_access` (ensure that record on its own — idempotent, created in
1163
+ the SOURCE app's update set and read back; CLI `dove-sn design-access --source <app>
1164
+ --target <app> --update-set <sys_id>`, `--dry-run` only reports exists/missing) /
1158
1165
  `add_index` (a single-column unique index via `sys_dictionary.unique`,
1159
1166
  read back from the `v_db_index` view - uniqueness enforcement is always reported
1160
1167
  unverified) / `index_list` (read-only: a table's database indexes from `v_db_index`,
package/dist/cli.js CHANGED
@@ -1340,15 +1340,60 @@ async function runCreateTable(flags) {
1340
1340
  return 2;
1341
1341
  return 0;
1342
1342
  }
1343
+ /**
1344
+ * dove-sn design-access:
1345
+ * --source x_cadso_journey --target x_cadso_automate
1346
+ * [--update-set <sys_id in source>] [--dry-run] [--json]
1347
+ * Ensures the sys_scope_design_access record that lets the SOURCE app author in the
1348
+ * TARGET app's tables (required by the platform UI for cross-scope columns). Idempotent;
1349
+ * --dry-run only reports whether it exists. --update-set is required unless --dry-run.
1350
+ */
1351
+ async function runDesignAccess(flags) {
1352
+ if (!flags.source || !flags.target) {
1353
+ process.stderr.write("design-access: --source (authoring app) and --target (app that owns the tables) are required\n");
1354
+ return 1;
1355
+ }
1356
+ var isDryRun = flags["dry-run"] === "true";
1357
+ if (!isDryRun && !flags["update-set"]) {
1358
+ process.stderr.write("design-access: --update-set (an update set in --source) is required on the live path (only --dry-run works without one)\n");
1359
+ return 1;
1360
+ }
1361
+ var result = await (0, table_1.ensureDesignAccess)({
1362
+ client: (0, client_1.createClient)({}),
1363
+ sourceScope: flags.source,
1364
+ targetScope: flags.target,
1365
+ updateSetSysId: flags["update-set"],
1366
+ dryRun: isDryRun,
1367
+ });
1368
+ if (flags.json === "true") {
1369
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
1370
+ }
1371
+ else {
1372
+ process.stdout.write("[" +
1373
+ result.status +
1374
+ "] " +
1375
+ result.sourceScope +
1376
+ " -> " +
1377
+ result.targetScope +
1378
+ (result.sysId ? " (" + result.sysId + ")" : "") +
1379
+ "\n" +
1380
+ result.note +
1381
+ "\n");
1382
+ }
1383
+ if (result.status === "failed")
1384
+ return 2;
1385
+ return 0;
1386
+ }
1343
1387
  /**
1344
1388
  * dove-sn add-column:
1345
1389
  * --table x_cadso_journey --label URL --type url
1346
1390
  * [--name url] [--max-length 1024] [--reference <table>]
1347
1391
  * [--mandatory] [--default <value>] [--dependent-on-field <element>]
1348
- * [--scope x_cadso_journey] [--cross-scope] [--update-set <sys_id>]
1349
- * [--from-json <spec.json>] [--dry-run] [--debug] [--json]
1392
+ * [--scope x_cadso_journey] [--cross-scope] [--ensure-design-access]
1393
+ * [--update-set <sys_id>] [--from-json <spec.json>] [--dry-run] [--debug] [--json]
1350
1394
  * --update-set is required unless --dry-run. --cross-scope opts in to a column
1351
- * OWNED by --scope when that differs from the table's scope.
1395
+ * OWNED by --scope when that differs from the table's scope; a missing Design Access
1396
+ * record is flagged, and --ensure-design-access creates it first.
1352
1397
  */
1353
1398
  async function runAddColumn(flags) {
1354
1399
  var spec = {};
@@ -1388,6 +1433,10 @@ async function runAddColumn(flags) {
1388
1433
  if (flags["cross-scope"] === "true" || spec.crossScope === true) {
1389
1434
  params.crossScope = true;
1390
1435
  }
1436
+ if (flags["ensure-design-access"] === "true" ||
1437
+ spec.ensureDesignAccess === true) {
1438
+ params.ensureDesignAccess = true;
1439
+ }
1391
1440
  var us = flags["update-set"] || spec.updateSetSysId;
1392
1441
  if (us)
1393
1442
  params.updateSetSysId = us;
@@ -2616,6 +2665,9 @@ async function dispatch(parsed) {
2616
2665
  if (parsed.command === "add-column") {
2617
2666
  return await runAddColumn(parsed.flags);
2618
2667
  }
2668
+ if (parsed.command === "design-access") {
2669
+ return await runDesignAccess(parsed.flags);
2670
+ }
2619
2671
  if (parsed.command === "add-index") {
2620
2672
  return await runAddIndex(parsed.flags);
2621
2673
  }
package/dist/cliUsage.js CHANGED
@@ -209,6 +209,10 @@ exports.VERB_USAGE = {
209
209
  flag: "cross-scope",
210
210
  note: "Opt in to a column OWNED by --scope, which differs from the table's scope (element becomes <scope>_<name>; the table must allow new fields; --update-set must be in --scope).",
211
211
  },
212
+ {
213
+ flag: "ensure-design-access",
214
+ note: "Cross-scope only: create the missing sys_scope_design_access record (--scope -> table's scope) first, in the same update set. Without it a missing record is only flagged.",
215
+ },
212
216
  { flag: "update-set", value: "<sys_id>", note: "REQUIRED on the live path (only --dry-run works without one)." },
213
217
  DRY_RUN_FLAG,
214
218
  DEBUG_FLAG,
@@ -222,6 +226,26 @@ exports.VERB_USAGE = {
222
226
  "Cross-scope: dove-sn add-column --table x_cadso_automate_email_batch --label 'Instance Step' --name instance_step --type reference --reference x_cadso_journey_instance_step --scope x_cadso_journey --cross-scope --update-set <journey set>",
223
227
  ],
224
228
  },
229
+ "design-access": {
230
+ summary: "Ensure the sys_scope_design_access record that lets one app author in another app's tables (required by the UI for cross-scope columns)",
231
+ required: [
232
+ { flag: "source", value: "<x_scope>", note: "The AUTHORING app (owns the record and its update set)." },
233
+ { flag: "target", value: "<x_scope>", note: "The app that OWNS the tables." },
234
+ ],
235
+ optional: [
236
+ { flag: "update-set", value: "<sys_id>", note: "Update set in --source. REQUIRED on the live path." },
237
+ DRY_RUN_FLAG,
238
+ JSON_FLAG,
239
+ ],
240
+ gate: "dry-run-flag",
241
+ gateNote: "--update-set is required unless --dry-run; --dry-run reports exists/missing only.",
242
+ example: "dove-sn design-access --source x_cadso_journey --target x_cadso_automate --update-set <journey set>",
243
+ notes: [
244
+ "Idempotent: an existing record is reported and nothing is written.",
245
+ "Symptom it fixes: \"Invalid 'Table' selected on the Dictionary Entry record ... can only select '<app>' tables with read access enabled\" — the table's own access flags are NOT the gate.",
246
+ "Exit 2 when the record could not be created or verified on read-back.",
247
+ ],
248
+ },
225
249
  "set-column": {
226
250
  summary: "Update an EXISTING column's schema (label/mandatory/default/read-only/max-length/dependent-on-field), then verify",
227
251
  required: [
package/dist/index.d.ts CHANGED
@@ -21,8 +21,8 @@ export { sincPlugin } from "./plugin";
21
21
  export { listTemplates, verifyArtifact, cloneSubflow, cloneActionType, resolveScope, slugInternalName, fetchActionSteps, findStepInput, triggerPublication, publishActionType, editActionType, defineActionType, planActionDefinition, validateDefineSpec, applyStepOps, verifySteps, summarizeSteps, formatStepPill, readFlow, readActionType, publishFlow, copyFlow, createFlow, buildPublishModel, editFlow, testFlow, DEFAULT_RUN_FLOW_PATH, generateSysId, topoSort, executeWritePlan, WriteOrderError, } from "./flowDesigner";
22
22
  export type { TemplateRef, ListTemplatesParams, FlowKind, VerifyExpect, VerifyFound, VerifyFailure, VerifyReport, VerifyArtifactParams, CloneSubflowParams, CloneSubflowResult, CloneActionTypeParams, CloneActionTypeResult, CloneActionTypePlan, CloneActionTypeStepReport, TriggerPublicationParams, TriggerPublicationResult, PublishActionTypeParams, PublishActionTypeResult, EditActionTypeParams, EditActionTypeResult, EditActionTypeOps, DefineActionSpec, DefineActionInputSpec, DefineActionOutputSpec, DefineStepSpec, DefineStepVarSpec, DefineStepOutputSpec, DefineStepValue, DefineActionTypeParams, DefineActionTypeResult, DefineActionDiff, DefineActionPlan, PlanActionDefinitionParams, ActionView, StepView, StepOps, StepRecord, StepSummary, StepIoSummary, PatchStepScriptOp, SetStepInputOp, AddStepOutputOp, AddStepInputOp, ApplyStepOpsResult, VerifyStepsResult, ReadFlowParams, ReadFlowResult, FlowStep, FlowVariable, ReadActionTypeParams, ReadActionTypeResult, ActionIo, PublishFlowParams, PublishFlowResult, CopyFlowParams, CopyFlowResult, CreateFlowParams, CreateFlowResult, EditFlowParams, EditFlowResult, EditFlowOps, StepInputPatch, TestFlowParams, TestFlowResult, WriteOp, WriteOpResult, } from "./flowDesigner";
23
23
  export type { ServiceNowClientConfig, ChoiceValue, ChoiceType, AddChoicesParams, AddChoicesResult, ChoiceActionResult, ChoiceFieldRef, RemoveChoicesParams, RemoveChoicesResult, ChoiceRemovalResult, DictionaryRecord, UpdateSetRecord, LayoutAction, LayoutRecordResult, LayoutResult, CreateViewParams, CreateViewResult, FormSectionSpec, SetFormLayoutParams, SetListLayoutParams, SetRelatedListsParams, ChunkRole, ChunkInfo, ChunkResult, PrunedResult, HostAssetsParams, HostAssetsResult, } from "./types";
24
- export { createTable, projectTableGraph, buildColumnXml, normalizeColumns, resolveType, applyTableSaveOverlay, defaultAccessFlags, TYPE_MAP, DEFAULT_SUPER_CLASS, DEFAULT_SAVE_ACTION, addColumn, deriveElement, addIndex, parseIndexColumns, indexMatchesColumns, listIndexes, createIndex, validateCreateIndex, DEFAULT_INDEX_FORM_PATH, NOT_IN_UPDATE_SET, setColumn, resolveAttributes, toStoredValue, setTable, resolveTableAttributes, } from "./table";
25
- export type { CreateTableParams, CreateTableResult, AddColumnParams, AddColumnResult, AddIndexParams, AddIndexResult, AddIndexVerification, ListIndexesParams, ListIndexesResult, TableIndex, CreateIndexParams, CreateIndexResult, SetColumnParams, SetColumnResult, ColumnAttributes, AttributeChange, SetTableParams, SetTableResult, TableAttributes, TableGraph, NormalizedColumn, ColumnSpec, AccessFlags, OverlaySpec, } from "./table";
24
+ export { createTable, projectTableGraph, buildColumnXml, normalizeColumns, resolveType, applyTableSaveOverlay, defaultAccessFlags, TYPE_MAP, DEFAULT_SUPER_CLASS, DEFAULT_SAVE_ACTION, addColumn, deriveElement, ensureDesignAccess, findDesignAccess, DESIGN_ACCESS_TABLE, addIndex, parseIndexColumns, indexMatchesColumns, listIndexes, createIndex, validateCreateIndex, DEFAULT_INDEX_FORM_PATH, NOT_IN_UPDATE_SET, setColumn, resolveAttributes, toStoredValue, setTable, resolveTableAttributes, } from "./table";
25
+ export type { CreateTableParams, CreateTableResult, AddColumnParams, AddColumnResult, DesignAccessFlag, EnsureDesignAccessParams, EnsureDesignAccessResult, AddIndexParams, AddIndexResult, AddIndexVerification, ListIndexesParams, ListIndexesResult, TableIndex, CreateIndexParams, CreateIndexResult, SetColumnParams, SetColumnResult, ColumnAttributes, AttributeChange, SetTableParams, SetTableResult, TableAttributes, TableGraph, NormalizedColumn, ColumnSpec, AccessFlags, OverlaySpec, } from "./table";
26
26
  export { setField } from "./setField";
27
27
  export { createRecord } from "./createRecord";
28
28
  export type { RecordWriteResult, SetFieldParams, SetFieldResult, } from "./setField";
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.editFlow = exports.buildPublishModel = exports.createFlow = exports.copyFlow = exports.publishFlow = exports.readActionType = exports.readFlow = exports.formatStepPill = exports.summarizeSteps = exports.verifySteps = exports.applyStepOps = exports.validateDefineSpec = exports.planActionDefinition = exports.defineActionType = exports.editActionType = exports.publishActionType = exports.triggerPublication = exports.findStepInput = exports.fetchActionSteps = exports.slugInternalName = exports.resolveScope = exports.cloneActionType = exports.cloneSubflow = exports.verifyArtifact = exports.listTemplates = exports.sincPlugin = exports.formatCreateViewResult = exports.formatLayoutResult = exports.setRelatedLists = exports.setFormLayout = exports.setListLayout = exports.createView = exports.formatRemoveChoicesResult = exports.formatAddChoicesResult = exports.formatHostAssetsResult = exports.classifyChunks = exports.hostAssets = exports.ChoiceWriteError = exports.removeChoicesFromField = exports.addChoicesToField = exports.resolveConfigFromEnvFile = exports.createClientFromEnvFile = exports.assertWriteAllowed = exports.evaluateWriteGate = exports.isCiEnvironment = exports.resolveExecutionContext = exports.PROCESSFLOW_PATH_PREFIX = exports.isProcessflowPath = exports.resolveFlowAuth = exports.createClient = void 0;
10
- exports.parseProgressTree = exports.parseXmlAnswer = exports.PUBLISH_TARGETS = exports.parsePublishTargets = exports.describeTarget = exports.resolveUpdateSetNaming = exports.buildPublishToUpdateSetFields = exports.buildCreateUpdateSetFields = exports.buildStartFields = exports.publishApp = exports.INVOKE_REST_METHODS = exports.invokeRest = exports.verifyReadBack = exports.readBackMatches = exports.hasHtmlEntity = exports.decodeHtmlEntities = exports.deleteRecord = exports.createRecord = exports.setField = exports.resolveTableAttributes = exports.setTable = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.NOT_IN_UPDATE_SET = exports.DEFAULT_INDEX_FORM_PATH = exports.validateCreateIndex = exports.createIndex = exports.listIndexes = exports.indexMatchesColumns = exports.parseIndexColumns = exports.addIndex = exports.deriveElement = exports.addColumn = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.TYPE_MAP = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.buildColumnXml = exports.projectTableGraph = exports.createTable = exports.WriteOrderError = exports.executeWritePlan = exports.topoSort = exports.generateSysId = exports.DEFAULT_RUN_FLOW_PATH = exports.testFlow = void 0;
11
- exports.SECRET_INTERNAL_TYPES = exports.SENTINEL = exports.isCapturable = exports.secretFieldsFromDictionary = exports.mergeSecretRules = exports.loadSecretRules = exports.defaultSecretRules = exports.escapeRegExp = exports.encodeXmlText = exports.encodeXmlEntities = exports.stripJsonValue = exports.stripField = exports.plannedStrips = exports.recordFieldNames = exports.readRecordTable = exports.readField = exports.verifyStripped = exports.stripSecrets = exports.DEFAULT_EXPORT_APP_TIMEOUT_MS = exports.buildPublishFields = exports.buildCreateSetFields = exports.exportApp = exports.DEFAULT_MAX_ROWS = exports.MAX_PAGE_SIZE = exports.DEFAULT_PAGE_SIZE = exports.UPDATE_XML_FIELDS = exports.xmlEscape = exports.formatUnloadDate = exports.parseStatsCount = exports.refreshTypeFields = exports.fetchUpdateXmlRows = exports.countUpdateXml = exports.countUnloadRecords = exports.renderUnload = exports.renderRemoteUpdateSet = exports.renderUpdateXmlRow = exports.exportUpdateSet = exports.PUBLISH_POLL_DELAYS_MS = exports.DEFAULT_PUBLISH_TIMEOUT_MS = exports.parseCicdProgress = exports.parseCicdPublishResponse = exports.harvestProgressResults = exports.flattenSteps = exports.classifyProgress = void 0;
10
+ exports.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;
12
12
  var client_1 = require("./client");
13
13
  Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } });
14
14
  Object.defineProperty(exports, "resolveFlowAuth", { enumerable: true, get: function () { return client_1.resolveFlowAuth; } });
@@ -91,6 +91,9 @@ Object.defineProperty(exports, "DEFAULT_SUPER_CLASS", { enumerable: true, get: f
91
91
  Object.defineProperty(exports, "DEFAULT_SAVE_ACTION", { enumerable: true, get: function () { return table_1.DEFAULT_SAVE_ACTION; } });
92
92
  Object.defineProperty(exports, "addColumn", { enumerable: true, get: function () { return table_1.addColumn; } });
93
93
  Object.defineProperty(exports, "deriveElement", { enumerable: true, get: function () { return table_1.deriveElement; } });
94
+ Object.defineProperty(exports, "ensureDesignAccess", { enumerable: true, get: function () { return table_1.ensureDesignAccess; } });
95
+ Object.defineProperty(exports, "findDesignAccess", { enumerable: true, get: function () { return table_1.findDesignAccess; } });
96
+ Object.defineProperty(exports, "DESIGN_ACCESS_TABLE", { enumerable: true, get: function () { return table_1.DESIGN_ACCESS_TABLE; } });
94
97
  Object.defineProperty(exports, "addIndex", { enumerable: true, get: function () { return table_1.addIndex; } });
95
98
  Object.defineProperty(exports, "parseIndexColumns", { enumerable: true, get: function () { return table_1.parseIndexColumns; } });
96
99
  Object.defineProperty(exports, "indexMatchesColumns", { enumerable: true, get: function () { return table_1.indexMatchesColumns; } });
@@ -10,7 +10,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
10
  import { z } from "zod";
11
11
  import type { ToolAnnotations } from "@tenonhq/dovetail-mcp-kit";
12
12
  import type { ServiceNowClient } from "../client";
13
- export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "remove_choices_from_field", "flow_view", "action_view", "action_edit", "action_clone", "action_define", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "add_index", "index_list", "index_create", "set_column", "set_table", "set_field", "create_record", "delete_record", "host_assets", "invoke_rest", "app_publish", "update_set_export", "app_export"];
13
+ export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "remove_choices_from_field", "flow_view", "action_view", "action_edit", "action_clone", "action_define", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit", "create_table", "add_column", "design_access", "add_index", "index_list", "index_create", "set_column", "set_table", "set_field", "create_record", "delete_record", "host_assets", "invoke_rest", "app_publish", "update_set_export", "app_export"];
14
14
  export type ToolName = (typeof TOOL_NAMES)[number];
15
15
  export interface RegistryDeps {
16
16
  /** Optional client injection for tests; defaults to createClient({}). */
@@ -57,6 +57,7 @@ exports.TOOL_NAMES = [
57
57
  "flow_edit",
58
58
  "create_table",
59
59
  "add_column",
60
+ "design_access",
60
61
  "add_index",
61
62
  "index_list",
62
63
  "index_create",
@@ -445,7 +446,11 @@ function buildDescriptors(deps = {}) {
445
446
  "<scope>_<name>, the dictionary row and its update-set capture land in scope); the table " +
446
447
  "must allow new fields from other scopes (sys_db_object.alter_access) and updateSetSysId " +
447
448
  "must belong to scope — both checked on dryRun too, and the stored element + sys_scope are " +
448
- "read back. updateSetSysId is required on the live path. dryRun:true returns the plan with " +
449
+ "read back. A cross-scope result carries designAccess { present, sysId, created }: the " +
450
+ "platform UI refuses cross-scope authoring without a sys_scope_design_access record " +
451
+ "(scope -> the table's scope), so a missing one is FLAGGED (not blocking — the headless " +
452
+ "insert works without it); ensureDesignAccess:true creates it first in the same update " +
453
+ "set. updateSetSysId is required on the live path. dryRun:true returns the plan with " +
449
454
  "no writes.",
450
455
  shape: schemas_1.addColumnSchema.shape,
451
456
  handler: async function (args) {
@@ -465,12 +470,43 @@ function buildDescriptors(deps = {}) {
465
470
  column: p.column,
466
471
  scope: p.scope,
467
472
  crossScope: p.crossScope,
473
+ ensureDesignAccess: p.ensureDesignAccess,
468
474
  updateSetSysId: p.updateSetSysId,
469
475
  dryRun: p.dryRun,
470
476
  debug: p.debug,
471
477
  });
472
478
  },
473
479
  },
480
+ {
481
+ name: "design_access",
482
+ annotations: dovetail_mcp_kit_1.WRITE_CREATE,
483
+ description: "Ensure the sys_scope_design_access record that lets sourceScope (the AUTHORING app) " +
484
+ "design in targetScope's tables (the app that OWNS them). The platform UI requires it " +
485
+ "for cross-scope columns — the symptom is \"Invalid 'Table' selected on the Dictionary " +
486
+ "Entry record ... can only select '<app>' tables with read access enabled\", and the " +
487
+ "table's own access flags are NOT the gate. Scopes are names or sys_scope sys_ids. " +
488
+ "Idempotent: an existing record returns status 'exists' and nothing is written. Live " +
489
+ "creates it via the scope-aware createRecord op switched to sourceScope, captured in " +
490
+ "updateSetSysId (must belong to sourceScope), then reads it back and asserts " +
491
+ "source/target. updateSetSysId is required on the live path; dryRun:true only reports " +
492
+ "exists / missing.",
493
+ shape: schemas_1.designAccessSchema.shape,
494
+ handler: async function (args) {
495
+ var p = schemas_1.designAccessSchema.parse(args);
496
+ if (p.dryRun !== true &&
497
+ (!p.updateSetSysId || !p.updateSetSysId.trim())) {
498
+ throw new Error("design_access: updateSetSysId (an update set in sourceScope) is required on " +
499
+ "the live path — set dryRun:true to only check whether the record exists.");
500
+ }
501
+ return (0, table_1.ensureDesignAccess)({
502
+ client: client(),
503
+ sourceScope: p.sourceScope,
504
+ targetScope: p.targetScope,
505
+ updateSetSysId: p.updateSetSysId,
506
+ dryRun: p.dryRun,
507
+ });
508
+ },
509
+ },
474
510
  {
475
511
  name: "add_index",
476
512
  annotations: dovetail_mcp_kit_1.WRITE_OVERWRITE,
@@ -1891,6 +1891,7 @@ export declare var addColumnSchema: z.ZodObject<{
1891
1891
  }>;
1892
1892
  scope: z.ZodOptional<z.ZodString>;
1893
1893
  crossScope: z.ZodOptional<z.ZodBoolean>;
1894
+ ensureDesignAccess: z.ZodOptional<z.ZodBoolean>;
1894
1895
  updateSetSysId: z.ZodOptional<z.ZodString>;
1895
1896
  dryRun: z.ZodOptional<z.ZodBoolean>;
1896
1897
  debug: z.ZodOptional<z.ZodBoolean>;
@@ -1911,6 +1912,7 @@ export declare var addColumnSchema: z.ZodObject<{
1911
1912
  updateSetSysId?: string | undefined;
1912
1913
  scope?: string | undefined;
1913
1914
  crossScope?: boolean | undefined;
1915
+ ensureDesignAccess?: boolean | undefined;
1914
1916
  }, {
1915
1917
  table: string;
1916
1918
  column: {
@@ -1928,6 +1930,23 @@ export declare var addColumnSchema: z.ZodObject<{
1928
1930
  updateSetSysId?: string | undefined;
1929
1931
  scope?: string | undefined;
1930
1932
  crossScope?: boolean | undefined;
1933
+ ensureDesignAccess?: boolean | undefined;
1934
+ }>;
1935
+ export declare var designAccessSchema: z.ZodObject<{
1936
+ sourceScope: z.ZodString;
1937
+ targetScope: z.ZodString;
1938
+ updateSetSysId: z.ZodOptional<z.ZodString>;
1939
+ dryRun: z.ZodOptional<z.ZodBoolean>;
1940
+ }, "strip", z.ZodTypeAny, {
1941
+ sourceScope: string;
1942
+ targetScope: string;
1943
+ dryRun?: boolean | undefined;
1944
+ updateSetSysId?: string | undefined;
1945
+ }, {
1946
+ sourceScope: string;
1947
+ targetScope: string;
1948
+ dryRun?: boolean | undefined;
1949
+ updateSetSysId?: string | undefined;
1931
1950
  }>;
1932
1951
  export declare var addIndexSchema: z.ZodObject<{
1933
1952
  table: 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.exportAppSchema = exports.exportUpdateSetSchema = exports.deleteRecordSchema = exports.createRecordSchema = exports.setFieldSchema = exports.setTableSchema = exports.tableAttributesSchema = exports.setColumnSchema = exports.columnAttributesSchema = exports.createIndexSchema = exports.listIndexesSchema = exports.addIndexSchema = exports.addColumnSchema = exports.createTableSchema = exports.columnSpecSchema = exports.hostAssetsSchema = exports.editFlowSchema = exports.defineActionSchema = exports.defineActionSpecSchema = exports.cloneActionSchema = exports.editActionSchema = exports.stepInputPatchSchema = exports.testFlowSchema = exports.createFlowSchema = exports.copyFlowSchema = exports.publishFlowSchema = exports.viewActionSchema = exports.viewFlowSchema = exports.removeChoicesFromFieldSchema = exports.addChoicesToFieldSchema = exports.choiceValueSchema = exports.setRelatedListsSchema = exports.setFormLayoutSchema = exports.formSectionSchema = exports.setListLayoutSchema = exports.createViewSchema = void 0;
7
+ exports.invokeRestSchema = exports.publishAppSchema = exports.exportAppSchema = exports.exportUpdateSetSchema = exports.deleteRecordSchema = exports.createRecordSchema = exports.setFieldSchema = exports.setTableSchema = exports.tableAttributesSchema = exports.setColumnSchema = exports.columnAttributesSchema = exports.createIndexSchema = exports.listIndexesSchema = exports.addIndexSchema = exports.designAccessSchema = exports.addColumnSchema = exports.createTableSchema = exports.columnSpecSchema = exports.hostAssetsSchema = exports.editFlowSchema = exports.defineActionSchema = exports.defineActionSpecSchema = exports.cloneActionSchema = exports.editActionSchema = exports.stepInputPatchSchema = exports.testFlowSchema = exports.createFlowSchema = exports.copyFlowSchema = exports.publishFlowSchema = exports.viewActionSchema = exports.viewFlowSchema = exports.removeChoicesFromFieldSchema = 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),
@@ -368,10 +368,23 @@ exports.addColumnSchema = zod_1.z.object({
368
368
  // scope (a Journey field on an Automate table). Without it a mismatched scope is
369
369
  // refused — on dryRun and live alike — because that is the classic wrong-scope slip.
370
370
  crossScope: zod_1.z.boolean().optional(),
371
+ // Cross-scope only: create the missing sys_scope_design_access record (scope -> the
372
+ // table's scope) first, in the same update set. Without it a missing record is flagged.
373
+ ensureDesignAccess: zod_1.z.boolean().optional(),
371
374
  updateSetSysId: zod_1.z.string().optional(),
372
375
  dryRun: zod_1.z.boolean().optional(),
373
376
  debug: zod_1.z.boolean().optional(),
374
377
  });
378
+ // design_access: ensure the sys_scope_design_access record that lets sourceScope author
379
+ // in targetScope's tables. updateSetSysId (an update set in sourceScope) is optional in
380
+ // the schema because dryRun needs none; the live-path requirement is enforced at the tool
381
+ // boundary (registry.ts), matching add_column.
382
+ exports.designAccessSchema = zod_1.z.object({
383
+ sourceScope: zod_1.z.string().min(1),
384
+ targetScope: zod_1.z.string().min(1),
385
+ updateSetSysId: zod_1.z.string().optional(),
386
+ dryRun: zod_1.z.boolean().optional(),
387
+ });
375
388
  // add-index keeps a column LIST because an index is conceptually multi-column, but the
376
389
  // only headless lever (sys_dictionary.unique) is per-COLUMN — so addIndex REFUSES a list
377
390
  // longer than one rather than silently building a different index than the one asked for.
@@ -34,6 +34,14 @@
34
34
  * is accepted as-is) and the element ServiceNow actually stored is read back and
35
35
  * reported, together with a `sys_scope` assertion on the read-back row.
36
36
  *
37
+ * DESIGN ACCESS. Since the Zurich bw40 hotfix the platform UI also requires a
38
+ * `sys_scope_design_access` record (source = the column's scope, target = the table's)
39
+ * before one app may author in another's tables. The headless insert here does not hit
40
+ * that check, so a missing record does not block the column — but it is FLAGGED on every
41
+ * cross-scope result (`designAccess`), because the next UI edit of the column, and the
42
+ * target instance, will. `ensureDesignAccess: true` creates it first, in the same update
43
+ * set (see ./designAccess.ts).
44
+ *
37
45
  * SIZING THE PHYSICAL COLUMN. A max_length carried on the INSERT sets the dictionary
38
46
  * row but NOT the column ServiceNow actually builds — it materialises at the platform
39
47
  * default regardless. An insert declaring string(4000) therefore leaves a varchar(255)
@@ -74,6 +82,12 @@ export interface AddColumnParams {
74
82
  * and the update set must belong to the column's scope.
75
83
  */
76
84
  crossScope?: boolean;
85
+ /**
86
+ * Cross-scope only: create the sys_scope_design_access record (column scope -> table
87
+ * scope) when it is missing, captured in the same update set, BEFORE the column insert.
88
+ * Without it a missing record is only flagged on the result.
89
+ */
90
+ ensureDesignAccess?: boolean;
77
91
  /** Update set sys_id to capture the insert into. REQUIRED on the live path (dry-run doesn't need it). */
78
92
  updateSetSysId?: string;
79
93
  /** Emit diagnostic detail in the result note. */
@@ -81,6 +95,21 @@ export interface AddColumnParams {
81
95
  /** Plan only — no writes. Pure + deterministic. */
82
96
  dryRun?: boolean;
83
97
  }
98
+ /** Design Access state for a cross-scope column (absent on a same-scope add). */
99
+ export interface DesignAccessFlag {
100
+ /** Always true when reported: a cross-scope column needs the record for UI/Studio work. */
101
+ required: true;
102
+ /** true = record exists (or was created); false = missing; null = could not be read. */
103
+ present: boolean | null;
104
+ /** sys_scope_design_access sys_id ("" when missing / unknown). */
105
+ sysId: string;
106
+ /** Authoring scope (the column's owner). */
107
+ sourceScope: string;
108
+ /** Table-owning scope. */
109
+ targetScope: string;
110
+ /** True when THIS run created the record. */
111
+ created: boolean;
112
+ }
84
113
  export interface AddColumnResult {
85
114
  status: "created" | "dry-run" | "failed" | "skipped";
86
115
  /** The table's name (resolved; echoes the input on dry-run). */
@@ -106,6 +135,8 @@ export interface AddColumnResult {
106
135
  verified: boolean;
107
136
  /** Human-readable note (success summary, the read-back result, or the failure body). */
108
137
  note: string;
138
+ /** Cross-scope only: whether the Design Access record the platform UI requires exists. */
139
+ designAccess?: DesignAccessFlag;
109
140
  }
110
141
  /**
111
142
  * Derive the dictionary element (column name) from a label the way Studio does for
@@ -114,4 +145,9 @@ export interface AddColumnResult {
114
145
  * Pass `column.name` explicitly for anything non-trivial.
115
146
  */
116
147
  export declare function deriveElement(label: string, explicit?: string): string;
148
+ /**
149
+ * The Design Access sentence appended to a cross-scope result's note ("" when none).
150
+ * Exported for tests.
151
+ */
152
+ export declare function designAccessNote(da: DesignAccessFlag | undefined, ensureRequested: boolean, isDryRun: boolean): string;
117
153
  export declare function addColumn(params: AddColumnParams): Promise<AddColumnResult>;
@@ -35,6 +35,14 @@
35
35
  * is accepted as-is) and the element ServiceNow actually stored is read back and
36
36
  * reported, together with a `sys_scope` assertion on the read-back row.
37
37
  *
38
+ * DESIGN ACCESS. Since the Zurich bw40 hotfix the platform UI also requires a
39
+ * `sys_scope_design_access` record (source = the column's scope, target = the table's)
40
+ * before one app may author in another's tables. The headless insert here does not hit
41
+ * that check, so a missing record does not block the column — but it is FLAGGED on every
42
+ * cross-scope result (`designAccess`), because the next UI edit of the column, and the
43
+ * target instance, will. `ensureDesignAccess: true` creates it first, in the same update
44
+ * set (see ./designAccess.ts).
45
+ *
38
46
  * SIZING THE PHYSICAL COLUMN. A max_length carried on the INSERT sets the dictionary
39
47
  * row but NOT the column ServiceNow actually builds — it materialises at the platform
40
48
  * default regardless. An insert declaring string(4000) therefore leaves a varchar(255)
@@ -55,9 +63,11 @@
55
63
  */
56
64
  Object.defineProperty(exports, "__esModule", { value: true });
57
65
  exports.deriveElement = deriveElement;
66
+ exports.designAccessNote = designAccessNote;
58
67
  exports.addColumn = addColumn;
59
68
  const setField_1 = require("../setField");
60
69
  const buildTableSave_1 = require("./buildTableSave");
70
+ const designAccess_1 = require("./designAccess");
61
71
  var SYS_ID = /^[0-9a-f]{32}$/i;
62
72
  /** Patch dependent_on_field on a sys_dictionary row, captured in the given update set. */
63
73
  async function setDependentOnField(client, columnSysId, updateSetSysId, dependentOnField) {
@@ -262,12 +272,35 @@ async function planColumnScope(client, params, element) {
262
272
  // element; an already-prefixed name is accepted as-is.
263
273
  var prefix = owner.name + "_";
264
274
  var prefixed = element.indexOf(prefix) === 0 ? element : prefix + element;
275
+ // Flag — never block on — the Design Access record the platform UI requires. A read
276
+ // failure is reported as UNKNOWN (null), not as missing: a blind check is not evidence.
277
+ var present = null;
278
+ var daSysId = "";
279
+ try {
280
+ daSysId = await (0, designAccess_1.findDesignAccess)({
281
+ client: client,
282
+ sourceScopeSysId: owner.sysId,
283
+ targetScopeSysId: resolved.scopeSysId,
284
+ });
285
+ present = daSysId !== "";
286
+ }
287
+ catch (e) {
288
+ present = null;
289
+ }
265
290
  return {
266
291
  resolved: resolved,
267
292
  scopeName: owner.name,
268
293
  scopeSysId: owner.sysId,
269
294
  crossScope: true,
270
295
  element: prefixed,
296
+ designAccess: {
297
+ required: true,
298
+ present: present,
299
+ sysId: daSysId,
300
+ sourceScope: owner.name,
301
+ targetScope: tableScopeName,
302
+ created: false,
303
+ },
271
304
  };
272
305
  }
273
306
  /** Resolve a sys_scope sys_id to its scope NAME (e.g. "x_cadso_core"). */
@@ -277,7 +310,58 @@ async function resolveScopeName(client, scopeSysId) {
277
310
  var rows = await client.table.query("sys_scope", "sys_id=" + scopeSysId, { limit: 1, fields: ["scope"] });
278
311
  return rows.length > 0 ? (0, setField_1.fieldToString)(rows[0].scope) : "";
279
312
  }
313
+ /**
314
+ * The Design Access sentence appended to a cross-scope result's note ("" when none).
315
+ * Exported for tests.
316
+ */
317
+ function designAccessNote(da, ensureRequested, isDryRun) {
318
+ if (!da)
319
+ return "";
320
+ var pair = "'" + da.sourceScope + "' -> '" + da.targetScope + "'";
321
+ if (da.created) {
322
+ return (" Design Access " +
323
+ pair +
324
+ " was created (sys_scope_design_access " +
325
+ da.sysId +
326
+ ").");
327
+ }
328
+ if (da.present === true) {
329
+ return (" Design Access " +
330
+ pair +
331
+ " is present (sys_scope_design_access " +
332
+ da.sysId +
333
+ ").");
334
+ }
335
+ var state = da.present === null
336
+ ? "could NOT be read (sys_scope_design_access query failed) — check it on the instance"
337
+ : "is MISSING";
338
+ if (isDryRun && ensureRequested && da.present === false) {
339
+ return (" DESIGN ACCESS REQUIRED: " +
340
+ pair +
341
+ " " +
342
+ state +
343
+ " — would create it first, in the same update set.");
344
+ }
345
+ return (" DESIGN ACCESS REQUIRED: " +
346
+ pair +
347
+ " " +
348
+ state +
349
+ ". The headless insert does not need it, but UI/Studio edits of this column and the " +
350
+ "target instance will refuse it — re-run with --ensure-design-access (MCP: " +
351
+ "ensureDesignAccess:true) or create it with dove-sn design-access.");
352
+ }
280
353
  async function addColumn(params) {
354
+ var daOut = {};
355
+ var result = await addColumnInner(params, daOut);
356
+ // The dry-run path attaches its own flag + note; every live return is decorated here so
357
+ // no exit path (created / skipped / any failure) can drop the flag.
358
+ if (!result.designAccess && daOut.value) {
359
+ result.designAccess = daOut.value;
360
+ result.note += designAccessNote(daOut.value, params.ensureDesignAccess === true, false);
361
+ }
362
+ return result;
363
+ }
364
+ async function addColumnInner(params, daOut) {
281
365
  validate(params);
282
366
  var client = params.client;
283
367
  // Normalize the single column (validates label, resolves the friendly type to an
@@ -323,7 +407,8 @@ async function addColumn(params) {
323
407
  : SYS_ID.test(params.table)
324
408
  ? params.table
325
409
  : "";
326
- return {
410
+ var dryDa = planned ? planned.designAccess : undefined;
411
+ var dryResult = {
327
412
  status: "dry-run",
328
413
  table: planTable,
329
414
  tableSysId: planTableSysId,
@@ -353,8 +438,12 @@ async function addColumn(params) {
353
438
  wantDependent +
354
439
  "' (which must already exist on the table)"
355
440
  : "") +
356
- ", then read it back.",
441
+ ", then read it back." +
442
+ designAccessNote(dryDa, params.ensureDesignAccess === true, true),
357
443
  };
444
+ if (dryDa)
445
+ dryResult.designAccess = dryDa;
446
+ return dryResult;
358
447
  }
359
448
  // ---- LIVE PATH ------------------------------------------------------------
360
449
  // Require an explicit update set so the schema change is captured in a KNOWN
@@ -370,6 +459,34 @@ async function addColumn(params) {
370
459
  var resolved = plan.resolved;
371
460
  var scopeName = plan.scopeName;
372
461
  element = plan.element;
462
+ // Create the missing Design Access record FIRST when asked — nothing about the column
463
+ // has been written yet, so a failure here stops cleanly with the instance untouched
464
+ // (or, if the record landed but did not verify, says exactly that).
465
+ var da = plan.designAccess;
466
+ if (da && da.present !== true && params.ensureDesignAccess === true) {
467
+ var ensured = await (0, designAccess_1.ensureDesignAccess)({
468
+ client: client,
469
+ sourceScope: da.sourceScope,
470
+ targetScope: da.targetScope,
471
+ updateSetSysId: params.updateSetSysId,
472
+ });
473
+ da = {
474
+ required: true,
475
+ present: ensured.present,
476
+ sysId: ensured.sysId,
477
+ sourceScope: da.sourceScope,
478
+ targetScope: da.targetScope,
479
+ created: ensured.status === "created",
480
+ };
481
+ if (ensured.status === "failed") {
482
+ var daFail = failure(resolved, col, element, params.updateSetSysId, "Design Access could not be ensured, so the column was NOT added: " +
483
+ ensured.note, undefined, scopeName);
484
+ daFail.designAccess = da;
485
+ return daFail;
486
+ }
487
+ }
488
+ if (da)
489
+ daOut.value = da;
373
490
  // max_length is deliberately NOT sent on the insert — see the sizing step below.
374
491
  // Reference (and date) columns carry no max_length at all.
375
492
  var wantLength = col.type === "reference" ? "" : col.maxLength;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Cross-scope DESIGN ACCESS — the `sys_scope_design_access` record that lets one app
3
+ * author (design) inside another app's tables.
4
+ *
5
+ * Why this exists: since the Zurich bw40 hotfix (tenonworkstudio, 2026-10-04), creating a
6
+ * column from app S on a table owned by app T through the platform UI / Studio fails with
7
+ *
8
+ * Invalid 'Table' selected on the Dictionary Entry record. The '<table>' table is in
9
+ * application '<T>', but the current application is '<S>'. The 'Table' field can only
10
+ * select '<T>' tables with read access enabled.
11
+ *
12
+ * even when the table's own Application Access flags (read / alter / configuration) are
13
+ * all on. The fix is a Design Access record: source_scope = S (the authoring app),
14
+ * target_package = T (the app that owns the table), owned by — and captured in an update
15
+ * set of — S. Without it the column can still be inserted headless (the scope-aware
16
+ * createRecord op does not hit the UI check), but the next person to open that column in
17
+ * the UI, and the target instance's checks, trip over it. So add-column --cross-scope FLAGS
18
+ * a missing record and can create it; this module is that check + create.
19
+ *
20
+ * Read-back discipline matches add-column: a created record is read back by the returned
21
+ * sys_id and its source/target asserted before it is called verified. ES6 only, no
22
+ * optional chaining, no `any`.
23
+ */
24
+ import type { ServiceNowClient } from "../client";
25
+ export declare var DESIGN_ACCESS_TABLE: string;
26
+ export interface ResolvedScopeRef {
27
+ /** Scope NAME (e.g. "x_cadso_journey"). */
28
+ name: string;
29
+ /** sys_scope sys_id. */
30
+ sysId: string;
31
+ }
32
+ /** Resolve a scope by NAME or sys_id; empty strings when not found. */
33
+ export declare function resolveScopeRef(client: ServiceNowClient, scope: string): Promise<ResolvedScopeRef>;
34
+ /**
35
+ * Find the Design Access record granting `sourceScopeSysId` design access to
36
+ * `targetScopeSysId`'s tables. Returns its sys_id, or "" when there is none.
37
+ */
38
+ export declare function findDesignAccess(params: {
39
+ client: ServiceNowClient;
40
+ sourceScopeSysId: string;
41
+ targetScopeSysId: string;
42
+ }): Promise<string>;
43
+ export interface EnsureDesignAccessParams {
44
+ client: ServiceNowClient;
45
+ /** The AUTHORING app — scope name or sys_scope sys_id (e.g. "x_cadso_journey"). */
46
+ sourceScope: string;
47
+ /** The app that OWNS the tables — scope name or sys_scope sys_id (e.g. "x_cadso_automate"). */
48
+ targetScope: string;
49
+ /** Update set (in the SOURCE scope) to capture the record in. Required on the live path. */
50
+ updateSetSysId?: string;
51
+ /** Report whether the record exists / would be created — no writes. */
52
+ dryRun?: boolean;
53
+ }
54
+ export interface EnsureDesignAccessResult {
55
+ status: "exists" | "created" | "dry-run" | "failed";
56
+ /** Source (authoring) scope name. */
57
+ sourceScope: string;
58
+ /** Target (table-owning) scope name. */
59
+ targetScope: string;
60
+ /** The Design Access record's sys_id ("" when absent on dry-run, or on failure before insert). */
61
+ sysId: string;
62
+ /** True when the record is present: found, or created and read back. */
63
+ present: boolean;
64
+ /** Update set the create was captured into ("" when nothing was written). */
65
+ updateSetSysId: string;
66
+ note: string;
67
+ }
68
+ /**
69
+ * Make sure `sourceScope` has design access to `targetScope`'s tables. Idempotent: an
70
+ * existing record is reported ("exists") and nothing is written. Dry-run reports whether
71
+ * it exists or would be created. Live creates it through the scope-aware createRecord op
72
+ * switched to the SOURCE scope, captured in `updateSetSysId` (which must belong to the
73
+ * source scope), then reads it back.
74
+ */
75
+ export declare function ensureDesignAccess(params: EnsureDesignAccessParams): Promise<EnsureDesignAccessResult>;
@@ -0,0 +1,250 @@
1
+ "use strict";
2
+ /**
3
+ * Cross-scope DESIGN ACCESS — the `sys_scope_design_access` record that lets one app
4
+ * author (design) inside another app's tables.
5
+ *
6
+ * Why this exists: since the Zurich bw40 hotfix (tenonworkstudio, 2026-10-04), creating a
7
+ * column from app S on a table owned by app T through the platform UI / Studio fails with
8
+ *
9
+ * Invalid 'Table' selected on the Dictionary Entry record. The '<table>' table is in
10
+ * application '<T>', but the current application is '<S>'. The 'Table' field can only
11
+ * select '<T>' tables with read access enabled.
12
+ *
13
+ * even when the table's own Application Access flags (read / alter / configuration) are
14
+ * all on. The fix is a Design Access record: source_scope = S (the authoring app),
15
+ * target_package = T (the app that owns the table), owned by — and captured in an update
16
+ * set of — S. Without it the column can still be inserted headless (the scope-aware
17
+ * createRecord op does not hit the UI check), but the next person to open that column in
18
+ * the UI, and the target instance's checks, trip over it. So add-column --cross-scope FLAGS
19
+ * a missing record and can create it; this module is that check + create.
20
+ *
21
+ * Read-back discipline matches add-column: a created record is read back by the returned
22
+ * sys_id and its source/target asserted before it is called verified. ES6 only, no
23
+ * optional chaining, no `any`.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.DESIGN_ACCESS_TABLE = void 0;
27
+ exports.resolveScopeRef = resolveScopeRef;
28
+ exports.findDesignAccess = findDesignAccess;
29
+ exports.ensureDesignAccess = ensureDesignAccess;
30
+ const setField_1 = require("../setField");
31
+ var SYS_ID = /^[0-9a-f]{32}$/i;
32
+ var SCOPE_NAME = /^[a-z0-9_]+$/i;
33
+ /** Safe to splice into an encoded query: no ^, =, spaces or other operators. */
34
+ var SAFE_ID = /^[0-9a-z_]+$/i;
35
+ exports.DESIGN_ACCESS_TABLE = "sys_scope_design_access";
36
+ /** Resolve a scope by NAME or sys_id; empty strings when not found. */
37
+ async function resolveScopeRef(client, scope) {
38
+ var s = String(scope || "").trim();
39
+ if (!s)
40
+ return { name: "", sysId: "" };
41
+ if (!SYS_ID.test(s) && !SCOPE_NAME.test(s)) {
42
+ throw new Error("design-access: '" + s + "' is not a scope name or sys_scope sys_id.");
43
+ }
44
+ var rows = await client.table.query("sys_scope", SYS_ID.test(s) ? "sys_id=" + s : "scope=" + s, { limit: 1, fields: ["sys_id", "scope"] });
45
+ if (rows.length === 0)
46
+ return { name: "", sysId: "" };
47
+ return {
48
+ name: (0, setField_1.fieldToString)(rows[0].scope),
49
+ sysId: (0, setField_1.fieldToString)(rows[0].sys_id),
50
+ };
51
+ }
52
+ /**
53
+ * Find the Design Access record granting `sourceScopeSysId` design access to
54
+ * `targetScopeSysId`'s tables. Returns its sys_id, or "" when there is none.
55
+ */
56
+ async function findDesignAccess(params) {
57
+ // Both ids are spliced into an encoded query — refuse anything that could carry a
58
+ // query operator (^, =, spaces) rather than querying on it.
59
+ if (!SAFE_ID.test(params.sourceScopeSysId) ||
60
+ !SAFE_ID.test(params.targetScopeSysId)) {
61
+ return "";
62
+ }
63
+ var rows = await params.client.table.query(exports.DESIGN_ACCESS_TABLE, "source_scope=" +
64
+ params.sourceScopeSysId +
65
+ "^target_package=" +
66
+ params.targetScopeSysId, { limit: 1, fields: ["sys_id"] });
67
+ return rows.length > 0 ? (0, setField_1.fieldToString)(rows[0].sys_id) : "";
68
+ }
69
+ /** Resolve both scopes, refusing unknown or identical ones. Shared by dry-run and live. */
70
+ async function resolvePair(params) {
71
+ if (!params || typeof params !== "object")
72
+ throw new Error("design-access: params object required.");
73
+ if (!params.client)
74
+ throw new Error("design-access: client is required.");
75
+ if (!params.sourceScope || !String(params.sourceScope).trim())
76
+ throw new Error("design-access: sourceScope (the authoring app) is required.");
77
+ if (!params.targetScope || !String(params.targetScope).trim())
78
+ throw new Error("design-access: targetScope (the app that owns the tables) is required.");
79
+ var source = await resolveScopeRef(params.client, params.sourceScope);
80
+ if (!source.sysId)
81
+ throw new Error("design-access: source scope '" +
82
+ params.sourceScope +
83
+ "' was not found in sys_scope.");
84
+ var target = await resolveScopeRef(params.client, params.targetScope);
85
+ if (!target.sysId)
86
+ throw new Error("design-access: target scope '" +
87
+ params.targetScope +
88
+ "' was not found in sys_scope.");
89
+ if (source.sysId === target.sysId)
90
+ throw new Error("design-access: source and target are the same scope ('" +
91
+ source.name +
92
+ "') — an app needs no design access to its own tables.");
93
+ return { source: source, target: target };
94
+ }
95
+ /**
96
+ * Make sure `sourceScope` has design access to `targetScope`'s tables. Idempotent: an
97
+ * existing record is reported ("exists") and nothing is written. Dry-run reports whether
98
+ * it exists or would be created. Live creates it through the scope-aware createRecord op
99
+ * switched to the SOURCE scope, captured in `updateSetSysId` (which must belong to the
100
+ * source scope), then reads it back.
101
+ */
102
+ async function ensureDesignAccess(params) {
103
+ var pair = await resolvePair(params);
104
+ var client = params.client;
105
+ var label = "'" + pair.source.name + "' -> '" + pair.target.name + "'";
106
+ var existing = await findDesignAccess({
107
+ client: client,
108
+ sourceScopeSysId: pair.source.sysId,
109
+ targetScopeSysId: pair.target.sysId,
110
+ });
111
+ var base = {
112
+ sourceScope: pair.source.name,
113
+ targetScope: pair.target.name,
114
+ };
115
+ if (existing) {
116
+ return {
117
+ status: "exists",
118
+ sourceScope: base.sourceScope,
119
+ targetScope: base.targetScope,
120
+ sysId: existing,
121
+ present: true,
122
+ updateSetSysId: "",
123
+ note: "Design Access " +
124
+ label +
125
+ " already exists (" +
126
+ exports.DESIGN_ACCESS_TABLE +
127
+ " " +
128
+ existing +
129
+ ") — nothing to do.",
130
+ };
131
+ }
132
+ var us = params.updateSetSysId ? params.updateSetSysId.trim() : "";
133
+ if (us) {
134
+ if (!SAFE_ID.test(us))
135
+ throw new Error("design-access: update set '" + us + "' is not a sys_id.");
136
+ var usRows = await client.table.query("sys_update_set", "sys_id=" + us, { limit: 1, fields: ["sys_id", "name", "application"] });
137
+ if (usRows.length === 0)
138
+ throw new Error("design-access: update set '" +
139
+ us +
140
+ "' was not found in sys_update_set.");
141
+ if ((0, setField_1.fieldToString)(usRows[0].application) !== pair.source.sysId) {
142
+ throw new Error("design-access: update set '" +
143
+ ((0, setField_1.fieldToString)(usRows[0].name) || us) +
144
+ "' does not belong to the source scope '" +
145
+ pair.source.name +
146
+ "' — the Design Access record is owned by the AUTHORING app and ships in its " +
147
+ "update set. Pass an update set in '" +
148
+ pair.source.name +
149
+ "'.");
150
+ }
151
+ }
152
+ if (params.dryRun) {
153
+ return {
154
+ status: "dry-run",
155
+ sourceScope: base.sourceScope,
156
+ targetScope: base.targetScope,
157
+ sysId: "",
158
+ present: false,
159
+ updateSetSysId: us,
160
+ note: "dry-run: no write. Design Access " +
161
+ label +
162
+ " is MISSING — would create it in scope '" +
163
+ pair.source.name +
164
+ "', captured into update set " +
165
+ (us || "(none provided — required on the live path)") +
166
+ ", then read it back.",
167
+ };
168
+ }
169
+ if (!us) {
170
+ throw new Error("design-access: updateSetSysId is required on the live path so the record is " +
171
+ "captured in a known update set of the source scope (dry-run does not need one).");
172
+ }
173
+ var created;
174
+ try {
175
+ created = await client.claude.createRecord({
176
+ table: exports.DESIGN_ACCESS_TABLE,
177
+ fields: {
178
+ source_scope: pair.source.sysId,
179
+ target_package: pair.target.sysId,
180
+ },
181
+ scope: pair.source.name,
182
+ update_set_sys_id: us,
183
+ });
184
+ }
185
+ catch (e) {
186
+ return failed(base, us, "", "insert failed: " + errText(e));
187
+ }
188
+ var newSysId = (0, setField_1.fieldToString)(created && created.sys_id);
189
+ if (!newSysId) {
190
+ return failed(base, us, "", "createRecord returned no sys_id — the insert may not have landed; check the instance.");
191
+ }
192
+ var rows;
193
+ try {
194
+ rows = await client.table.query(exports.DESIGN_ACCESS_TABLE, "sys_id=" + newSysId, { limit: 1, fields: ["sys_id", "source_scope", "target_package"] });
195
+ }
196
+ catch (e) {
197
+ return failed(base, us, newSysId, "record created but the read-back failed: " + errText(e));
198
+ }
199
+ if (rows.length === 0) {
200
+ return failed(base, us, newSysId, "createRecord returned " +
201
+ newSysId +
202
+ " but no record was found on read-back.");
203
+ }
204
+ var gotSource = (0, setField_1.fieldToString)(rows[0].source_scope);
205
+ var gotTarget = (0, setField_1.fieldToString)(rows[0].target_package);
206
+ if (gotSource !== pair.source.sysId || gotTarget !== pair.target.sysId) {
207
+ return failed(base, us, newSysId, "record " +
208
+ newSysId +
209
+ " read back as source_scope '" +
210
+ (gotSource || "(empty)") +
211
+ "' / target_package '" +
212
+ (gotTarget || "(empty)") +
213
+ "', not the requested '" +
214
+ pair.source.sysId +
215
+ "' / '" +
216
+ pair.target.sysId +
217
+ "'. Fix or delete it on the instance.");
218
+ }
219
+ return {
220
+ status: "created",
221
+ sourceScope: base.sourceScope,
222
+ targetScope: base.targetScope,
223
+ sysId: newSysId,
224
+ present: true,
225
+ updateSetSysId: us,
226
+ note: "Created Design Access " +
227
+ label +
228
+ " (" +
229
+ exports.DESIGN_ACCESS_TABLE +
230
+ " " +
231
+ newSysId +
232
+ ") — verified on read-back, captured into update set " +
233
+ us +
234
+ ".",
235
+ };
236
+ }
237
+ function errText(e) {
238
+ return e && e.message ? e.message : String(e);
239
+ }
240
+ function failed(base, updateSetSysId, sysId, note) {
241
+ return {
242
+ status: "failed",
243
+ sourceScope: base.sourceScope,
244
+ targetScope: base.targetScope,
245
+ sysId: sysId,
246
+ present: false,
247
+ updateSetSysId: updateSetSysId,
248
+ note: "design-access: " + note,
249
+ };
250
+ }
@@ -12,7 +12,9 @@ export type { ColumnSpec, AccessFlags, OverlaySpec } from "./buildTableSave";
12
12
  export { resolveFormAuth, openFormSession, setCurrentApplication, getRecordForm, getNewRecordForm, getWithSession, getFormPage, parseFormInputs, postForm, scrapeCk, decodeHtmlEntities, } from "./formSession";
13
13
  export type { FormAuth, FormSession, HarvestedForm, FetchedFormPage, PostResult, } from "./formSession";
14
14
  export { addColumn, deriveElement } from "./addColumn";
15
- export type { AddColumnParams, AddColumnResult } from "./addColumn";
15
+ export type { AddColumnParams, AddColumnResult, DesignAccessFlag } from "./addColumn";
16
+ export { ensureDesignAccess, findDesignAccess, resolveScopeRef, DESIGN_ACCESS_TABLE, } from "./designAccess";
17
+ export type { EnsureDesignAccessParams, EnsureDesignAccessResult, ResolvedScopeRef, } from "./designAccess";
16
18
  export { addIndex, scanForDuplicates, parseIndexColumns, indexMatchesColumns, } from "./addIndex";
17
19
  export type { AddIndexParams, AddIndexResult, AddIndexVerification, DuplicateScan, DuplicateValue, } from "./addIndex";
18
20
  export { listIndexes, assertTableName } from "./listIndexes";
@@ -5,8 +5,8 @@
5
5
  * live-validation caveat.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.diffInherited = exports.effectiveValue = exports.findLabelRow = exports.findOverrideRow = exports.resolveTableScope = exports.explainMaxLengthNotOverridable = exports.LABEL_LANGUAGE = exports.OVERRIDABLE = exports.findTruncationRisk = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.NOT_IN_UPDATE_SET = exports.DEFAULT_INDEX_FORM_PATH = exports.validateCreateIndex = exports.createIndex = exports.assertTableName = exports.listIndexes = exports.indexMatchesColumns = exports.parseIndexColumns = exports.scanForDuplicates = exports.addIndex = exports.deriveElement = exports.addColumn = exports.decodeHtmlEntities = exports.scrapeCk = exports.postForm = exports.parseFormInputs = exports.getFormPage = exports.getWithSession = 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
- exports.resolveTableAttributes = exports.setTable = exports.labelUpdateName = exports.overrideUpdateName = exports.applyInheritedWrites = void 0;
8
+ exports.resolveTableScope = exports.explainMaxLengthNotOverridable = exports.LABEL_LANGUAGE = exports.OVERRIDABLE = exports.findTruncationRisk = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.NOT_IN_UPDATE_SET = exports.DEFAULT_INDEX_FORM_PATH = exports.validateCreateIndex = exports.createIndex = exports.assertTableName = exports.listIndexes = exports.indexMatchesColumns = exports.parseIndexColumns = exports.scanForDuplicates = exports.addIndex = exports.DESIGN_ACCESS_TABLE = exports.resolveScopeRef = exports.findDesignAccess = exports.ensureDesignAccess = exports.deriveElement = exports.addColumn = exports.decodeHtmlEntities = exports.scrapeCk = exports.postForm = exports.parseFormInputs = exports.getFormPage = exports.getWithSession = 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
+ exports.resolveTableAttributes = exports.setTable = exports.labelUpdateName = exports.overrideUpdateName = exports.applyInheritedWrites = exports.diffInherited = exports.effectiveValue = exports.findLabelRow = exports.findOverrideRow = void 0;
10
10
  var createTable_1 = require("./createTable");
11
11
  Object.defineProperty(exports, "createTable", { enumerable: true, get: function () { return createTable_1.createTable; } });
12
12
  Object.defineProperty(exports, "projectTableGraph", { enumerable: true, get: function () { return createTable_1.projectTableGraph; } });
@@ -40,6 +40,11 @@ Object.defineProperty(exports, "decodeHtmlEntities", { enumerable: true, get: fu
40
40
  var addColumn_1 = require("./addColumn");
41
41
  Object.defineProperty(exports, "addColumn", { enumerable: true, get: function () { return addColumn_1.addColumn; } });
42
42
  Object.defineProperty(exports, "deriveElement", { enumerable: true, get: function () { return addColumn_1.deriveElement; } });
43
+ var designAccess_1 = require("./designAccess");
44
+ Object.defineProperty(exports, "ensureDesignAccess", { enumerable: true, get: function () { return designAccess_1.ensureDesignAccess; } });
45
+ Object.defineProperty(exports, "findDesignAccess", { enumerable: true, get: function () { return designAccess_1.findDesignAccess; } });
46
+ Object.defineProperty(exports, "resolveScopeRef", { enumerable: true, get: function () { return designAccess_1.resolveScopeRef; } });
47
+ Object.defineProperty(exports, "DESIGN_ACCESS_TABLE", { enumerable: true, get: function () { return designAccess_1.DESIGN_ACCESS_TABLE; } });
43
48
  var addIndex_1 = require("./addIndex");
44
49
  Object.defineProperty(exports, "addIndex", { enumerable: true, get: function () { return addIndex_1.addIndex; } });
45
50
  Object.defineProperty(exports, "scanForDuplicates", { enumerable: true, get: function () { return addIndex_1.scanForDuplicates; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-servicenow",
3
- "version": "0.0.49",
3
+ "version": "0.0.50",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },