in2-agent-server 0.20260821.185429
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 +369 -0
- package/agents/plan-requirement.md +32 -0
- package/dist/campfire/configItem.js +201 -0
- package/dist/campfire/configItemDescription.js +379 -0
- package/dist/campfire/deriveFromBundle.js +1013 -0
- package/dist/cli.js +465 -0
- package/dist/core/betaFeedback.js +493 -0
- package/dist/core/chapterInstructions.js +473 -0
- package/dist/core/claudeAssets.js +61 -0
- package/dist/core/gate.js +201 -0
- package/dist/core/ids.js +9 -0
- package/dist/core/install.js +60 -0
- package/dist/core/operatorQa.js +462 -0
- package/dist/core/orgCallMeter.js +178 -0
- package/dist/core/parcels.js +1235 -0
- package/dist/core/paths.js +105 -0
- package/dist/core/phases.js +225 -0
- package/dist/core/planFile.js +251 -0
- package/dist/core/planFragments.js +119 -0
- package/dist/core/readsLog.js +398 -0
- package/dist/core/registration.js +24 -0
- package/dist/core/result.js +38 -0
- package/dist/core/resume.js +233 -0
- package/dist/core/revisionChain.js +351 -0
- package/dist/core/richText.js +61 -0
- package/dist/core/semanticHash.js +89 -0
- package/dist/core/sessionOrg.js +60 -0
- package/dist/core/sfQuery.js +19 -0
- package/dist/core/spawn.js +182 -0
- package/dist/core/supervisorContract.js +230 -0
- package/dist/core/telemetry.js +1297 -0
- package/dist/core/telemetryContext.js +154 -0
- package/dist/core/telemetrySpool.js +244 -0
- package/dist/core/transcriptUpload.js +516 -0
- package/dist/core/update.js +363 -0
- package/dist/core/viewer.js +117 -0
- package/dist/core/workspace.js +92 -0
- package/dist/core/writeGrants.js +93 -0
- package/dist/core/writebackManifest.js +92 -0
- package/dist/direct/applyLoop.js +2516 -0
- package/dist/direct/batchReport.js +510 -0
- package/dist/direct/comparatorNoise.js +75 -0
- package/dist/direct/containers.js +85 -0
- package/dist/direct/contamination.js +221 -0
- package/dist/direct/crud.js +56 -0
- package/dist/direct/destructive.js +168 -0
- package/dist/direct/knownTokens.js +94 -0
- package/dist/direct/lint.js +382 -0
- package/dist/direct/metadataSchema.generated.json +1 -0
- package/dist/direct/metadataSchema.js +179 -0
- package/dist/direct/operatorPage.js +349 -0
- package/dist/direct/orgRepo.js +1531 -0
- package/dist/direct/orgRepoSpawnCount.js +21 -0
- package/dist/direct/plannedScope.js +119 -0
- package/dist/direct/preImageTargets.js +63 -0
- package/dist/direct/refVerify.js +191 -0
- package/dist/direct/registry.js +129 -0
- package/dist/direct/removals.js +262 -0
- package/dist/direct/rollback.js +1513 -0
- package/dist/direct/rollbackRun.js +1360 -0
- package/dist/direct/sessionProjection.js +126 -0
- package/dist/direct/snapshotComparability.js +165 -0
- package/dist/direct/sourceTree.js +113 -0
- package/dist/direct/unstatedChanges.js +106 -0
- package/dist/direct/waveAssembly.js +556 -0
- package/dist/direct/waveCounts.js +19 -0
- package/dist/direct/waveDerivation.js +152 -0
- package/dist/direct/waveGrade.js +185 -0
- package/dist/index.js +90 -0
- package/dist/orgread/customFieldSnapshot.js +48 -0
- package/dist/orgread/fieldMetadata.js +178 -0
- package/dist/orgread/genericSnapshot.js +61 -0
- package/dist/orgread/layoutSnapshot.js +151 -0
- package/dist/orgread/salesforceOrgReader.js +184 -0
- package/dist/orgread/snapshotTypes.js +9 -0
- package/dist/projection/bridge.js +26 -0
- package/dist/projection/diff.js +141 -0
- package/dist/projection/mapmd.js +213 -0
- package/dist/projection/nameAmbiguity.js +106 -0
- package/dist/projection/precedent.js +136 -0
- package/dist/projection/project.js +214 -0
- package/dist/projection/scope.js +110 -0
- package/dist/projection/types.js +5 -0
- package/dist/tools/analyze.js +322 -0
- package/dist/tools/auth.js +915 -0
- package/dist/tools/campfire.js +1742 -0
- package/dist/tools/closeBatch.js +154 -0
- package/dist/tools/closeText.js +35 -0
- package/dist/tools/direct.js +1613 -0
- package/dist/tools/feedback.js +318 -0
- package/dist/tools/operator.js +287 -0
- package/dist/tools/org.js +739 -0
- package/dist/tools/phase.js +249 -0
- package/dist/tools/projection.js +172 -0
- package/dist/tools/session.js +313 -0
- package/dist/tools/update.js +82 -0
- package/dist/tools/waves.js +793 -0
- package/package.json +48 -0
- package/reference/campfire-configuration-items-guide.md +710 -0
- package/reference/deployability-cribs.md +119 -0
- package/reference/operator-voice.md +141 -0
- package/reference/org-read-cribs.md +34 -0
- package/reference/scoped-dml-interim.md +68 -0
- package/reference/supervisor-playbook.md +313 -0
- package/reference/templates.md +233 -0
- package/skills/in2-delivery/SKILL.md +322 -0
- package/skills/in2-delivery/references/campfire-configuration-items-guide.md +710 -0
- package/skills/in2-delivery/references/deployability-cribs.md +119 -0
- package/skills/in2-delivery/references/operator-voice.md +141 -0
- package/skills/in2-delivery/references/org-read-cribs.md +34 -0
- package/skills/in2-delivery/references/scoped-dml-interim.md +68 -0
- package/skills/in2-delivery/references/templates.md +233 -0
- package/skills/plan-requirement/SKILL.md +188 -0
- package/viewer/index.html +899 -0
- package/viewer/server.mjs +433 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { richTextFragment } from "../core/richText.js";
|
|
2
|
+
/** Values past this many are summarised as a count — a 50-value picklist is not a description. */
|
|
3
|
+
const MAX_PROJECTED_VALUES = 12;
|
|
4
|
+
/**
|
|
5
|
+
* Description__c's hard cap. The Configuration Item write is a per-row upsert with per-row failure
|
|
6
|
+
* isolation (see `campfire.ts`'s `campfire_write_config_items`, ~1636-1685) — one oversized row fails
|
|
7
|
+
* on its own, not the whole manifest. But this codebase has already lost a whole batch to exactly this
|
|
8
|
+
* failure class once, on a DIFFERENT all-or-none call: CF63, where one oversized `Blocked_Reason__c`
|
|
9
|
+
* refused every row of an sObject-Collections `update_campfire_requirements` call. The length cap here
|
|
10
|
+
* keeps that failure class from recurring on this writer too. `describeConfigItem` must never return a
|
|
11
|
+
* string longer than this, for any input — see `capFragment` below.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_DESCRIPTION_CHARS = 32_768;
|
|
14
|
+
/**
|
|
15
|
+
* Cap on any single caller-supplied value interpolated into a sentence — a formula, or one side of
|
|
16
|
+
* an observed/intended pair. A Salesforce formula can run to 3,900 characters and a deviation list
|
|
17
|
+
* can carry many of these; bounding each value keeps one outsized field from dominating the row.
|
|
18
|
+
*/
|
|
19
|
+
const MAX_VALUE_CHARS = 240;
|
|
20
|
+
/** Truncates a single interpolated value; unchanged when it is already within the cap. */
|
|
21
|
+
function truncateValue(value) {
|
|
22
|
+
return value.length > MAX_VALUE_CHARS ? `${value.slice(0, MAX_VALUE_CHARS)}…` : value;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Flow `start.triggerType` values that mean "this flow runs against a record save/delete" — the only
|
|
26
|
+
* values a "Record-triggered on X" sentence is true for. `start.object` is also present on scheduled
|
|
27
|
+
* flows (alongside `start.schedule`), so presence of an object alone never implies record-triggering;
|
|
28
|
+
* see the `Flow` case in `projectConfiguration` below.
|
|
29
|
+
*/
|
|
30
|
+
const RECORD_TRIGGER_TYPES = new Set(["RecordBeforeSave", "RecordBeforeDelete", "RecordAfterSave"]);
|
|
31
|
+
/**
|
|
32
|
+
* Cap on the number of DEVIATION sentences. A Layout or Profile row can differ on dozens of leaves
|
|
33
|
+
* at once, and a description enumerating all of them stops being a description — the remainder is
|
|
34
|
+
* named as a count instead of dropped silently.
|
|
35
|
+
*/
|
|
36
|
+
const MAX_DEVIATION_SENTENCES = 20;
|
|
37
|
+
/**
|
|
38
|
+
* Paths whose value in Salesforce settles asynchronously, so a post-deploy read can outrun it.
|
|
39
|
+
*
|
|
40
|
+
* Live-caught on the 08-02 A_Agreement__c batch: an OWD-private wave read back
|
|
41
|
+
* `sharingModel=ReadWrite` because the sharing recalculation had not finished. The difference was
|
|
42
|
+
* lag, not a deviation — so the hint rides only on these paths, and a blanket hint would teach the
|
|
43
|
+
* reader to discount every real deviation too.
|
|
44
|
+
*/
|
|
45
|
+
const ASYNC_ORG_PATH_RE = /sharingModel/i;
|
|
46
|
+
const ASYNC_DIFFERENCE_HINT = "This setting applies asynchronously — re-read the org once recalculation settles before treating this as a real difference.";
|
|
47
|
+
/**
|
|
48
|
+
* The noun phrase for a type, in the words a consultant would use.
|
|
49
|
+
*
|
|
50
|
+
* Keyed by CAMPFIRE type. A type absent here falls back to a spaced-out form of its own name, which
|
|
51
|
+
* is correct often enough ("PathAssistant" -> "path assistant") to beat maintaining 55 entries.
|
|
52
|
+
*/
|
|
53
|
+
const TYPE_NOUN = {
|
|
54
|
+
ApexClass: "Apex class",
|
|
55
|
+
ApexPage: "Visualforce page",
|
|
56
|
+
ApexTrigger: "Apex trigger",
|
|
57
|
+
App: "app",
|
|
58
|
+
ApprovalProcess: "approval process",
|
|
59
|
+
CompactLayout: "compact layout",
|
|
60
|
+
CustomField: "field",
|
|
61
|
+
CustomLabel: "custom label",
|
|
62
|
+
CustomMetadata: "custom metadata record",
|
|
63
|
+
CustomObject: "object",
|
|
64
|
+
CustomPermission: "custom permission",
|
|
65
|
+
Dashboard: "dashboard",
|
|
66
|
+
EmailTemplate: "email template",
|
|
67
|
+
FieldSet: "field set",
|
|
68
|
+
Flow: "flow",
|
|
69
|
+
GlobalValueSet: "global value set",
|
|
70
|
+
Group: "public group",
|
|
71
|
+
Layout: "page layout",
|
|
72
|
+
LightningPage: "Lightning page",
|
|
73
|
+
ListView: "list view",
|
|
74
|
+
PathAssistant: "path",
|
|
75
|
+
PermissionSet: "permission set",
|
|
76
|
+
PermissionSetGroup: "permission set group",
|
|
77
|
+
Profile: "profile",
|
|
78
|
+
Queue: "queue",
|
|
79
|
+
QuickAction: "action",
|
|
80
|
+
RecordType: "record type",
|
|
81
|
+
Report: "report",
|
|
82
|
+
ReportType: "report type",
|
|
83
|
+
// Role and NamedCredential are in CI_RULES because a live QA run delivered them and the
|
|
84
|
+
// writeback dropped them (2026-08-14, ses_09b70d19); a table that silently degrades is how that
|
|
85
|
+
// happened, so both carry explicit entries even though the fallback would spell them right.
|
|
86
|
+
NamedCredential: "named credential",
|
|
87
|
+
Role: "role",
|
|
88
|
+
StandardField: "standard field",
|
|
89
|
+
Tab: "tab",
|
|
90
|
+
ValidationRule: "validation rule",
|
|
91
|
+
WebLink: "button",
|
|
92
|
+
WorkflowEmailAlert: "email alert",
|
|
93
|
+
WorkflowFieldUpdate: "field update"
|
|
94
|
+
};
|
|
95
|
+
/** "LightningPage" -> "lightning page". Only reached for types absent from TYPE_NOUN. */
|
|
96
|
+
function humanizeType(type) {
|
|
97
|
+
return type.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
|
|
98
|
+
}
|
|
99
|
+
function str(value) {
|
|
100
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : undefined;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Everything the org has shown us about this component, folded across deliveries in wave order.
|
|
104
|
+
*
|
|
105
|
+
* PER-KEY LAST OBSERVATION WINS; A PARTIAL LATER BODY MUST NOT ERASE UNRELATED OBSERVED FACTS.
|
|
106
|
+
* A wave's observed body is only as wide as the facets that wave touched — a label-only re-deploy
|
|
107
|
+
* carries `label`, not the value set. Taking the last body wholesale dropped facts the org had
|
|
108
|
+
* already returned: a picklist field renamed in a later wave stopped being describable as a
|
|
109
|
+
* picklist at all, and an object whose second wave was a `{sharingModel}` facetUpdate lost its
|
|
110
|
+
* label from the identity line. (Confirmed through the real derivation on both shapes.)
|
|
111
|
+
*
|
|
112
|
+
* The fold is SHALLOW on purpose. A later wave that re-read `valueSet` re-read the whole value
|
|
113
|
+
* set, so its version replaces the earlier one entire; deep-merging two value sets would splice
|
|
114
|
+
* together a list the org never held at any single moment — a fabricated observation, the one
|
|
115
|
+
* thing this module may not produce.
|
|
116
|
+
*
|
|
117
|
+
* Deliveries without a body (an unconfirmed read, a refused delete) contribute nothing rather
|
|
118
|
+
* than erasing what earlier waves established.
|
|
119
|
+
*/
|
|
120
|
+
function finalConfiguration(row) {
|
|
121
|
+
let folded = {};
|
|
122
|
+
for (const delivery of row.deliveries ?? []) {
|
|
123
|
+
const configuration = delivery.deliveredConfiguration;
|
|
124
|
+
if (!configuration || Object.keys(configuration).length === 0)
|
|
125
|
+
continue;
|
|
126
|
+
folded = { ...folded, ...configuration };
|
|
127
|
+
}
|
|
128
|
+
return folded;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* A boolean facet as the org stated it, or undefined when the org did not state it.
|
|
132
|
+
*
|
|
133
|
+
* TRI-STATE, not a coercion. The XML parser hands booleans back as the STRINGS "true"/"false", so
|
|
134
|
+
* both forms are real observations — a `typeof === "boolean"` gate rejected the wire shape and a
|
|
135
|
+
* validation rule's Active/Inactive never printed at all. But null, an empty element, and an
|
|
136
|
+
* unrecognized casing are all "not observed", and collapsing them to false asserts a fact no read
|
|
137
|
+
* produced; for a validation rule that assertion is load-bearing in the wrong direction —
|
|
138
|
+
* "Inactive" tells a human the rule blocks nothing.
|
|
139
|
+
*/
|
|
140
|
+
function observedBoolean(value) {
|
|
141
|
+
if (value === true || value === "true")
|
|
142
|
+
return true;
|
|
143
|
+
if (value === false || value === "false")
|
|
144
|
+
return false;
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The noun phrase, refined by observed metadata where the metadata makes it more specific.
|
|
149
|
+
* A CustomField whose observed `type` is Picklist is a "picklist field", not a "field".
|
|
150
|
+
*/
|
|
151
|
+
function nounFor(row, config) {
|
|
152
|
+
const base = TYPE_NOUN[row.type] ?? humanizeType(row.type);
|
|
153
|
+
if (row.type !== "CustomField" && row.type !== "StandardField")
|
|
154
|
+
return base;
|
|
155
|
+
const fieldType = str(config.type);
|
|
156
|
+
return fieldType ? `${fieldType.toLowerCase()} ${base}` : base;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Inline values, in document order. A picklist field nests them at `valueSet.valueSetDefinition
|
|
160
|
+
* .value`; a GlobalValueSet carries its own under top-level `customValue` — routing the global set
|
|
161
|
+
* through the field path read the wrong key and the projection was silently dead code. A value's
|
|
162
|
+
* `label` is what a user sees and its `fullName` the stored API name, so label wins; an entry the
|
|
163
|
+
* parser collapsed to a bare string (`<value>Alpha</value>`) is the value itself, not a miss.
|
|
164
|
+
*/
|
|
165
|
+
function observedValues(config, key) {
|
|
166
|
+
const raw = key === "customValue"
|
|
167
|
+
? config.customValue
|
|
168
|
+
: config.valueSet?.valueSetDefinition?.value;
|
|
169
|
+
const entries = Array.isArray(raw) ? raw : raw ? [raw] : [];
|
|
170
|
+
return entries
|
|
171
|
+
.map((entry) => entry !== null && typeof entry === "object"
|
|
172
|
+
? (str(entry.label) ?? str(entry.fullName))
|
|
173
|
+
: str(entry))
|
|
174
|
+
.filter((value) => Boolean(value));
|
|
175
|
+
}
|
|
176
|
+
function projectValues(values) {
|
|
177
|
+
if (values.length === 0)
|
|
178
|
+
return undefined;
|
|
179
|
+
if (values.length <= MAX_PROJECTED_VALUES)
|
|
180
|
+
return `Values: ${values.join(", ")}.`;
|
|
181
|
+
const shown = values.slice(0, MAX_PROJECTED_VALUES).join(", ");
|
|
182
|
+
return `Values: ${shown} (+${values.length - MAX_PROJECTED_VALUES} more).`;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The salient configuration sentences for a row, from observed metadata only.
|
|
186
|
+
*
|
|
187
|
+
* A type with nothing concrete to project contributes nothing — an empty array is the correct and
|
|
188
|
+
* common answer, and padding it with generic prose is exactly the failure this module exists to
|
|
189
|
+
* avoid.
|
|
190
|
+
*/
|
|
191
|
+
function projectConfiguration(row, config) {
|
|
192
|
+
const parts = [];
|
|
193
|
+
switch (row.type) {
|
|
194
|
+
case "CustomField":
|
|
195
|
+
case "StandardField": {
|
|
196
|
+
const values = projectValues(observedValues(config, "valueSet"));
|
|
197
|
+
if (values)
|
|
198
|
+
parts.push(values);
|
|
199
|
+
const formula = str(config.formula);
|
|
200
|
+
if (formula)
|
|
201
|
+
parts.push(`Formula: ${truncateValue(formula)}`);
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case "GlobalValueSet": {
|
|
205
|
+
const values = projectValues(observedValues(config, "customValue"));
|
|
206
|
+
if (values)
|
|
207
|
+
parts.push(values);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
case "ValidationRule": {
|
|
211
|
+
const active = observedBoolean(config.active);
|
|
212
|
+
if (active !== undefined)
|
|
213
|
+
parts.push(active ? "Active." : "Inactive.");
|
|
214
|
+
const message = str(config.errorMessage);
|
|
215
|
+
if (message)
|
|
216
|
+
parts.push(`Error message: "${truncateValue(message)}"`);
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
case "Flow": {
|
|
220
|
+
const start = config.start;
|
|
221
|
+
const triggerObject = str(start?.object);
|
|
222
|
+
const triggerType = str(start?.triggerType);
|
|
223
|
+
// `start.object` alone never settles how the flow triggers — it is present on scheduled flows
|
|
224
|
+
// too. Only an observed record-save/delete triggerType earns "Record-triggered"; a recognised
|
|
225
|
+
// Scheduled type gets its own true sentence; anything else names the object without claiming
|
|
226
|
+
// how it triggers, rather than guessing.
|
|
227
|
+
if (triggerObject && triggerType && RECORD_TRIGGER_TYPES.has(triggerType)) {
|
|
228
|
+
parts.push(`Record-triggered on ${triggerObject}.`);
|
|
229
|
+
}
|
|
230
|
+
else if (triggerObject && triggerType === "Scheduled") {
|
|
231
|
+
parts.push(`Scheduled, running against ${triggerObject}.`);
|
|
232
|
+
}
|
|
233
|
+
else if (triggerObject) {
|
|
234
|
+
parts.push(`Starts from ${triggerObject}.`);
|
|
235
|
+
}
|
|
236
|
+
const processType = str(config.processType);
|
|
237
|
+
if (processType)
|
|
238
|
+
parts.push(`Process type: ${processType}.`);
|
|
239
|
+
const status = str(config.status);
|
|
240
|
+
if (status)
|
|
241
|
+
parts.push(`Status: ${status}.`);
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
case "Role": {
|
|
245
|
+
// The one fact that places a role: where it reports. A top role has no parent and says
|
|
246
|
+
// nothing rather than inventing "top of hierarchy" the org never stored.
|
|
247
|
+
const parent = str(config.parentRole);
|
|
248
|
+
if (parent)
|
|
249
|
+
parts.push(`Parent role: ${parent}.`);
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
case "NamedCredential": {
|
|
253
|
+
// Legacy shape puts the URL on `endpoint`; the SecuredEndpoint shape moves the same URL into
|
|
254
|
+
// a `Url` named-credential parameter. Both are things the org returned; neither is inferred.
|
|
255
|
+
const url = str(config.endpoint) ?? namedCredentialUrl(config);
|
|
256
|
+
if (url)
|
|
257
|
+
parts.push(`URL: ${truncateValue(url)}.`);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
default:
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
return parts;
|
|
264
|
+
}
|
|
265
|
+
/** The SecuredEndpoint half of NamedCredential's two URL shapes — see the case above. */
|
|
266
|
+
function namedCredentialUrl(config) {
|
|
267
|
+
const raw = config.namedCredentialParameters;
|
|
268
|
+
const entries = Array.isArray(raw) ? raw : raw ? [raw] : [];
|
|
269
|
+
for (const entry of entries) {
|
|
270
|
+
if (entry === null || typeof entry !== "object")
|
|
271
|
+
continue;
|
|
272
|
+
const parameter = entry;
|
|
273
|
+
const name = str(parameter.parameterName);
|
|
274
|
+
const parameterType = str(parameter.parameterType);
|
|
275
|
+
if (name?.toLowerCase() === "url" || parameterType?.toLowerCase() === "url") {
|
|
276
|
+
const value = str(parameter.parameterValue);
|
|
277
|
+
if (value)
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* The identity sentence: label (or API name), what it is, and where it lives.
|
|
285
|
+
*
|
|
286
|
+
* `label` is what most types carry; FlexiPage / PathAssistant / WebLink use `masterLabel`; a Role
|
|
287
|
+
* carries its human name in `name`. When the org returned none of them the API name IS the name —
|
|
288
|
+
* prettifying it here would be inventing a label the org never stored.
|
|
289
|
+
*/
|
|
290
|
+
function identity(row, config) {
|
|
291
|
+
const name = str(config.label) ?? str(config.masterLabel) ?? str(config.name) ?? row.apiName;
|
|
292
|
+
const noun = nounFor(row, config);
|
|
293
|
+
const where = row.objectApiName ? ` on ${row.objectApiName}` : "";
|
|
294
|
+
return `${name} — ${noun}${where}.`;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* One sentence per observed difference on the FINAL delivery only — mirroring `finalConfiguration`,
|
|
298
|
+
* which already treats the last delivery as the one that finished the component. A component that
|
|
299
|
+
* differed in an earlier wave and matched in a later, corrective wave has been fixed: carrying an
|
|
300
|
+
* earlier wave's difference forward would print it as an unqualified present-tense claim about
|
|
301
|
+
* current org state, which is exactly the false-inference failure this module exists to avoid. Capped
|
|
302
|
+
* at `MAX_DEVIATION_SENTENCES` — beyond that, one final sentence names how many more were dropped
|
|
303
|
+
* rather than growing the row without bound.
|
|
304
|
+
*/
|
|
305
|
+
function deviations(row) {
|
|
306
|
+
const finalDelivery = (row.deliveries ?? []).at(-1);
|
|
307
|
+
if (!finalDelivery || finalDelivery.readBack !== "differed")
|
|
308
|
+
return [];
|
|
309
|
+
const all = (finalDelivery.differences ?? []).map((difference) => `DEVIATION: ${difference.path} — the org stored ${truncateValue(difference.observed)}; ` +
|
|
310
|
+
`the approved configuration specified ${truncateValue(difference.intended)}.` +
|
|
311
|
+
(ASYNC_ORG_PATH_RE.test(difference.path) ? ` ${ASYNC_DIFFERENCE_HINT}` : ""));
|
|
312
|
+
if (all.length <= MAX_DEVIATION_SENTENCES)
|
|
313
|
+
return all;
|
|
314
|
+
const shown = all.slice(0, MAX_DEVIATION_SENTENCES);
|
|
315
|
+
shown.push(`(+${all.length - MAX_DEVIATION_SENTENCES} more deviations not shown.)`);
|
|
316
|
+
return shown;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* What happened to this component, for rows the run did not deploy.
|
|
320
|
+
*
|
|
321
|
+
* A CI list is what a requirement is BUILT FROM, not a log of one run's deltas, so these rows
|
|
322
|
+
* belong on the manifest and have to say why they carry no delivery.
|
|
323
|
+
*/
|
|
324
|
+
function originSentence(row) {
|
|
325
|
+
if (row.origin === "pre-existing") {
|
|
326
|
+
return ["Already present in the org and owned by this requirement; not deployed by this run."];
|
|
327
|
+
}
|
|
328
|
+
if (row.origin === "routed") {
|
|
329
|
+
return ["Owned by this requirement but not deployable by the direct tier."];
|
|
330
|
+
}
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Backstop for `MAX_DESCRIPTION_CHARS`. The per-value and per-sentence caps above bound the known
|
|
335
|
+
* unbounded inputs, but this is the guarantee that actually matters: `describeConfigItem` never
|
|
336
|
+
* returns a string longer than the field's cap, for any input.
|
|
337
|
+
*
|
|
338
|
+
* Bounds the RETURNED fragment, not the plain body: `richTextFragment` grows some characters when it
|
|
339
|
+
* transforms them — `&` -> `&`, a newline -> `<br/>` — so a plain body sized to the cap can come
|
|
340
|
+
* out over it. Truncates the PLAIN body and re-escapes rather than cutting the escaped string
|
|
341
|
+
* directly — slicing mid-transform (e.g. inside `&`) would corrupt the field with a dangling
|
|
342
|
+
* `&am`.
|
|
343
|
+
*/
|
|
344
|
+
function capFragment(body) {
|
|
345
|
+
let text = body;
|
|
346
|
+
let fragment = richTextFragment(text);
|
|
347
|
+
while (fragment.length > MAX_DESCRIPTION_CHARS && text.length > 0) {
|
|
348
|
+
const excess = fragment.length - MAX_DESCRIPTION_CHARS;
|
|
349
|
+
// `excess` is measured in ESCAPED characters; the slice removes PLAIN ones. Dividing by the
|
|
350
|
+
// measured inflation ratio keeps the two units aligned — subtracting escaped-excess from the
|
|
351
|
+
// plain body directly over-cut by the ratio (5x on `&`-dense text) and COMPOUNDED per pass,
|
|
352
|
+
// reducing a 35k body with 25% escapable characters to a bare "…" while the loop reported
|
|
353
|
+
// success. Ceil guarantees at least one character goes per pass, so the loop still terminates;
|
|
354
|
+
// richTextFragment never shrinks a character, so the ratio is always >= 1.
|
|
355
|
+
const ratio = fragment.length / text.length;
|
|
356
|
+
const cut = Math.max(1, Math.ceil(excess / ratio));
|
|
357
|
+
text = text.slice(0, Math.max(0, text.length - cut - 1)) + "…";
|
|
358
|
+
fragment = richTextFragment(text);
|
|
359
|
+
}
|
|
360
|
+
return fragment;
|
|
361
|
+
}
|
|
362
|
+
export function describeConfigItem(row) {
|
|
363
|
+
const config = finalConfiguration(row);
|
|
364
|
+
// Asserted from the OBSERVED deliveries, never from intent: a delete the org refused is
|
|
365
|
+
// destructive in the request and a no-op in fact, and this sentence is a claim about the org.
|
|
366
|
+
const deleted = row.destructive && (row.deliveries ?? []).some((d) => d.action === "deleted");
|
|
367
|
+
const parts = [
|
|
368
|
+
identity(row, config),
|
|
369
|
+
...projectConfiguration(row, config),
|
|
370
|
+
...originSentence(row),
|
|
371
|
+
...(deleted ? ["Deleted from the org."] : []),
|
|
372
|
+
...deviations(row),
|
|
373
|
+
...(row.note ? [row.note] : [])
|
|
374
|
+
];
|
|
375
|
+
// Returns the FINISHED rich-text fragment, not raw prose. Composing it here rather than at the
|
|
376
|
+
// call site is what keeps caller-supplied names — all apt to carry a `<` — from reaching
|
|
377
|
+
// Description__c unescaped, and `capFragment` is what keeps it under Description__c's cap.
|
|
378
|
+
return capFragment(parts.join(" "));
|
|
379
|
+
}
|