@tenonhq/dovetail-servicenow 0.0.33 → 0.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -893,6 +893,10 @@ function printHelp() {
893
893
  " A max-length SHRINK is REFUSED while rows hold longer values —\n" +
894
894
  " ServiceNow silently ignores such a shrink (200 OK, no change).\n" +
895
895
  " Shorten or clear those values first, then re-run.\n" +
896
+ " An INHERITED column (one defined on a parent table) is narrowed for\n" +
897
+ " YOUR table alone, via sys_dictionary_override / sys_documentation —\n" +
898
+ " the parent and its other children are untouched. max-length is the\n" +
899
+ " exception: it is the parent's physical column and is refused.\n" +
896
900
  " --element / --internal-type are REFUSED with an explanation:\n" +
897
901
  " ServiceNow silently ignores both on an existing column.\n" +
898
902
  " set-table Update an EXISTING TABLE's own dictionary row (the collection row),\n" +
@@ -1214,6 +1218,12 @@ async function runSetColumn(flags, bare) {
1214
1218
  result.table +
1215
1219
  "." +
1216
1220
  result.column +
1221
+ // Say when the change went to an override rather than the column's own row. The
1222
+ // caller asked for a table + column; without this they have no reason to expect
1223
+ // the write landed on a different record type entirely.
1224
+ (result.via === "override"
1225
+ ? " — override (inherited from " + result.definedOn + ")"
1226
+ : "") +
1217
1227
  (result.verified && result.status === "applied" ? " — verified" : "") +
1218
1228
  (result.status === "applied" && !result.capturedInUpdateSet
1219
1229
  ? " — NOT CAPTURED"
@@ -370,7 +370,14 @@ function buildDescriptors(deps = {}) {
370
370
  "refuses such a shrink (200 OK, column unchanged, data preserved), so the tool names the " +
371
371
  "blocking rows instead of issuing a write that would be quietly ignored. Clear those values " +
372
372
  "first, then re-run — there is no override, because forcing it would either do nothing or " +
373
- "destroy data. dryRun:true diffs against the instance, flags the blocking rows, and writes nothing.",
373
+ "destroy data. INHERITED COLUMNS ARE SUPPORTED: on an extended table the column is defined on " +
374
+ "an ancestor, and set_column narrows it for THAT TABLE ALONE via sys_dictionary_override " +
375
+ "(mandatory/default/readOnly) or sys_documentation (label) — the ancestor and every sibling " +
376
+ "table are left untouched, and the result reports via:'override' with definedOn set. Do NOT " +
377
+ "call set_column against the parent table to change a child's column: that changes it for " +
378
+ "EVERY descendant. maxLength is the sole exception — it is the ancestor's physical column, has " +
379
+ "no per-child override, and is refused with an explanation. dryRun:true diffs against the " +
380
+ "instance, flags the blocking rows, and writes nothing.",
374
381
  shape: schemas_1.setColumnSchema.shape,
375
382
  handler: async function (args) {
376
383
  var p = schemas_1.setColumnSchema.parse(args);
@@ -15,5 +15,7 @@ export { addColumn, deriveElement } from "./addColumn";
15
15
  export type { AddColumnParams, AddColumnResult } from "./addColumn";
16
16
  export { setColumn, resolveAttributes, toStoredValue, findTruncationRisk, } from "./setColumn";
17
17
  export type { SetColumnParams, SetColumnResult, ColumnAttributes, AttributeChange, TruncationRisk, } from "./setColumn";
18
+ export { OVERRIDABLE, LABEL_LANGUAGE, explainMaxLengthNotOverridable, resolveTableScope, findOverrideRow, findLabelRow, effectiveValue, diffInherited, applyInheritedWrites, overrideUpdateName, labelUpdateName, } from "./overrideColumn";
19
+ export type { OverridableAttribute, InheritedChange, InheritedWriteParams, InheritedWriteResult, } from "./overrideColumn";
18
20
  export { setTable, resolveTableAttributes } from "./setTable";
19
21
  export type { SetTableParams, SetTableResult, TableAttributes, } from "./setTable";
@@ -5,7 +5,7 @@
5
5
  * live-validation caveat.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.resolveTableAttributes = exports.setTable = exports.findTruncationRisk = exports.toStoredValue = exports.resolveAttributes = exports.setColumn = exports.deriveElement = exports.addColumn = exports.decodeHtmlEntities = exports.scrapeCk = exports.postForm = exports.parseFormInputs = exports.getNewRecordForm = exports.getRecordForm = exports.setCurrentApplication = exports.openFormSession = exports.resolveFormAuth = exports.TYPE_MAP = exports.listEditKey = exports.showInMenuKey = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.xmlEscape = exports.buildColumnXml = exports.DEFAULT_COLUMNS_REL_ID = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.parseSysIdFromLocation = exports.projectTableGraph = exports.createTable = void 0;
8
+ exports.resolveTableAttributes = exports.setTable = exports.labelUpdateName = exports.overrideUpdateName = exports.applyInheritedWrites = 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.deriveElement = exports.addColumn = exports.decodeHtmlEntities = exports.scrapeCk = exports.postForm = exports.parseFormInputs = exports.getNewRecordForm = exports.getRecordForm = exports.setCurrentApplication = exports.openFormSession = exports.resolveFormAuth = exports.TYPE_MAP = exports.listEditKey = exports.showInMenuKey = exports.defaultAccessFlags = exports.applyTableSaveOverlay = exports.resolveType = exports.normalizeColumns = exports.xmlEscape = exports.buildColumnXml = exports.DEFAULT_COLUMNS_REL_ID = exports.DEFAULT_SAVE_ACTION = exports.DEFAULT_SUPER_CLASS = exports.parseSysIdFromLocation = exports.projectTableGraph = exports.createTable = void 0;
9
9
  var createTable_1 = require("./createTable");
10
10
  Object.defineProperty(exports, "createTable", { enumerable: true, get: function () { return createTable_1.createTable; } });
11
11
  Object.defineProperty(exports, "projectTableGraph", { enumerable: true, get: function () { return createTable_1.projectTableGraph; } });
@@ -42,6 +42,18 @@ Object.defineProperty(exports, "setColumn", { enumerable: true, get: function ()
42
42
  Object.defineProperty(exports, "resolveAttributes", { enumerable: true, get: function () { return setColumn_1.resolveAttributes; } });
43
43
  Object.defineProperty(exports, "toStoredValue", { enumerable: true, get: function () { return setColumn_1.toStoredValue; } });
44
44
  Object.defineProperty(exports, "findTruncationRisk", { enumerable: true, get: function () { return setColumn_1.findTruncationRisk; } });
45
+ var overrideColumn_1 = require("./overrideColumn");
46
+ Object.defineProperty(exports, "OVERRIDABLE", { enumerable: true, get: function () { return overrideColumn_1.OVERRIDABLE; } });
47
+ Object.defineProperty(exports, "LABEL_LANGUAGE", { enumerable: true, get: function () { return overrideColumn_1.LABEL_LANGUAGE; } });
48
+ Object.defineProperty(exports, "explainMaxLengthNotOverridable", { enumerable: true, get: function () { return overrideColumn_1.explainMaxLengthNotOverridable; } });
49
+ Object.defineProperty(exports, "resolveTableScope", { enumerable: true, get: function () { return overrideColumn_1.resolveTableScope; } });
50
+ Object.defineProperty(exports, "findOverrideRow", { enumerable: true, get: function () { return overrideColumn_1.findOverrideRow; } });
51
+ Object.defineProperty(exports, "findLabelRow", { enumerable: true, get: function () { return overrideColumn_1.findLabelRow; } });
52
+ Object.defineProperty(exports, "effectiveValue", { enumerable: true, get: function () { return overrideColumn_1.effectiveValue; } });
53
+ Object.defineProperty(exports, "diffInherited", { enumerable: true, get: function () { return overrideColumn_1.diffInherited; } });
54
+ Object.defineProperty(exports, "applyInheritedWrites", { enumerable: true, get: function () { return overrideColumn_1.applyInheritedWrites; } });
55
+ Object.defineProperty(exports, "overrideUpdateName", { enumerable: true, get: function () { return overrideColumn_1.overrideUpdateName; } });
56
+ Object.defineProperty(exports, "labelUpdateName", { enumerable: true, get: function () { return overrideColumn_1.labelUpdateName; } });
45
57
  var setTable_1 = require("./setTable");
46
58
  Object.defineProperty(exports, "setTable", { enumerable: true, get: function () { return setTable_1.setTable; } });
47
59
  Object.defineProperty(exports, "resolveTableAttributes", { enumerable: true, get: function () { return setTable_1.resolveTableAttributes; } });
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The INHERITED-column path for `set-column`.
3
+ *
4
+ * WHY THIS EXISTS. set-column originally refused every inherited column outright and told
5
+ * the caller to go change the parent instead, warning in the same breath that doing so
6
+ * "changes the column for EVERY table that extends" it. That advice was wrong, and
7
+ * actively dangerous: it is the destructive option, and ServiceNow has shipped the
8
+ * correct one for years. A child table narrows an inherited column for ITSELF via
9
+ * sys_dictionary_override (mandatory / default / read-only) or sys_documentation (label),
10
+ * touching neither the parent nor any sibling.
11
+ *
12
+ * The refusal was never verified against an instance — the irony being that the verb's
13
+ * own thesis is "do not trust a response you have not read back". Everything below was
14
+ * established live on tenonworkshed, 2026-07-16, by querying real rows:
15
+ *
16
+ * - sys_dictionary_override carries a VALUE column and a paired `<attr>_override`
17
+ * boolean, for mandatory / read_only / default_value (plus calculation, dependent,
18
+ * reference_qual, attributes — outside this verb's five). The boolean is what
19
+ * ACTIVATES the override: a value written without its flag is inert.
20
+ *
21
+ * - There is NO max_length override and NO label override on that table. max_length is
22
+ * physical on the defining table's column, so it genuinely cannot be narrowed per
23
+ * child — that one refusal was right, for a reason the old message never gave.
24
+ *
25
+ * - LABEL is not an override at all; it is a sys_documentation row keyed on the CHILD
26
+ * table. Proof in the platform itself: `problem.short_description` reads "Problem
27
+ * statement" while `task.short_description` — the row that actually defines the
28
+ * column — still reads "Short description". sys_dictionary has no row for
29
+ * problem.short_description whatsoever. OOB ServiceNow does exactly what set-column
30
+ * said was impossible.
31
+ *
32
+ * - `base_table` is the table that DEFINES the column, NOT the immediate parent.
33
+ * Verified against a deep chain: cmdb_ci_endpoint_sharepoint_service extends
34
+ * cmdb_ci_endpoint_inclusion, yet its override rows carry base_table=cmdb_ci.
35
+ *
36
+ * - SCOPE FOLLOWS THE CHILD, not the parent. x_cadso_work_project's overrides of
37
+ * task's columns sit in the x_cadso scope while task is global; problem_task's sit
38
+ * in global. Resolving scope from the parent's dictionary row — the obvious move,
39
+ * and what choices.ts does for its own case — would land a scoped child's override
40
+ * in `global`. That is the exact wrong-scope failure Craftsman's CLAUDE.md calls out.
41
+ *
42
+ * CAPTURE NAMES ARE NOT ANALOGOUS, and this is a trap worth stating plainly. Three
43
+ * record types, three different sys_update_xml naming conventions:
44
+ *
45
+ * sys_dictionary sys_dictionary_<table>_<element>
46
+ * sys_dictionary_override sys_dictionary_override_<SYS_ID> <- keyed by sys_id
47
+ * sys_documentation sys_documentation_<table>_<element>_<language>
48
+ *
49
+ * Deriving the override's name by analogy with the other two produces a query that
50
+ * matches nothing, so a perfectly-captured change would report itself as uncaptured. Both
51
+ * names below were read off real update rows, not inferred.
52
+ *
53
+ * ES6 only, no optional chaining, no `any`.
54
+ */
55
+ import type { ServiceNowClient } from "../client";
56
+ import type { AttributeChange } from "./setColumn";
57
+ /** The language a label override is written for. ServiceNow keys sys_documentation by
58
+ * language, and the capture name embeds it, so it is explicit rather than implied. */
59
+ export declare var LABEL_LANGUAGE: string;
60
+ export interface OverridableAttribute {
61
+ /** The sys_dictionary_override column that holds the value. */
62
+ field: string;
63
+ /** The boolean that ACTIVATES it. A value written without its flag is inert — the
64
+ * child silently keeps inheriting, which would read as a successful no-op. */
65
+ flag: string;
66
+ }
67
+ /**
68
+ * The inherited attributes a child can narrow for itself, keyed by the sys_dictionary
69
+ * column set-column diffs on. Anything absent is NOT overridable per-child:
70
+ * - column_label -> sys_documentation instead (see applyLabelOverride)
71
+ * - max_length -> physical on the defining table; no override exists at all
72
+ */
73
+ export declare var OVERRIDABLE: Record<string, OverridableAttribute>;
74
+ /**
75
+ * Why max_length — alone among set-column's five attributes — cannot be narrowed on a
76
+ * child, and what that means for the caller.
77
+ *
78
+ * Deliberately max_length-specific rather than a general "attribute X is not
79
+ * overridable" helper: of the five, the other four ARE overridable, so a generic branch
80
+ * would be unreachable code speculating about a case that does not exist. If a sixth
81
+ * attribute is ever added to WRITABLE, this needs revisiting — which a dead generic
82
+ * branch would have quietly hidden.
83
+ *
84
+ * Unlike the old blanket refusal this one is true, and it says WHY rather than
85
+ * recommending the destructive alternative as though it were routine.
86
+ */
87
+ export declare function explainMaxLengthNotOverridable(table: string, column: string, definedOn: string): string;
88
+ /** The scope the override belongs to — resolved from the CHILD table, never the parent.
89
+ * A scoped child overriding a global parent's column must land in the child's scope. */
90
+ export declare function resolveTableScope(client: ServiceNowClient, table: string): Promise<string>;
91
+ /** The child's existing override row for this column, or null. */
92
+ export declare function findOverrideRow(client: ServiceNowClient, table: string, column: string): Promise<Record<string, unknown> | null>;
93
+ /** The child's existing label row for this column, or null. */
94
+ export declare function findLabelRow(client: ServiceNowClient, table: string, column: string): Promise<Record<string, unknown> | null>;
95
+ /**
96
+ * The value this child ACTUALLY sees for an attribute today.
97
+ *
98
+ * An override's value only counts when its flag is on. A row carrying mandatory=true with
99
+ * mandatory_override=false is inert — the child still inherits — so reading the value
100
+ * alone would diff against a number the platform is ignoring, and report `unchanged` for
101
+ * a column that behaves the opposite way.
102
+ */
103
+ export declare function effectiveValue(field: string, overrideRow: Record<string, unknown> | null, parentRow: Record<string, unknown>, labelRow: Record<string, unknown> | null): string;
104
+ /** One attribute's before -> after.
105
+ *
106
+ * This is setColumn's AttributeChange, imported as a TYPE. setColumn imports values from
107
+ * here, so a value-import back would close a runtime cycle — but `import type` is erased
108
+ * at compile time and cannot. Re-declaring the shape would be a second source of truth
109
+ * for the same three fields, which is worse than the cycle it does not prevent. */
110
+ export type InheritedChange = AttributeChange;
111
+ /**
112
+ * What would actually change for this child, comparing against the value it sees TODAY
113
+ * (its override if one is active, otherwise the inherited value).
114
+ *
115
+ * Note what is deliberately NOT treated as a change: a requested value that already
116
+ * matches the INHERITED one. Writing an override there would pin the child to a value it
117
+ * already has, decoupling it from the parent as an invisible side effect of a request
118
+ * that asked for no such thing. Reporting `unchanged` — and saying the column still
119
+ * tracks the parent — is the honest answer.
120
+ */
121
+ export declare function diffInherited(writes: Record<string, string>, overrideRow: Record<string, unknown> | null, parentRow: Record<string, unknown>, labelRow: Record<string, unknown> | null): Array<InheritedChange>;
122
+ export interface InheritedWriteParams {
123
+ client: ServiceNowClient;
124
+ table: string;
125
+ column: string;
126
+ /** The table that DEFINES the column — what base_table must be set to. */
127
+ definedOn: string;
128
+ changes: Array<InheritedChange>;
129
+ overrideRow: Record<string, unknown> | null;
130
+ labelRow: Record<string, unknown> | null;
131
+ updateSetSysId: string;
132
+ }
133
+ export interface InheritedWriteResult {
134
+ /** sys_id of the override row touched, "" when no override attribute changed. */
135
+ overrideSysId: string;
136
+ /** sys_id of the label row touched, "" when the label did not change. */
137
+ labelSysId: string;
138
+ /** True only when every written value was READ BACK from the instance and matched. */
139
+ verified: boolean;
140
+ mismatched: Array<string>;
141
+ /** True when every record written was found in the named update set. */
142
+ captured: boolean;
143
+ }
144
+ /** Update-set capture name for an override row — keyed by SYS_ID, unlike sys_dictionary.
145
+ * Read off real update rows on tenonworkshed; deriving it by analogy yields a query that
146
+ * matches nothing and reports a captured change as uncaptured. */
147
+ export declare function overrideUpdateName(overrideSysId: string): string;
148
+ /** Update-set capture name for a label row: sys_documentation_<table>_<element>_<lang>. */
149
+ export declare function labelUpdateName(table: string, column: string): string;
150
+ /**
151
+ * Write the child's overrides, then READ THEM BACK. The whole verb exists because
152
+ * ServiceNow returns 200 for writes it silently discards, and that reasoning does not
153
+ * stop applying just because the target table changed.
154
+ */
155
+ export declare function applyInheritedWrites(params: InheritedWriteParams): Promise<InheritedWriteResult>;
@@ -0,0 +1,388 @@
1
+ "use strict";
2
+ /**
3
+ * The INHERITED-column path for `set-column`.
4
+ *
5
+ * WHY THIS EXISTS. set-column originally refused every inherited column outright and told
6
+ * the caller to go change the parent instead, warning in the same breath that doing so
7
+ * "changes the column for EVERY table that extends" it. That advice was wrong, and
8
+ * actively dangerous: it is the destructive option, and ServiceNow has shipped the
9
+ * correct one for years. A child table narrows an inherited column for ITSELF via
10
+ * sys_dictionary_override (mandatory / default / read-only) or sys_documentation (label),
11
+ * touching neither the parent nor any sibling.
12
+ *
13
+ * The refusal was never verified against an instance — the irony being that the verb's
14
+ * own thesis is "do not trust a response you have not read back". Everything below was
15
+ * established live on tenonworkshed, 2026-07-16, by querying real rows:
16
+ *
17
+ * - sys_dictionary_override carries a VALUE column and a paired `<attr>_override`
18
+ * boolean, for mandatory / read_only / default_value (plus calculation, dependent,
19
+ * reference_qual, attributes — outside this verb's five). The boolean is what
20
+ * ACTIVATES the override: a value written without its flag is inert.
21
+ *
22
+ * - There is NO max_length override and NO label override on that table. max_length is
23
+ * physical on the defining table's column, so it genuinely cannot be narrowed per
24
+ * child — that one refusal was right, for a reason the old message never gave.
25
+ *
26
+ * - LABEL is not an override at all; it is a sys_documentation row keyed on the CHILD
27
+ * table. Proof in the platform itself: `problem.short_description` reads "Problem
28
+ * statement" while `task.short_description` — the row that actually defines the
29
+ * column — still reads "Short description". sys_dictionary has no row for
30
+ * problem.short_description whatsoever. OOB ServiceNow does exactly what set-column
31
+ * said was impossible.
32
+ *
33
+ * - `base_table` is the table that DEFINES the column, NOT the immediate parent.
34
+ * Verified against a deep chain: cmdb_ci_endpoint_sharepoint_service extends
35
+ * cmdb_ci_endpoint_inclusion, yet its override rows carry base_table=cmdb_ci.
36
+ *
37
+ * - SCOPE FOLLOWS THE CHILD, not the parent. x_cadso_work_project's overrides of
38
+ * task's columns sit in the x_cadso scope while task is global; problem_task's sit
39
+ * in global. Resolving scope from the parent's dictionary row — the obvious move,
40
+ * and what choices.ts does for its own case — would land a scoped child's override
41
+ * in `global`. That is the exact wrong-scope failure Craftsman's CLAUDE.md calls out.
42
+ *
43
+ * CAPTURE NAMES ARE NOT ANALOGOUS, and this is a trap worth stating plainly. Three
44
+ * record types, three different sys_update_xml naming conventions:
45
+ *
46
+ * sys_dictionary sys_dictionary_<table>_<element>
47
+ * sys_dictionary_override sys_dictionary_override_<SYS_ID> <- keyed by sys_id
48
+ * sys_documentation sys_documentation_<table>_<element>_<language>
49
+ *
50
+ * Deriving the override's name by analogy with the other two produces a query that
51
+ * matches nothing, so a perfectly-captured change would report itself as uncaptured. Both
52
+ * names below were read off real update rows, not inferred.
53
+ *
54
+ * ES6 only, no optional chaining, no `any`.
55
+ */
56
+ Object.defineProperty(exports, "__esModule", { value: true });
57
+ exports.OVERRIDABLE = exports.LABEL_LANGUAGE = void 0;
58
+ exports.explainMaxLengthNotOverridable = explainMaxLengthNotOverridable;
59
+ exports.resolveTableScope = resolveTableScope;
60
+ exports.findOverrideRow = findOverrideRow;
61
+ exports.findLabelRow = findLabelRow;
62
+ exports.effectiveValue = effectiveValue;
63
+ exports.diffInherited = diffInherited;
64
+ exports.overrideUpdateName = overrideUpdateName;
65
+ exports.labelUpdateName = labelUpdateName;
66
+ exports.applyInheritedWrites = applyInheritedWrites;
67
+ const setField_1 = require("../setField");
68
+ const choices_1 = require("../choices");
69
+ /** The language a label override is written for. ServiceNow keys sys_documentation by
70
+ * language, and the capture name embeds it, so it is explicit rather than implied. */
71
+ exports.LABEL_LANGUAGE = "en";
72
+ /**
73
+ * The inherited attributes a child can narrow for itself, keyed by the sys_dictionary
74
+ * column set-column diffs on. Anything absent is NOT overridable per-child:
75
+ * - column_label -> sys_documentation instead (see applyLabelOverride)
76
+ * - max_length -> physical on the defining table; no override exists at all
77
+ */
78
+ exports.OVERRIDABLE = {
79
+ mandatory: { field: "mandatory", flag: "mandatory_override" },
80
+ default_value: { field: "default_value", flag: "default_value_override" },
81
+ read_only: { field: "read_only", flag: "read_only_override" },
82
+ };
83
+ /**
84
+ * Why max_length — alone among set-column's five attributes — cannot be narrowed on a
85
+ * child, and what that means for the caller.
86
+ *
87
+ * Deliberately max_length-specific rather than a general "attribute X is not
88
+ * overridable" helper: of the five, the other four ARE overridable, so a generic branch
89
+ * would be unreachable code speculating about a case that does not exist. If a sixth
90
+ * attribute is ever added to WRITABLE, this needs revisiting — which a dead generic
91
+ * branch would have quietly hidden.
92
+ *
93
+ * Unlike the old blanket refusal this one is true, and it says WHY rather than
94
+ * recommending the destructive alternative as though it were routine.
95
+ */
96
+ function explainMaxLengthNotOverridable(table, column, definedOn) {
97
+ return ("set-column: refusing to change max_length of '" +
98
+ column +
99
+ "' on '" +
100
+ table +
101
+ "' — the column is INHERITED from '" +
102
+ definedOn +
103
+ "'. max_length is PHYSICAL: it is " +
104
+ "the real database column on '" +
105
+ definedOn +
106
+ "', and sys_dictionary_override has no " +
107
+ "max_length field, so it cannot be narrowed for '" +
108
+ table +
109
+ "' alone. Changing it " +
110
+ "at the source (set-column --table " +
111
+ definedOn +
112
+ " --column " +
113
+ column +
114
+ ") resizes " +
115
+ "the column for EVERY table that extends '" +
116
+ definedOn +
117
+ "', so that is a deliberate " +
118
+ "decision and is not made on your behalf. The other attributes (label, mandatory, " +
119
+ "default, read-only) CAN be set on '" +
120
+ table +
121
+ "' alone and are applied as overrides.");
122
+ }
123
+ /** The scope the override belongs to — resolved from the CHILD table, never the parent.
124
+ * A scoped child overriding a global parent's column must land in the child's scope. */
125
+ async function resolveTableScope(client, table) {
126
+ var rows = await client.table.query("sys_db_object", "name=" + (0, choices_1.encodeQueryValue)(table), { limit: 1, fields: ["sys_scope"] });
127
+ if (rows.length === 0)
128
+ return "";
129
+ var scopeSysId = (0, setField_1.fieldToString)(rows[0].sys_scope);
130
+ if (!scopeSysId)
131
+ return "";
132
+ // "global" is already the namespace; sys_scope has no row to resolve it against.
133
+ if (scopeSysId === "global")
134
+ return "global";
135
+ var scopeRows = await client.table.query("sys_scope", "sys_id=" + (0, choices_1.encodeQueryValue)(scopeSysId), { limit: 1, fields: ["scope", "name"] });
136
+ if (scopeRows.length === 0)
137
+ return "";
138
+ return (0, setField_1.fieldToString)(scopeRows[0].scope) || (0, setField_1.fieldToString)(scopeRows[0].name);
139
+ }
140
+ /** The child's existing override row for this column, or null. */
141
+ async function findOverrideRow(client, table, column) {
142
+ var fields = ["sys_id", "name", "element", "base_table"];
143
+ var keys = Object.keys(exports.OVERRIDABLE);
144
+ for (var i = 0; i < keys.length; i += 1) {
145
+ fields.push(exports.OVERRIDABLE[keys[i]].field);
146
+ fields.push(exports.OVERRIDABLE[keys[i]].flag);
147
+ }
148
+ var rows = await client.table.query("sys_dictionary_override", "name=" + (0, choices_1.encodeQueryValue)(table) + "^element=" + (0, choices_1.encodeQueryValue)(column), { limit: 1, fields: fields });
149
+ return rows.length > 0 ? rows[0] : null;
150
+ }
151
+ /** The child's existing label row for this column, or null. */
152
+ async function findLabelRow(client, table, column) {
153
+ var rows = await client.table.query("sys_documentation", "name=" +
154
+ (0, choices_1.encodeQueryValue)(table) +
155
+ "^element=" +
156
+ (0, choices_1.encodeQueryValue)(column) +
157
+ "^language=" +
158
+ (0, choices_1.encodeQueryValue)(exports.LABEL_LANGUAGE), { limit: 1, fields: ["sys_id", "label", "language"] });
159
+ return rows.length > 0 ? rows[0] : null;
160
+ }
161
+ /**
162
+ * The value this child ACTUALLY sees for an attribute today.
163
+ *
164
+ * An override's value only counts when its flag is on. A row carrying mandatory=true with
165
+ * mandatory_override=false is inert — the child still inherits — so reading the value
166
+ * alone would diff against a number the platform is ignoring, and report `unchanged` for
167
+ * a column that behaves the opposite way.
168
+ */
169
+ function effectiveValue(field, overrideRow, parentRow, labelRow) {
170
+ // The label is not an override; it is the child's own sys_documentation row. When the
171
+ // child has none it simply shows the defining table's label, which sys_dictionary
172
+ // already surfaces as column_label on the parent row.
173
+ if (field === "column_label") {
174
+ if (labelRow)
175
+ return (0, setField_1.fieldToString)(labelRow.label);
176
+ return (0, setField_1.fieldToString)(parentRow.column_label);
177
+ }
178
+ var spec = exports.OVERRIDABLE[field];
179
+ if (spec && overrideRow) {
180
+ var flag = (0, setField_1.fieldToString)(overrideRow[spec.flag]);
181
+ if (flag === "true")
182
+ return (0, setField_1.fieldToString)(overrideRow[spec.field]);
183
+ }
184
+ return (0, setField_1.fieldToString)(parentRow[field]);
185
+ }
186
+ /**
187
+ * What would actually change for this child, comparing against the value it sees TODAY
188
+ * (its override if one is active, otherwise the inherited value).
189
+ *
190
+ * Note what is deliberately NOT treated as a change: a requested value that already
191
+ * matches the INHERITED one. Writing an override there would pin the child to a value it
192
+ * already has, decoupling it from the parent as an invisible side effect of a request
193
+ * that asked for no such thing. Reporting `unchanged` — and saying the column still
194
+ * tracks the parent — is the honest answer.
195
+ */
196
+ function diffInherited(writes, overrideRow, parentRow, labelRow) {
197
+ var changes = [];
198
+ var fields = Object.keys(writes);
199
+ for (var i = 0; i < fields.length; i += 1) {
200
+ var field = fields[i];
201
+ var from = effectiveValue(field, overrideRow, parentRow, labelRow);
202
+ if (from !== writes[field]) {
203
+ changes.push({ attribute: field, from: from, to: writes[field] });
204
+ }
205
+ }
206
+ return changes;
207
+ }
208
+ /** Update-set capture name for an override row — keyed by SYS_ID, unlike sys_dictionary.
209
+ * Read off real update rows on tenonworkshed; deriving it by analogy yields a query that
210
+ * matches nothing and reports a captured change as uncaptured. */
211
+ function overrideUpdateName(overrideSysId) {
212
+ return "sys_dictionary_override_" + overrideSysId;
213
+ }
214
+ /** Update-set capture name for a label row: sys_documentation_<table>_<element>_<lang>. */
215
+ function labelUpdateName(table, column) {
216
+ return "sys_documentation_" + table + "_" + column + "_" + exports.LABEL_LANGUAGE;
217
+ }
218
+ async function isCaptured(client, updateSetSysId, name) {
219
+ var rows = await client.table.query("sys_update_xml", "update_set=" +
220
+ (0, choices_1.encodeQueryValue)(updateSetSysId) +
221
+ "^name=" +
222
+ (0, choices_1.encodeQueryValue)(name), { limit: 1, fields: ["sys_id"] });
223
+ return rows.length > 0;
224
+ }
225
+ /**
226
+ * Write the child's overrides, then READ THEM BACK. The whole verb exists because
227
+ * ServiceNow returns 200 for writes it silently discards, and that reasoning does not
228
+ * stop applying just because the target table changed.
229
+ */
230
+ async function applyInheritedWrites(params) {
231
+ var client = params.client;
232
+ var overrideChanges = params.changes.filter(function (c) {
233
+ return Boolean(exports.OVERRIDABLE[c.attribute]);
234
+ });
235
+ var labelChange = params.changes.filter(function (c) {
236
+ return c.attribute === "column_label";
237
+ })[0];
238
+ // The sys_ids of any PRE-EXISTING rows — used only to TARGET an update, never reported.
239
+ // A label-only call must not claim it touched the override row that happened to already
240
+ // exist, and an override-only call must not claim it touched a pre-existing label row.
241
+ var existingOverrideSysId = params.overrideRow
242
+ ? (0, setField_1.fieldToString)(params.overrideRow.sys_id)
243
+ : "";
244
+ var existingLabelSysId = params.labelRow
245
+ ? (0, setField_1.fieldToString)(params.labelRow.sys_id)
246
+ : "";
247
+ // What this call actually WROTE. Stays "" unless the matching branch runs, so a returned
248
+ // sys_id names a row THIS call touched — honouring the InheritedWriteResult docstrings,
249
+ // which promise "" when the attribute did not change.
250
+ var overrideSysId = "";
251
+ var labelSysId = "";
252
+ // Scope follows the CHILD — taking it from the parent would drop a scoped child's
253
+ // override into global. Resolved at most once and only when a record is actually
254
+ // created: a mixed label+mandatory request needs the same answer twice, and an update
255
+ // to existing rows needs it not at all.
256
+ var scopeMemo = null;
257
+ async function childScope() {
258
+ if (scopeMemo === null) {
259
+ scopeMemo = await resolveTableScope(client, params.table);
260
+ }
261
+ return scopeMemo ? scopeMemo : undefined;
262
+ }
263
+ // --- sys_dictionary_override -------------------------------------------------------
264
+ if (overrideChanges.length > 0) {
265
+ var overrideFields = {};
266
+ for (var i = 0; i < overrideChanges.length; i += 1) {
267
+ var spec = exports.OVERRIDABLE[overrideChanges[i].attribute];
268
+ overrideFields[spec.field] = overrideChanges[i].to;
269
+ // The flag is what makes the value count. Writing the value alone leaves the child
270
+ // inheriting while the row claims otherwise.
271
+ overrideFields[spec.flag] = "true";
272
+ }
273
+ if (existingOverrideSysId) {
274
+ await client.claude.pushWithUpdateSet({
275
+ update_set_sys_id: params.updateSetSysId,
276
+ table: "sys_dictionary_override",
277
+ record_sys_id: existingOverrideSysId,
278
+ fields: overrideFields,
279
+ });
280
+ overrideSysId = existingOverrideSysId;
281
+ }
282
+ else {
283
+ overrideFields.name = params.table;
284
+ overrideFields.element = params.column;
285
+ // base_table is the DEFINING table, not the immediate parent (verified live).
286
+ overrideFields.base_table = params.definedOn;
287
+ var created = await client.claude.createRecord({
288
+ table: "sys_dictionary_override",
289
+ fields: overrideFields,
290
+ scope: await childScope(),
291
+ update_set_sys_id: params.updateSetSysId,
292
+ });
293
+ overrideSysId = (0, setField_1.fieldToString)(created.sys_id);
294
+ }
295
+ }
296
+ // --- sys_documentation (label) -----------------------------------------------------
297
+ if (labelChange) {
298
+ if (existingLabelSysId) {
299
+ await client.claude.pushWithUpdateSet({
300
+ update_set_sys_id: params.updateSetSysId,
301
+ table: "sys_documentation",
302
+ record_sys_id: existingLabelSysId,
303
+ fields: { label: labelChange.to },
304
+ });
305
+ labelSysId = existingLabelSysId;
306
+ }
307
+ else {
308
+ var createdLabel = await client.claude.createRecord({
309
+ table: "sys_documentation",
310
+ fields: {
311
+ name: params.table,
312
+ element: params.column,
313
+ label: labelChange.to,
314
+ language: exports.LABEL_LANGUAGE,
315
+ },
316
+ scope: await childScope(),
317
+ update_set_sys_id: params.updateSetSysId,
318
+ });
319
+ labelSysId = (0, setField_1.fieldToString)(createdLabel.sys_id);
320
+ }
321
+ }
322
+ // --- read back ---------------------------------------------------------------------
323
+ var mismatched = [];
324
+ if (overrideChanges.length > 0) {
325
+ var afterOverride = await findOverrideRow(client, params.table, params.column);
326
+ if (!afterOverride) {
327
+ mismatched.push("no sys_dictionary_override row for " +
328
+ params.table +
329
+ "." +
330
+ params.column +
331
+ " could be read back after the write");
332
+ }
333
+ else {
334
+ for (var j = 0; j < overrideChanges.length; j += 1) {
335
+ var oSpec = exports.OVERRIDABLE[overrideChanges[j].attribute];
336
+ var got = (0, setField_1.fieldToString)(afterOverride[oSpec.field]);
337
+ var gotFlag = (0, setField_1.fieldToString)(afterOverride[oSpec.flag]);
338
+ if (got !== overrideChanges[j].to) {
339
+ mismatched.push(overrideChanges[j].attribute +
340
+ " reads back as '" +
341
+ got +
342
+ "', not '" +
343
+ overrideChanges[j].to +
344
+ "'");
345
+ }
346
+ else if (gotFlag !== "true") {
347
+ // The value landed but the override is not switched on, so the child still
348
+ // inherits. Nothing visibly failed, and the column does not behave as asked.
349
+ mismatched.push(oSpec.flag +
350
+ " reads back as '" +
351
+ gotFlag +
352
+ "', so the " +
353
+ overrideChanges[j].attribute +
354
+ " override is INERT and " +
355
+ params.table +
356
+ " still inherits from " +
357
+ params.definedOn);
358
+ }
359
+ }
360
+ }
361
+ }
362
+ if (labelChange) {
363
+ var afterLabel = await findLabelRow(client, params.table, params.column);
364
+ var gotLabel = afterLabel ? (0, setField_1.fieldToString)(afterLabel.label) : "";
365
+ if (gotLabel !== labelChange.to) {
366
+ mismatched.push("label reads back as '" + gotLabel + "', not '" + labelChange.to + "'");
367
+ }
368
+ }
369
+ // --- capture -----------------------------------------------------------------------
370
+ var captured = true;
371
+ if (overrideChanges.length > 0 && overrideSysId) {
372
+ captured =
373
+ captured &&
374
+ (await isCaptured(client, params.updateSetSysId, overrideUpdateName(overrideSysId)));
375
+ }
376
+ if (labelChange) {
377
+ captured =
378
+ captured &&
379
+ (await isCaptured(client, params.updateSetSysId, labelUpdateName(params.table, params.column)));
380
+ }
381
+ return {
382
+ overrideSysId: overrideSysId,
383
+ labelSysId: labelSysId,
384
+ verified: mismatched.length === 0,
385
+ mismatched: mismatched,
386
+ captured: captured,
387
+ };
388
+ }
@@ -50,6 +50,19 @@
50
50
  * do nothing. That is why every write is read back and compared, and why nothing is ever
51
51
  * reported as applied on the strength of a status code.
52
52
  *
53
+ * INHERITED COLUMNS. On an extended table the column is defined on an ancestor, not here.
54
+ * That is not an error and not a dead end: mandatory / default / read_only are narrowed
55
+ * for THIS table alone via sys_dictionary_override, and label via sys_documentation,
56
+ * leaving the ancestor and every sibling untouched. See overrideColumn.ts, which carries
57
+ * the live-verified detail. max_length is the one real exception — it is the ancestor's
58
+ * physical column and has no per-child override, so it is refused with the reason.
59
+ *
60
+ * This verb originally refused every inherited column and told the caller to go edit the
61
+ * ancestor "since that changes it for EVERY table that extends" it. That was the
62
+ * destructive option offered as the only one, and it was never checked against an
63
+ * instance — a claim about ServiceNow trusted on reasoning alone, in the one file whose
64
+ * whole argument is that ServiceNow must be read back rather than believed.
65
+ *
53
66
  * Fields are taken from a strict ALLOWLIST, not an open map — an unbounded write to
54
67
  * sys_dictionary lets a caller quietly corrupt the schema.
55
68
  *
@@ -95,8 +108,21 @@ export interface SetColumnResult {
95
108
  status: "dry-run" | "applied" | "unchanged" | "failed";
96
109
  table: string;
97
110
  column: string;
98
- /** sys_id of the sys_dictionary row. */
111
+ /** sys_id of the sys_dictionary row that DEFINES the column. On an inherited column
112
+ * this is the ancestor's row — the child has none — so it is not what was written;
113
+ * see overrideSysId / labelSysId for that. */
99
114
  columnSysId: string;
115
+ /** How the change was made. "dictionary" — written straight to the column's own
116
+ * sys_dictionary row. "override" — the column is inherited, so it was narrowed for
117
+ * THIS table alone via sys_dictionary_override / sys_documentation, leaving the
118
+ * defining table and every sibling untouched. */
119
+ via: "dictionary" | "override";
120
+ /** The ancestor that defines the column, when it is inherited; "" when it is local. */
121
+ definedOn: string;
122
+ /** sys_id of the sys_dictionary_override row written, when via === "override". */
123
+ overrideSysId?: string;
124
+ /** sys_id of the sys_documentation row written, when a label override was applied. */
125
+ labelSysId?: string;
100
126
  updateSetSysId: string;
101
127
  /** The attributes that differed and were written (empty when nothing changed). */
102
128
  changes: Array<AttributeChange>;
@@ -51,6 +51,19 @@
51
51
  * do nothing. That is why every write is read back and compared, and why nothing is ever
52
52
  * reported as applied on the strength of a status code.
53
53
  *
54
+ * INHERITED COLUMNS. On an extended table the column is defined on an ancestor, not here.
55
+ * That is not an error and not a dead end: mandatory / default / read_only are narrowed
56
+ * for THIS table alone via sys_dictionary_override, and label via sys_documentation,
57
+ * leaving the ancestor and every sibling untouched. See overrideColumn.ts, which carries
58
+ * the live-verified detail. max_length is the one real exception — it is the ancestor's
59
+ * physical column and has no per-child override, so it is refused with the reason.
60
+ *
61
+ * This verb originally refused every inherited column and told the caller to go edit the
62
+ * ancestor "since that changes it for EVERY table that extends" it. That was the
63
+ * destructive option offered as the only one, and it was never checked against an
64
+ * instance — a claim about ServiceNow trusted on reasoning alone, in the one file whose
65
+ * whole argument is that ServiceNow must be read back rather than believed.
66
+ *
54
67
  * Fields are taken from a strict ALLOWLIST, not an open map — an unbounded write to
55
68
  * sys_dictionary lets a caller quietly corrupt the schema.
56
69
  *
@@ -66,6 +79,10 @@ const setField_1 = require("../setField");
66
79
  // through this first. A stray "^" or "=" does not error — it silently changes what the
67
80
  // query MEANS, which is the worst kind of bug to ship into a schema tool.
68
81
  const choices_1 = require("../choices");
82
+ // The inherited-column path. An extended table's columns live on an ancestor's
83
+ // dictionary rows, and ServiceNow's answer for narrowing one per-child is
84
+ // sys_dictionary_override / sys_documentation — not editing the ancestor.
85
+ const overrideColumn_1 = require("./overrideColumn");
69
86
  /** How far up a super_class chain to look for an inherited column before giving up. */
70
87
  var MAX_INHERITANCE_DEPTH = 20;
71
88
  /**
@@ -157,8 +174,12 @@ function resolveAttributes(attributes) {
157
174
  * child's — a child of x_cadso_journey_flow carries only a couple of dictionary rows of
158
175
  * its own while every real column is inherited. So a plain name+element lookup against
159
176
  * the child finds nothing, even though the column is plainly there on the form. Walking
160
- * super_class turns "no such column" into "it lives on <parent>", which is the
161
- * difference between a dead end and an answer.
177
+ * super_class finds the row that really defines it.
178
+ *
179
+ * That answer used to end the story: set-column reported "it lives on <parent>, go change
180
+ * it there" and stopped. It now ROUTES instead — the defining table is where the column's
181
+ * inherited values are read from for the diff, and what `base_table` is set to on the
182
+ * override that narrows it for the child. See overrideColumn.ts.
162
183
  */
163
184
  async function findDefiningTable(client, table, column) {
164
185
  var current = table;
@@ -185,31 +206,31 @@ async function findDefiningTable(client, table, column) {
185
206
  }
186
207
  return "";
187
208
  }
188
- /** Fetch the column's dictionary row, or throw a message that says what to check. */
189
- async function fetchColumn(client, table, column, fields) {
209
+ /**
210
+ * Find the column's dictionary row — on the table itself, or on the ancestor that
211
+ * defines it — or throw a message that says what to check.
212
+ *
213
+ * An inherited column is NOT an error. It is the normal shape of an extended table, and
214
+ * the caller's request ("make this column mandatory on this table") is answerable
215
+ * exactly as asked, by overriding it for the child. Resolution just reports where the
216
+ * definition lives; setColumn decides what to write.
217
+ */
218
+ async function resolveColumn(client, table, column, fields) {
190
219
  var rows = await client.table.query("sys_dictionary", "name=" + (0, choices_1.encodeQueryValue)(table) + "^element=" + (0, choices_1.encodeQueryValue)(column), { limit: 1, fields: fields });
191
220
  if (rows.length > 0)
192
- return rows[0];
221
+ return { row: rows[0], definedOn: "" };
193
222
  // Before declaring the column missing, check whether it is simply inherited. Saying
194
223
  // "no such column" about a column the caller can see on the form sends them hunting
195
224
  // for a typo that is not there.
196
225
  var owner = await findDefiningTable(client, table, column);
197
226
  if (owner) {
198
- throw new Error("set-column: '" +
199
- column +
200
- "' is not defined on '" +
201
- table +
202
- "' — it is INHERITED from '" +
203
- owner +
204
- "'. Change it there: set-column --table " +
205
- owner +
206
- " --column " +
207
- column +
208
- ". Note that doing so changes the column for EVERY table that extends " +
209
- owner +
210
- ", not just " +
211
- table +
212
- " — which is why this is not done implicitly on your behalf.");
227
+ var ownerRows = await client.table.query("sys_dictionary", "name=" +
228
+ (0, choices_1.encodeQueryValue)(owner) +
229
+ "^element=" +
230
+ (0, choices_1.encodeQueryValue)(column), { limit: 1, fields: fields });
231
+ if (ownerRows.length > 0) {
232
+ return { row: ownerRows[0], definedOn: owner };
233
+ }
213
234
  }
214
235
  throw new Error("set-column: no column '" +
215
236
  column +
@@ -357,9 +378,15 @@ async function setColumn(params) {
357
378
  var table = String(params.table).trim();
358
379
  var column = String(params.column).trim();
359
380
  var readFields = ["sys_id", "element", "internal_type"].concat(targets);
360
- var row = await fetchColumn(params.client, table, column, readFields);
381
+ var resolved = await resolveColumn(params.client, table, column, readFields);
382
+ var row = resolved.row;
361
383
  // max_length is meaningless on a type that has no length. ServiceNow would take the
362
384
  // write and ignore it (a fourth silent no-op), so refuse it here where we can say why.
385
+ //
386
+ // This runs BEFORE the inherited branch on purpose. The type is a property of the
387
+ // COLUMN, so it is wrong wherever the column is defined — and the inherited refusal
388
+ // says "change it at the source instead", which for a lengthless type would send the
389
+ // caller to the parent to attempt something that cannot work there either.
363
390
  if (writes.max_length !== undefined) {
364
391
  var columnType = (0, setField_1.fieldToString)(row.internal_type);
365
392
  if (LENGTHLESS_TYPES.indexOf(columnType) !== -1) {
@@ -371,6 +398,17 @@ async function setColumn(params) {
371
398
  "ServiceNow would accept the write and ignore it. Drop --max-length.");
372
399
  }
373
400
  }
401
+ // The column is INHERITED. It is narrowed for this table alone via an override — never
402
+ // by editing the ancestor, which would change it for every sibling too.
403
+ if (resolved.definedOn) {
404
+ return await setInheritedColumn(params, {
405
+ table: table,
406
+ column: column,
407
+ writes: writes,
408
+ parentRow: row,
409
+ definedOn: resolved.definedOn,
410
+ });
411
+ }
374
412
  var columnSysId = (0, setField_1.fieldToString)(row.sys_id);
375
413
  // Diff against what the instance actually stores. Only a genuine difference is
376
414
  // written: ServiceNow fires the physical ALTER on a CHANGE, and a same-value write
@@ -428,6 +466,8 @@ async function setColumn(params) {
428
466
  table: table,
429
467
  column: column,
430
468
  columnSysId: columnSysId,
469
+ via: "dictionary",
470
+ definedOn: "",
431
471
  updateSetSysId: params.updateSetSysId ? params.updateSetSysId : "",
432
472
  changes: changes,
433
473
  verified: false,
@@ -479,6 +519,8 @@ async function setColumn(params) {
479
519
  table: table,
480
520
  column: column,
481
521
  columnSysId: columnSysId,
522
+ via: "dictionary",
523
+ definedOn: "",
482
524
  updateSetSysId: updateSetSysId,
483
525
  changes: [],
484
526
  verified: true,
@@ -515,6 +557,8 @@ async function setColumn(params) {
515
557
  table: table,
516
558
  column: column,
517
559
  columnSysId: columnSysId,
560
+ via: "dictionary",
561
+ definedOn: "",
518
562
  updateSetSysId: updateSetSysId,
519
563
  changes: changes,
520
564
  verified: false,
@@ -538,7 +582,7 @@ async function setColumn(params) {
538
582
  var after;
539
583
  var captured;
540
584
  try {
541
- after = await fetchColumn(params.client, table, column, readFields);
585
+ after = (await resolveColumn(params.client, table, column, readFields)).row;
542
586
  captured = await assertCaptured(params.client, table, column, updateSetSysId);
543
587
  }
544
588
  catch (e) {
@@ -547,6 +591,8 @@ async function setColumn(params) {
547
591
  table: table,
548
592
  column: column,
549
593
  columnSysId: columnSysId,
594
+ via: "dictionary",
595
+ definedOn: "",
550
596
  updateSetSysId: updateSetSysId,
551
597
  changes: changes,
552
598
  verified: false,
@@ -577,6 +623,8 @@ async function setColumn(params) {
577
623
  table: table,
578
624
  column: column,
579
625
  columnSysId: columnSysId,
626
+ via: "dictionary",
627
+ definedOn: "",
580
628
  updateSetSysId: updateSetSysId,
581
629
  changes: changes,
582
630
  verified: false,
@@ -603,6 +651,8 @@ async function setColumn(params) {
603
651
  table: table,
604
652
  column: column,
605
653
  columnSysId: columnSysId,
654
+ via: "dictionary",
655
+ definedOn: "",
606
656
  updateSetSysId: updateSetSysId,
607
657
  changes: changes,
608
658
  verified: true,
@@ -629,6 +679,201 @@ async function setColumn(params) {
629
679
  "promoted. Check the update set is in progress and in the column's scope.",
630
680
  };
631
681
  }
682
+ /**
683
+ * Set an INHERITED column's attributes for this table alone.
684
+ *
685
+ * The caller asked to change a column on a child table. ServiceNow's answer to that is an
686
+ * override on the child — not an edit to the ancestor, which would silently change the
687
+ * column for every other table extending it. Four of the five attributes set-column
688
+ * supports can be narrowed this way; max_length cannot, because it is the ancestor's
689
+ * physical column, and that is the one case where "change it at the source" is the honest
690
+ * answer (given with its blast radius spelled out, rather than as a casual suggestion).
691
+ */
692
+ async function setInheritedColumn(params, ctx) {
693
+ var columnSysId = (0, setField_1.fieldToString)(ctx.parentRow.sys_id);
694
+ // Refuse before touching anything else, so a dry-run fails identically to a live run.
695
+ if (ctx.writes.max_length !== undefined) {
696
+ throw new Error((0, overrideColumn_1.explainMaxLengthNotOverridable)(ctx.table, ctx.column, ctx.definedOn));
697
+ }
698
+ var overrideRow = await (0, overrideColumn_1.findOverrideRow)(params.client, ctx.table, ctx.column);
699
+ var labelRow = await (0, overrideColumn_1.findLabelRow)(params.client, ctx.table, ctx.column);
700
+ var changes = (0, overrideColumn_1.diffInherited)(ctx.writes, overrideRow, ctx.parentRow, labelRow);
701
+ var base = {
702
+ table: ctx.table,
703
+ column: ctx.column,
704
+ columnSysId: columnSysId,
705
+ via: "override",
706
+ definedOn: ctx.definedOn,
707
+ };
708
+ if (params.dryRun) {
709
+ return Object.assign({}, base, {
710
+ status: "dry-run",
711
+ updateSetSysId: params.updateSetSysId ? params.updateSetSysId : "",
712
+ changes: changes,
713
+ verified: false,
714
+ capturedInUpdateSet: false,
715
+ note: changes.length === 0
716
+ ? "dry-run: no write. " +
717
+ ctx.column +
718
+ " is inherited from " +
719
+ ctx.definedOn +
720
+ " and already presents every requested value on " +
721
+ ctx.table +
722
+ " — nothing would change."
723
+ : "dry-run: no write. " +
724
+ ctx.column +
725
+ " is inherited from " +
726
+ ctx.definedOn +
727
+ "; would override " +
728
+ describeChanges(changes) +
729
+ " for " +
730
+ ctx.table +
731
+ " ALONE (" +
732
+ describeTargets(changes) +
733
+ "), leaving " +
734
+ ctx.definedOn +
735
+ " and its other children untouched. Captured into update set " +
736
+ (params.updateSetSysId
737
+ ? params.updateSetSysId
738
+ : "(none provided)") +
739
+ ".",
740
+ });
741
+ }
742
+ if (!params.updateSetSysId || !String(params.updateSetSysId).trim()) {
743
+ throw new Error("set-column: --update-set <sys_id> is required so the schema change is captured " +
744
+ "and can be promoted. An uncaptured change exists only on this instance.");
745
+ }
746
+ var updateSetSysId = String(params.updateSetSysId).trim();
747
+ await assertUpdateSetOpen(params.client, updateSetSysId);
748
+ // Nothing differs from what the child already sees. Note WHY no override was written:
749
+ // pinning a value the column already inherits would decouple it from the ancestor as an
750
+ // invisible side effect of a request that never asked for that.
751
+ if (changes.length === 0) {
752
+ return Object.assign({}, base, {
753
+ status: "unchanged",
754
+ updateSetSysId: updateSetSysId,
755
+ changes: [],
756
+ verified: true,
757
+ capturedInUpdateSet: false,
758
+ note: ctx.table +
759
+ "." +
760
+ ctx.column +
761
+ " already presents every requested value — nothing written. The column is " +
762
+ "inherited from " +
763
+ ctx.definedOn +
764
+ " and no override was created, so it still TRACKS " +
765
+ ctx.definedOn +
766
+ ": a later change there will follow through to " +
767
+ ctx.table +
768
+ ".",
769
+ });
770
+ }
771
+ var written;
772
+ try {
773
+ written = await (0, overrideColumn_1.applyInheritedWrites)({
774
+ client: params.client,
775
+ table: ctx.table,
776
+ column: ctx.column,
777
+ definedOn: ctx.definedOn,
778
+ changes: changes,
779
+ overrideRow: overrideRow,
780
+ labelRow: labelRow,
781
+ updateSetSysId: updateSetSysId,
782
+ });
783
+ }
784
+ catch (e) {
785
+ return Object.assign({}, base, {
786
+ status: "failed",
787
+ updateSetSysId: updateSetSysId,
788
+ changes: changes,
789
+ verified: false,
790
+ capturedInUpdateSet: false,
791
+ note: "the override write for " +
792
+ ctx.table +
793
+ "." +
794
+ ctx.column +
795
+ " failed: " +
796
+ (e && e.message ? e.message : String(e)) +
797
+ ". It is NOT known whether it landed — read sys_dictionary_override for name=" +
798
+ ctx.table +
799
+ "^element=" +
800
+ ctx.column +
801
+ " on the instance before retrying.",
802
+ });
803
+ }
804
+ if (!written.verified) {
805
+ return Object.assign({}, base, {
806
+ status: "failed",
807
+ updateSetSysId: updateSetSysId,
808
+ overrideSysId: written.overrideSysId,
809
+ labelSysId: written.labelSysId,
810
+ changes: changes,
811
+ verified: false,
812
+ capturedInUpdateSet: written.captured,
813
+ note: "the override write returned success but the instance does NOT reflect it: " +
814
+ written.mismatched.join("; ") +
815
+ ". Treat " +
816
+ ctx.table +
817
+ "." +
818
+ ctx.column +
819
+ " as NOT overridden and reconcile it on the instance.",
820
+ });
821
+ }
822
+ return Object.assign({}, base, {
823
+ status: "applied",
824
+ updateSetSysId: updateSetSysId,
825
+ overrideSysId: written.overrideSysId,
826
+ labelSysId: written.labelSysId,
827
+ changes: changes,
828
+ verified: true,
829
+ capturedInUpdateSet: written.captured,
830
+ note: written.captured
831
+ ? "Overrode " +
832
+ describeChanges(changes) +
833
+ " for " +
834
+ ctx.table +
835
+ "." +
836
+ ctx.column +
837
+ " — inherited from " +
838
+ ctx.definedOn +
839
+ ", now narrowed for " +
840
+ ctx.table +
841
+ " ALONE (" +
842
+ describeTargets(changes) +
843
+ "); " +
844
+ ctx.definedOn +
845
+ " and its other children are unchanged. Verified by read-back and captured in " +
846
+ "update set " +
847
+ updateSetSysId +
848
+ "."
849
+ : "Overrode " +
850
+ describeChanges(changes) +
851
+ " for " +
852
+ ctx.table +
853
+ "." +
854
+ ctx.column +
855
+ " and verified by read-back, but the change was NOT captured in update set " +
856
+ updateSetSysId +
857
+ " — it is live on this instance and cannot be promoted. Check the update set is " +
858
+ "in progress and in " +
859
+ ctx.table +
860
+ "'s scope.",
861
+ });
862
+ }
863
+ /** Which record type each change was written to — the two are not interchangeable, and
864
+ * a reader who does not know that will go looking for a label on the override row. */
865
+ function describeTargets(changes) {
866
+ var hasOverride = changes.some(function (c) {
867
+ return Boolean(overrideColumn_1.OVERRIDABLE[c.attribute]);
868
+ });
869
+ var hasLabel = changes.some(function (c) {
870
+ return c.attribute === "column_label";
871
+ });
872
+ if (hasOverride && hasLabel) {
873
+ return "sys_dictionary_override + sys_documentation";
874
+ }
875
+ return hasLabel ? "sys_documentation" : "sys_dictionary_override";
876
+ }
632
877
  /** "max_length 40 -> 4000, mandatory false -> true" */
633
878
  function describeChanges(changes) {
634
879
  return changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-servicenow",
3
- "version": "0.0.33",
3
+ "version": "0.0.34",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },