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.
Files changed (115) hide show
  1. package/README.md +369 -0
  2. package/agents/plan-requirement.md +32 -0
  3. package/dist/campfire/configItem.js +201 -0
  4. package/dist/campfire/configItemDescription.js +379 -0
  5. package/dist/campfire/deriveFromBundle.js +1013 -0
  6. package/dist/cli.js +465 -0
  7. package/dist/core/betaFeedback.js +493 -0
  8. package/dist/core/chapterInstructions.js +473 -0
  9. package/dist/core/claudeAssets.js +61 -0
  10. package/dist/core/gate.js +201 -0
  11. package/dist/core/ids.js +9 -0
  12. package/dist/core/install.js +60 -0
  13. package/dist/core/operatorQa.js +462 -0
  14. package/dist/core/orgCallMeter.js +178 -0
  15. package/dist/core/parcels.js +1235 -0
  16. package/dist/core/paths.js +105 -0
  17. package/dist/core/phases.js +225 -0
  18. package/dist/core/planFile.js +251 -0
  19. package/dist/core/planFragments.js +119 -0
  20. package/dist/core/readsLog.js +398 -0
  21. package/dist/core/registration.js +24 -0
  22. package/dist/core/result.js +38 -0
  23. package/dist/core/resume.js +233 -0
  24. package/dist/core/revisionChain.js +351 -0
  25. package/dist/core/richText.js +61 -0
  26. package/dist/core/semanticHash.js +89 -0
  27. package/dist/core/sessionOrg.js +60 -0
  28. package/dist/core/sfQuery.js +19 -0
  29. package/dist/core/spawn.js +182 -0
  30. package/dist/core/supervisorContract.js +230 -0
  31. package/dist/core/telemetry.js +1297 -0
  32. package/dist/core/telemetryContext.js +154 -0
  33. package/dist/core/telemetrySpool.js +244 -0
  34. package/dist/core/transcriptUpload.js +516 -0
  35. package/dist/core/update.js +363 -0
  36. package/dist/core/viewer.js +117 -0
  37. package/dist/core/workspace.js +92 -0
  38. package/dist/core/writeGrants.js +93 -0
  39. package/dist/core/writebackManifest.js +92 -0
  40. package/dist/direct/applyLoop.js +2516 -0
  41. package/dist/direct/batchReport.js +510 -0
  42. package/dist/direct/comparatorNoise.js +75 -0
  43. package/dist/direct/containers.js +85 -0
  44. package/dist/direct/contamination.js +221 -0
  45. package/dist/direct/crud.js +56 -0
  46. package/dist/direct/destructive.js +168 -0
  47. package/dist/direct/knownTokens.js +94 -0
  48. package/dist/direct/lint.js +382 -0
  49. package/dist/direct/metadataSchema.generated.json +1 -0
  50. package/dist/direct/metadataSchema.js +179 -0
  51. package/dist/direct/operatorPage.js +349 -0
  52. package/dist/direct/orgRepo.js +1531 -0
  53. package/dist/direct/orgRepoSpawnCount.js +21 -0
  54. package/dist/direct/plannedScope.js +119 -0
  55. package/dist/direct/preImageTargets.js +63 -0
  56. package/dist/direct/refVerify.js +191 -0
  57. package/dist/direct/registry.js +129 -0
  58. package/dist/direct/removals.js +262 -0
  59. package/dist/direct/rollback.js +1513 -0
  60. package/dist/direct/rollbackRun.js +1360 -0
  61. package/dist/direct/sessionProjection.js +126 -0
  62. package/dist/direct/snapshotComparability.js +165 -0
  63. package/dist/direct/sourceTree.js +113 -0
  64. package/dist/direct/unstatedChanges.js +106 -0
  65. package/dist/direct/waveAssembly.js +556 -0
  66. package/dist/direct/waveCounts.js +19 -0
  67. package/dist/direct/waveDerivation.js +152 -0
  68. package/dist/direct/waveGrade.js +185 -0
  69. package/dist/index.js +90 -0
  70. package/dist/orgread/customFieldSnapshot.js +48 -0
  71. package/dist/orgread/fieldMetadata.js +178 -0
  72. package/dist/orgread/genericSnapshot.js +61 -0
  73. package/dist/orgread/layoutSnapshot.js +151 -0
  74. package/dist/orgread/salesforceOrgReader.js +184 -0
  75. package/dist/orgread/snapshotTypes.js +9 -0
  76. package/dist/projection/bridge.js +26 -0
  77. package/dist/projection/diff.js +141 -0
  78. package/dist/projection/mapmd.js +213 -0
  79. package/dist/projection/nameAmbiguity.js +106 -0
  80. package/dist/projection/precedent.js +136 -0
  81. package/dist/projection/project.js +214 -0
  82. package/dist/projection/scope.js +110 -0
  83. package/dist/projection/types.js +5 -0
  84. package/dist/tools/analyze.js +322 -0
  85. package/dist/tools/auth.js +915 -0
  86. package/dist/tools/campfire.js +1742 -0
  87. package/dist/tools/closeBatch.js +154 -0
  88. package/dist/tools/closeText.js +35 -0
  89. package/dist/tools/direct.js +1613 -0
  90. package/dist/tools/feedback.js +318 -0
  91. package/dist/tools/operator.js +287 -0
  92. package/dist/tools/org.js +739 -0
  93. package/dist/tools/phase.js +249 -0
  94. package/dist/tools/projection.js +172 -0
  95. package/dist/tools/session.js +313 -0
  96. package/dist/tools/update.js +82 -0
  97. package/dist/tools/waves.js +793 -0
  98. package/package.json +48 -0
  99. package/reference/campfire-configuration-items-guide.md +710 -0
  100. package/reference/deployability-cribs.md +119 -0
  101. package/reference/operator-voice.md +141 -0
  102. package/reference/org-read-cribs.md +34 -0
  103. package/reference/scoped-dml-interim.md +68 -0
  104. package/reference/supervisor-playbook.md +313 -0
  105. package/reference/templates.md +233 -0
  106. package/skills/in2-delivery/SKILL.md +322 -0
  107. package/skills/in2-delivery/references/campfire-configuration-items-guide.md +710 -0
  108. package/skills/in2-delivery/references/deployability-cribs.md +119 -0
  109. package/skills/in2-delivery/references/operator-voice.md +141 -0
  110. package/skills/in2-delivery/references/org-read-cribs.md +34 -0
  111. package/skills/in2-delivery/references/scoped-dml-interim.md +68 -0
  112. package/skills/in2-delivery/references/templates.md +233 -0
  113. package/skills/plan-requirement/SKILL.md +188 -0
  114. package/viewer/index.html +899 -0
  115. package/viewer/server.mjs +433 -0
@@ -0,0 +1,382 @@
1
+ import { XMLParser, XMLValidator } from "fast-xml-parser";
2
+ import { checkAgainstSchema, schemaVersion } from "./metadataSchema.js";
3
+ import { ORG_ENFORCED_ELEMENTS } from "./knownTokens.js";
4
+ const PARSER = new XMLParser({ ignoreAttributes: true, parseTagValue: false, trimValues: true });
5
+ export function parseMetadataXml(xml) {
6
+ if (XMLValidator.validate(xml) !== true)
7
+ return null;
8
+ const doc = PARSER.parse(xml);
9
+ const roots = Object.keys(doc).filter((k) => k !== "?xml");
10
+ if (roots.length !== 1)
11
+ return null;
12
+ const rootKey = roots[0];
13
+ if (!rootKey)
14
+ return null;
15
+ const body = doc[rootKey];
16
+ if (!body || typeof body !== "object")
17
+ return null;
18
+ return { root: rootKey, body: body };
19
+ }
20
+ const CUSTOM_TOKEN_RE = /\b[A-Za-z][A-Za-z0-9_]*__(?:c|mdt|r)\b/g;
21
+ // Elements that hold NESTED metadata members, each of which needs its own <fullName>. Deliberately
22
+ // a list rather than a schema lookup: these are container relationships, not value objects, and the
23
+ // artifact does not mark the difference (`valueSet` and `nameField` are nested objects too, and
24
+ // correctly have no fullName).
25
+ const CHILD_CONTAINERS = new Set([
26
+ "compactLayouts",
27
+ "listViews",
28
+ "validationRules",
29
+ "webLinks",
30
+ "recordTypes",
31
+ "fields",
32
+ "fieldSets",
33
+ "businessProcesses",
34
+ "sharingReasons"
35
+ // NOT approvalStep: an approval step is identified by <name>, not <fullName>, and CF75 deployed
36
+ // one successfully in exactly that shape. Including it made this rule block a valid file — caught
37
+ // by the existing schema-tier test, which is the second time today a container list has been the
38
+ // wrong instinct. Add an entry only when a real deploy has rejected the nameless form.
39
+ ]);
40
+ // Types that live UNDER an object in source format (objects/<Obj>/<plural>/<Name>.<ext>-meta.xml).
41
+ // Their file path names the parent, not the component, so the body must carry its own <fullName>.
42
+ // Verified against CF76's authored tree: every CustomField and CompactLayout file carries one;
43
+ // CustomObject, CustomTab, Flow, Layout, Group, FlexiPage, ApprovalProcess and CustomApplication —
44
+ // all top-level — carry none.
45
+ const OBJECT_CHILD_TYPES = new Set([
46
+ "CustomField",
47
+ "CompactLayout",
48
+ "ListView",
49
+ "ValidationRule",
50
+ "WebLink",
51
+ "RecordType",
52
+ "FieldSet",
53
+ "BusinessProcess",
54
+ "SharingReason",
55
+ "Index"
56
+ ]);
57
+ // A metadata element that holds either one child or many reads back as an object or an array of
58
+ // them; the rules below want the same shape either way.
59
+ const asList = (node) => (Array.isArray(node) ? node : [node]).filter((c) => Boolean(c) && typeof c === "object" && !Array.isArray(c));
60
+ // Types whose <description> the org caps at 255 characters.
61
+ //
62
+ // TYPE-SCOPED, deliberately, never a blanket length rule: the cap differs per type — a CustomField
63
+ // or CustomObject description takes 1000 — so a global check would refuse files the org accepts
64
+ // daily. That is the "guessing a type list" mistake this file has already made twice. An entry
65
+ // here means a real deploy REJECTED the over-long value, not that the reasoning looks the same.
66
+ const DESCRIPTION_MAX = 255;
67
+ const DESCRIPTION_CAPPED_TYPES = new Set(["ValidationRule", "PermissionSet"]);
68
+ // The same components authored INSIDE a CustomObject file rather than as their own: container
69
+ // element -> the capped child type it holds.
70
+ const DESCRIPTION_CAPPED_CONTAINERS = { validationRules: "ValidationRule" };
71
+ export function runLint(edits, knownCustomTokens, // from the pre-projection; null = referential tier skipped
72
+ acks, provenValues, // org-proven values for elements no schema can validate
73
+ plannedTokens // tokens the approved batch will CREATE (plan.json), for the referential split
74
+ ) {
75
+ const findings = [];
76
+ const acked = new Set(acks);
77
+ const editedTokens = new Set();
78
+ for (const e of edits) {
79
+ editedTokens.add(e.fullName);
80
+ for (const part of e.fullName.split("."))
81
+ editedTokens.add(part);
82
+ }
83
+ for (const edit of edits) {
84
+ const component = `${edit.typeName}:${edit.fullName}`;
85
+ const push = (rule, severity, message, ackable = true) => findings.push({ rule, severity, component, message, ackable });
86
+ const parsed = parseMetadataXml(edit.xml);
87
+ if (!parsed) {
88
+ push("XML_MALFORMED", "error", "XML does not parse or has no single metadata root element.", false);
89
+ continue;
90
+ }
91
+ if (parsed.root !== edit.typeName) {
92
+ push("ROOT_TYPE_MISMATCH", "error", `Root element <${parsed.root}> does not match declared typeName ${edit.typeName}.`, false);
93
+ continue;
94
+ }
95
+ const md = parsed.body;
96
+ // LEMMA (D2, live-caught batch 1 + checkOnly probes 2026-07-22): a Picklist's
97
+ // field-level <defaultValue> is FORMULA-parsed; a quoted literal validates green
98
+ // and is then silently ignored — defaults live on the value's <default>true</default>.
99
+ if ((md.type === "Picklist" || md.type === "MultiselectPicklist") && md.defaultValue !== undefined) {
100
+ push("LEMMA_D2_FIELD_LEVEL_DEFAULT", "error", `Field-level <defaultValue> on a ${md.type} is formula-parsed and silently ignored (lemma D2). Set <default>true</default> on the value inside valueSetDefinition instead.`);
101
+ }
102
+ // LEMMA (D3, live-caught batch 1; checkOnly rejects it with a precise error): the
103
+ // platform forbids required long text areas.
104
+ if ((md.type === "LongTextArea" || md.type === "Html") && String(md.required) === "true") {
105
+ push("LEMMA_D3_REQUIRED_LTA", "error", `<required>true</required> on a ${md.type} is rejected by the platform (lemma D3): "Can not specify 'required' for a CustomField of type LongTextArea".`);
106
+ }
107
+ // LEMMA (batch-2 step 6, live-caught): a top-level CustomObject deploy must carry
108
+ // the full definition; a partial file (e.g. only compactLayoutAssignment) fails
109
+ // "Must specify a non-empty label". What is illegal is the PARTIAL file, not the deploy —
110
+ // the fix is to complete it from the org's pre-image, which is also the safer brownfield
111
+ // path (CustomObject MERGES, so children are not collateral). The CRUD route this message
112
+ // used to recommend outright is a read-modify-write that REPLACES top-level properties, and
113
+ // it has now leaked data twice; it belongs to facets a source deploy cannot carry.
114
+ if (edit.typeName === "CustomObject" && md.label === undefined) {
115
+ push("LEMMA_PARTIAL_CUSTOMOBJECT", "error", "CustomObject deploy without <label> — top-level object deploys must carry the full definition (live-caught: bare compactLayoutAssignment file failed). Complete the file from the org's pre-image (direct_apply retrieves it to pre/source/): CustomObject merges, so a full definition changes the facet without touching children. direct_apply objectFacetUpdates is the fallback for facets a source deploy cannot carry.");
116
+ }
117
+ // LEMMA (CF63 w2, live-caught 2026-07-31): deploying a Flow with <status>Obsolete</status> does
118
+ // NOT deactivate the flow. Salesforce VERSIONS flows — the deploy lands a new, inactive version
119
+ // and the previously-active version keeps running. The deploy reports Succeeded, so the only
120
+ // signal is the read-back, and even that is subtle: the run saw "intended Obsolete, still reads
121
+ // Active" and had to go find the real mechanism.
122
+ //
123
+ // Worse, the tool surface TAUGHT this: the deployment-task writer's description asserted a Flow
124
+ // "deploys with the status its XML DECLARES", which is true for a CREATE and false for a
125
+ // deactivation. That sentence is corrected; this rule catches the attempt itself.
126
+ //
127
+ // Deactivation is a FlowDefinition deploy with <activeVersionNumber>0</activeVersionNumber>,
128
+ // which this tier already supports — the same run used it successfully twice, once it knew.
129
+ //
130
+ // A warning rather than an error: deploying a brand-new flow directly to Obsolete is legal, if
131
+ // unusual, and the point is to name the mechanism before a wave is spent discovering it.
132
+ if (edit.typeName === "Flow" && String(md.status) === "Obsolete") {
133
+ push("LEMMA_FLOW_OBSOLETE_IS_NOT_DEACTIVATION", "warning", `<status>Obsolete</status> on a Flow does not deactivate it. Salesforce versions flows: this deploy adds a new INACTIVE version and leaves the currently-active version running, while reporting Succeeded. ` +
134
+ `To deactivate ${edit.fullName}, deploy a FlowDefinition instead — fullName ${edit.fullName}, ` +
135
+ `<activeVersionNumber>0</activeVersionNumber> — and confirm with FlowDefinitionView/Flow IsActive rather than the Flow's own status. ` +
136
+ `Ignore this only if you genuinely mean to CREATE a new flow that is obsolete from birth.`);
137
+ }
138
+ // LEMMA (6.5.1 run, live-caught 2026-07-24): a Create QuickAction burned THREE
139
+ // checkOnly round-trips on missing required facets ("Required field is missing:
140
+ // optionsCreateFeedItem", then "[TargetField]"). Object Create/Update actions need
141
+ // BOTH <targetObject> (the object the action operates on) and <optionsCreateFeedItem>.
142
+ // Catch it locally so the model authors it right the first time instead of iterating
143
+ // against the org. Note: an object-SPECIFIC create action (fullName Object.Action)
144
+ // targeting that same object can still reject; a GLOBAL action (bare fullName) with
145
+ // <targetObject> is the reliable shape.
146
+ if (edit.typeName === "QuickAction" && (md.type === "Create" || md.type === "Update")) {
147
+ const missing = [];
148
+ if (md.targetObject === undefined)
149
+ missing.push("targetObject");
150
+ if (md.optionsCreateFeedItem === undefined)
151
+ missing.push("optionsCreateFeedItem");
152
+ // Third required facet, live-caught in the 6.5.4 run one wave AFTER the first two:
153
+ // "QuickActions of type Create must have a layout".
154
+ if (md.quickActionLayout === undefined)
155
+ missing.push("quickActionLayout");
156
+ if (missing.length > 0) {
157
+ push("LEMMA_QUICKACTION_CREATE_SHAPE", "error", `QuickAction type=${md.type} is missing required facet(s): ${missing.join(", ")} — checkOnly rejects each of these in turn ("Required field is missing" / "QuickActions of type Create must have a layout"). Needs targetObject (the object the action operates on), optionsCreateFeedItem (true|false), and a quickActionLayout naming the fields; if an object-specific action still rejects on [TargetField], author it as a global action (bare fullName) with targetObject.`);
158
+ }
159
+ }
160
+ // LEMMA (6.5.4 run, live-caught): "Element showRunAssignmentRulesButton invalid at this
161
+ // location in type Layout" — a family of Layout flags exists only for specific STANDARD
162
+ // objects (Case/Lead assignment rules, Case solutions/knowledge/submit-and-attach). On a
163
+ // custom object's layout the whole deploy is rejected. fullName is "Object-Layout Name".
164
+ if (edit.typeName === "Layout") {
165
+ const host = edit.fullName.split("-")[0] ?? "";
166
+ if (/__(c|mdt)$/.test(host)) {
167
+ const standardOnly = [
168
+ "showRunAssignmentRulesButton",
169
+ "showSubmitAndAttachButton",
170
+ "showSolutionSection",
171
+ "showKnowledgeComponent"
172
+ ].filter((flag) => md[flag] !== undefined);
173
+ if (standardOnly.length > 0) {
174
+ push("LEMMA_LAYOUT_STANDARD_ONLY_FLAG", "error", `Layout on custom object ${host} carries standard-object-only element(s): ${standardOnly.join(", ")} — the platform rejects the whole layout ("invalid at this location in type Layout"). These are Case/Lead facets; drop them unless the requirement asked for one.`);
175
+ }
176
+ }
177
+ }
178
+ // LEMMA (batch-2 step 7, live-caught): agentic authoring invented design-time
179
+ // component tokens; runtime_sales:* does not exist in this org family.
180
+ if (/runtime_sales:/.test(edit.xml)) {
181
+ push("LEMMA_HALLUCINATED_TOKEN", "warning", "componentName namespace 'runtime_sales:' matches a live-caught hallucination (org truth was console:history / notes:utilityBarNoteList). Ground tokens from a projection of an existing page.");
182
+ }
183
+ // LEMMA (CF74/CF76/CF77/CF78, rejected by the org on FOUR consecutive runs before becoming a
184
+ // rule — the quirk corpus has no automatic feedback from REJECTED_BY_ORG, so this one is
185
+ // hand-carried): flexipage:recentItems deploys ONLY with an `entityNames`
186
+ // componentInstanceProperty. CF78's rejection finally named the missing property
187
+ // ("missing required property [entityNames]"); without it the checkOnly rejects the whole
188
+ // wave. The proven alternative every prior run shipped is omitting the utility entirely.
189
+ if (/flexipage:recentItems/.test(edit.xml) && !/entityNames/.test(edit.xml)) {
190
+ push("LEMMA_RECENTITEMS_ENTITYNAMES", "warning", "flexipage:recentItems without an `entityNames` componentInstanceProperty was REJECTED by the org on four consecutive runs (\"missing required property [entityNames]\"), taking its wave's checkOnly with it. Add <componentInstanceProperties> with name entityNames listing the object API name(s), or omit the utility — every delivered CF-series bar ships History + Notes only.");
191
+ }
192
+ // LEMMA (EV-31…40 run, live-caught 2026-08-02): a <description> over 255 characters.
193
+ //
194
+ // Three of that run's REJECTED_BY_ORG outcomes were this one class — two ValidationRules and a
195
+ // PermissionSet — and direct_lint returned CLEAN before every one, so each bought a ~45s
196
+ // checkOnly round trip and a retry cycle for a fact a string length already knew.
197
+ //
198
+ // The identical bound is enforced locally on Campfire's `Blocked_Reason__c`
199
+ // (MAX_LENGTHS in tools/campfire.ts) for the identical reason: an all-or-nothing write where
200
+ // one over-long string refuses every row. Same check, other side of the tool.
201
+ const cappedDescriptions = [];
202
+ if (DESCRIPTION_CAPPED_TYPES.has(edit.typeName))
203
+ cappedDescriptions.push({ label: edit.typeName, value: md.description });
204
+ for (const [container, childType] of Object.entries(DESCRIPTION_CAPPED_CONTAINERS)) {
205
+ for (const child of asList(md[container])) {
206
+ cappedDescriptions.push({ label: `${childType} ${String(child.fullName ?? "")}`.trim(), value: child.description });
207
+ }
208
+ }
209
+ for (const { label, value } of cappedDescriptions) {
210
+ if (typeof value !== "string" || value.length <= DESCRIPTION_MAX)
211
+ continue;
212
+ push("LEMMA_DESCRIPTION_TOO_LONG", "error", `<description> on ${label} is ${value.length} characters. The org caps it at ${DESCRIPTION_MAX} and rejects the deploy — a deploy is all-or-nothing, so it takes the whole wave with it. Trim it to ${DESCRIPTION_MAX} characters or fewer and let the requirement carry the long form.`);
213
+ }
214
+ // LEMMA (EV-31…40 run, live-caught 2026-08-02): a listView filter on a CHECKBOX field takes the
215
+ // literals `1` and `0`. `true`/`false` — the spelling every other boolean element in metadata
216
+ // uses, and so the one an author reaches for — is REJECTED. It cost four list views in one run.
217
+ //
218
+ // Nothing local knows whether the field IS a checkbox; that needs the org. So the rule keys on
219
+ // the LITERAL instead, which is what makes it statically decidable. The one false positive is a
220
+ // text or picklist field whose value genuinely is the word "true" — that is what the ack is for.
221
+ const filterSets = [];
222
+ if (edit.typeName === "ListView")
223
+ filterSets.push({ view: edit.fullName, filters: md.filters });
224
+ for (const view of asList(md.listViews))
225
+ filterSets.push({ view: String(view.fullName ?? edit.fullName), filters: view.filters });
226
+ for (const { view, filters } of filterSets) {
227
+ const literals = asList(filters).filter((f) => /^(true|false)$/i.test(String(f.value ?? "")));
228
+ if (literals.length === 0)
229
+ continue;
230
+ push("LEMMA_LISTVIEW_CHECKBOX_LITERAL", "error", `List view ${view} filters on ${literals.map((f) => `${String(f.field ?? "?")}=${String(f.value)}`).join(", ")}. A checkbox filter value must be 1 (checked) or 0 (unchecked) — Salesforce rejects true/false and the deploy is all-or-nothing, so one filter rolls back the wave. Replace true with 1 and false with 0. Ack only if the field is genuinely a text or picklist field whose value is the word itself.`);
231
+ }
232
+ // LEMMA (EV-31…40 run, live-caught 2026-08-02): "To mute Delete, first mute Modify All Records".
233
+ //
234
+ // In a MutingPermissionSet every <…>true</…> means MUTE this permission, so the platform's
235
+ // permission dependencies run BACKWARDS from the ones a normal permission set obeys. Modify All
236
+ // Records grants Delete, so muting Delete alone would leave the user still able to delete
237
+ // everything — the org refuses the whole deploy rather than land a contradiction.
238
+ //
239
+ // Only this ONE edge is a rule, because it is the one a deploy has actually rejected. The same
240
+ // lattice implies siblings (muting Read would need Edit, Delete, View All and Modify All muted
241
+ // too), but a guessed list has been the wrong instinct twice in this file already. Extend it
242
+ // when the org says so, not when the reasoning looks symmetric.
243
+ if (edit.typeName === "MutingPermissionSet") {
244
+ for (const op of asList(md.objectPermissions)) {
245
+ if (String(op.allowDelete) !== "true" || String(op.modifyAllRecords) === "true")
246
+ continue;
247
+ push("LEMMA_MUTING_DELETE_NEEDS_MODIFY_ALL", "error", `<objectPermissions> for ${String(op.object ?? "?")} mutes Delete without muting Modify All Records — the org rejects the deploy: "To mute Delete, first mute Modify All Records". In a muting permission set true means MUTE, and Modify All Records grants Delete, so it has to be muted alongside. Add <modifyAllRecords>true</modifyAllRecords> to the same <objectPermissions>.`);
248
+ }
249
+ }
250
+ // LEMMA — an object-child component, in its own file, with no <fullName>.
251
+ //
252
+ // The path names the PARENT object, not the component, so nothing else identifies it. CF76 w4
253
+ // authored a standalone CompactLayout without one; checkOnly rejected the wave and — a deploy
254
+ // being all-or-nothing — took every other component in it down too. Its sibling ListView in the
255
+ // same directory carried one, so the asymmetry was locally visible the whole time.
256
+ //
257
+ // The schema tier cannot say this even now that inherited members resolve: @salesforce/types
258
+ // marks fullName OPTIONAL on all 750 types that declare it, which is correct for a top-level
259
+ // component (the path names it) and wrong for a child. Optionality cannot express "depends on
260
+ // where this file sits".
261
+ if (OBJECT_CHILD_TYPES.has(edit.typeName) && !("fullName" in md)) {
262
+ // ERROR only where a real deploy has actually rejected it. CompactLayout has that evidence
263
+ // (CF76 w4, checkOnly, whole wave rolled back). For the others the reasoning is identical —
264
+ // the path names the parent, not the component — and every field ever deployed from this tool
265
+ // carried one, but nothing has PROVEN the nameless form fails. Guessing a type list has been
266
+ // wrong twice already today, so those warn rather than block.
267
+ push("LEMMA_CHILD_MISSING_FULLNAME", edit.typeName === "CompactLayout" ? "error" : "warning", `${edit.typeName} is an object-child type: its file path names the parent object, not this component, so the body must carry its own <fullName>. Salesforce rejects the deploy, and a deploy is all-or-nothing — this one element takes the whole wave with it.`);
268
+ }
269
+ // LEMMA — a nested metadata member with no <fullName>.
270
+ //
271
+ // Lives here rather than in the schema tier because the schema CANNOT express it:
272
+ // @salesforce/types declares `Metadata = { fullName?: string }`, and that same optional marker
273
+ // flows to every child. Optional is correct for the ROOT, where fullName rides the file path in
274
+ // source format; it is wrong for a child, which nothing in the path identifies. The types have
275
+ // no way to distinguish the two, so asserting it there would be inventing a constraint the
276
+ // artifact does not carry.
277
+ //
278
+ // Live origin: CF75 w2 authored a CompactLayout with no <fullName> while its sibling listView
279
+ // and validationRule happened to carry theirs. Rejected at checkOnly, and because a deploy is
280
+ // all-or-nothing it rolled back the whole wave for that one element.
281
+ for (const [container, children] of Object.entries(md)) {
282
+ if (!CHILD_CONTAINERS.has(container))
283
+ continue;
284
+ const list = Array.isArray(children) ? children : [children];
285
+ const nameless = list.filter((c) => c && typeof c === "object" && !("fullName" in c));
286
+ if (nameless.length > 0) {
287
+ push("LEMMA_CHILD_MISSING_FULLNAME", "error", `${nameless.length} <${container}> child(ren) have no <fullName>. A nested member is identified by its own fullName — only the ROOT component takes its name from the file path. Salesforce rejects the whole file, so one nameless child rolls back every component in the wave.`);
288
+ }
289
+ }
290
+ // SCHEMA tier — the official Metadata API definitions, checked locally. Ordered BEFORE the
291
+ // referential tier because a structurally invalid file cannot have meaningful references.
292
+ // Enum values, missing-required elements, and MISPLACED elements — all three near-certain.
293
+ // An element-NAME check is still deliberately absent (a newer org may know elements this
294
+ // artifact does not), but an element the schema declares ELSEWHERE appearing where it is not
295
+ // declared is a different, safe claim: it is in the wrong place, and we can name the right one. Live origin: `<type>group</type>` on an approval approver cost a 45s checkOnly AND
296
+ // rolled back four correct components with it, because a deploy is all-or-nothing.
297
+ for (const f of checkAgainstSchema(edit.typeName, md)) {
298
+ push(f.kind === "enum" ? "SCHEMA_ENUM_VALUE" : f.kind === "placement" ? "SCHEMA_ELEMENT_MISPLACED" : "SCHEMA_REQUIRED_MISSING", "warning", `${f.message} (${schemaVersion()})`);
299
+ }
300
+ // ORG-ENFORCED tier: elements the schema types as a free string but the org validates against
301
+ // its own contents (CustomTab.motif, FlexiPage.componentName). Nothing static can check these,
302
+ // so the check is precedent: a component ALREADY IN THE ORG using this value proves the org
303
+ // accepts it. A warning, never an error — a value can be legitimately new to this org, and the
304
+ // point is to make the author confirm rather than to refuse.
305
+ if (provenValues) {
306
+ const used = new Map();
307
+ const scan = (node) => {
308
+ if (Array.isArray(node))
309
+ return void node.forEach(scan);
310
+ if (!node || typeof node !== "object")
311
+ return;
312
+ for (const [k, v] of Object.entries(node)) {
313
+ if (ORG_ENFORCED_ELEMENTS.has(k) && typeof v === "string" && v !== "") {
314
+ const set = used.get(k) ?? new Set();
315
+ set.add(v);
316
+ used.set(k, set);
317
+ }
318
+ else
319
+ scan(v);
320
+ }
321
+ };
322
+ scan(md);
323
+ for (const [element, values] of used) {
324
+ const proven = provenValues.get(`${edit.typeName}.${element}`) ?? new Set();
325
+ const unproven = [...values].filter((v) => !proven.has(v));
326
+ if (unproven.length === 0)
327
+ continue;
328
+ const precedent = proven.size > 0
329
+ ? `Values already proven in this org: ${[...proven].sort().slice(0, 8).join(", ")}${proven.size > 8 ? ", …" : ""}.`
330
+ : `No ${edit.typeName} in the projection uses <${element}>, so there is no precedent to compare against — widen the projection to include existing ${edit.typeName} components, or confirm the value another way.`;
331
+ push("UNPROVEN_ORG_TOKEN", "warning", `<${element}> value(s) not proven in this org: ${unproven.sort().join(", ")}. The Metadata API types this element as a free string, so NOTHING can validate it locally — the org decides, and it rejects an unknown value at deploy, taking the whole wave with it. ${precedent}`);
332
+ }
333
+ }
334
+ // Referential tier: every custom-API token referenced must exist in the projected
335
+ // slice or be introduced by this batch of edits.
336
+ //
337
+ // TWO CLASSIFICATIONS, because one name used to carry two opposite situations (QA round 1,
338
+ // item 10). A token the PLAN says this batch will create is a dependency that has not landed
339
+ // yet — real when the creator wave runs later, and exactly what a `dependsOn` edge should have
340
+ // ordered. Everything residual is a genuine unknown: a typo, a wrong relationship name, or a
341
+ // real component outside every projection's scope — and only the org can split those last two,
342
+ // which is the apply loop's verification step, not this function's. Lint stays synchronous and
343
+ // org-free; it CLASSIFIES, the caller verifies.
344
+ if (knownCustomTokens) {
345
+ const unknown = new Set();
346
+ const pending = new Set();
347
+ for (const match of edit.xml.match(CUSTOM_TOKEN_RE) ?? []) {
348
+ const token = match.endsWith("__r") ? `${match.slice(0, -3)}__c` : match;
349
+ if (knownCustomTokens.has(token) || editedTokens.has(token))
350
+ continue;
351
+ if (plannedTokens?.has(token) || plannedTokens?.has(match))
352
+ pending.add(match);
353
+ else
354
+ unknown.add(match);
355
+ }
356
+ if (pending.size > 0) {
357
+ findings.push({
358
+ rule: "REF_PENDING_DEPENDENCY",
359
+ severity: "warning",
360
+ component,
361
+ message: `References this batch PLANS to create but the org does not hold yet: ${[...pending].sort().join(", ")}. ` +
362
+ "Fine in a rehearsal or when the creator ships in this same wave; in a real deploy that runs first, the org rejects the reference. " +
363
+ "Check the creator is in this wave or an earlier one — a later wave means plan.json is missing a dependsOn edge.",
364
+ ackable: true,
365
+ tokens: [...pending].sort()
366
+ });
367
+ }
368
+ if (unknown.size > 0) {
369
+ findings.push({
370
+ rule: "REF_UNKNOWN",
371
+ severity: "warning",
372
+ component,
373
+ message: `References not found in the projected slice, this edit batch, or the batch plan: ${[...unknown].sort().join(", ")} — widen the projection scope or verify the names.`,
374
+ ackable: true,
375
+ tokens: [...unknown].sort()
376
+ });
377
+ }
378
+ }
379
+ }
380
+ const blocked = findings.some((f) => f.severity === "error" && !(f.ackable && acked.has(f.rule)));
381
+ return { findings, blocked };
382
+ }