sf-intelligence 0.2.4 → 0.2.5

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 (3) hide show
  1. package/dist/index.js +927 -182
  2. package/package.json +1 -1
  3. package/server.json +2 -2
package/dist/index.js CHANGED
@@ -357,12 +357,163 @@ var init_src = __esm({
357
357
  }
358
358
  });
359
359
 
360
+ // ../graph/dist/src/relationship-refs.js
361
+ var objectApiNameOf, AMBIGUOUS, buildRelationshipMaps, resolveTraversalTarget, RELATIONSHIP_RESOLVER_SOURCE, mintRelationshipTraversalEdges;
362
+ var init_relationship_refs = __esm({
363
+ "../graph/dist/src/relationship-refs.js"() {
364
+ "use strict";
365
+ objectApiNameOf = (parentId) => {
366
+ if (parentId === null)
367
+ return null;
368
+ const prefix = "CustomObject:";
369
+ return parentId.startsWith(prefix) ? parentId.slice(prefix.length) : null;
370
+ };
371
+ AMBIGUOUS = /* @__PURE__ */ Symbol("ambiguous");
372
+ buildRelationshipMaps = (nodes) => {
373
+ const parentward = /* @__PURE__ */ new Map();
374
+ const childward = /* @__PURE__ */ new Map();
375
+ for (const node of nodes) {
376
+ if (node.type !== "CustomField")
377
+ continue;
378
+ const owningObject = objectApiNameOf(node.parentId);
379
+ if (owningObject === null)
380
+ continue;
381
+ const referenceTo = node.properties["referenceTo"];
382
+ if (typeof referenceTo !== "string" || referenceTo.length === 0)
383
+ continue;
384
+ const fieldApiName = node.apiName;
385
+ const traversal = fieldApiName.endsWith("__c") ? `${fieldApiName.slice(0, -3)}__r` : fieldApiName.replace(/Id$/, "");
386
+ if (traversal.length > 0) {
387
+ const key = `${owningObject.toLowerCase()}|${traversal.toLowerCase()}`;
388
+ const existing = parentward.get(key);
389
+ if (existing === void 0) {
390
+ parentward.set(key, referenceTo);
391
+ } else if (existing !== referenceTo) {
392
+ parentward.set(key, AMBIGUOUS);
393
+ }
394
+ }
395
+ const relationshipName = node.properties["relationshipName"];
396
+ if (typeof relationshipName === "string" && relationshipName.length > 0) {
397
+ for (const key of [relationshipName, `${relationshipName}__r`]) {
398
+ const lower = key.toLowerCase();
399
+ const existing = childward.get(lower);
400
+ if (existing === void 0) {
401
+ childward.set(lower, owningObject);
402
+ } else if (existing !== owningObject) {
403
+ childward.set(lower, AMBIGUOUS);
404
+ }
405
+ }
406
+ }
407
+ }
408
+ const resolve18 = (raw) => {
409
+ const out = /* @__PURE__ */ new Map();
410
+ for (const [key, value] of raw) {
411
+ if (value !== AMBIGUOUS)
412
+ out.set(key, value);
413
+ }
414
+ return out;
415
+ };
416
+ return { parentward: resolve18(parentward), childward: resolve18(childward) };
417
+ };
418
+ resolveTraversalTarget = (path2, owningObject, maps) => {
419
+ const segments = path2.split(".");
420
+ if (segments.length < 2)
421
+ return null;
422
+ let current = owningObject;
423
+ for (const hop of segments.slice(0, -1)) {
424
+ const next = maps.parentward.get(`${current.toLowerCase()}|${hop.toLowerCase()}`);
425
+ if (next === void 0)
426
+ return null;
427
+ current = next;
428
+ }
429
+ const field = segments[segments.length - 1];
430
+ if (field === void 0 || field.length === 0)
431
+ return null;
432
+ return `CustomField:${current}.${field}`;
433
+ };
434
+ RELATIONSHIP_RESOLVER_SOURCE = "relationship-resolver";
435
+ mintRelationshipTraversalEdges = (nodes, edges) => {
436
+ const maps = buildRelationshipMaps(nodes);
437
+ const fieldIds = /* @__PURE__ */ new Set();
438
+ for (const node of nodes) {
439
+ if (node.type === "CustomField")
440
+ fieldIds.add(node.id);
441
+ }
442
+ const seen = /* @__PURE__ */ new Set();
443
+ for (const edge of edges) {
444
+ seen.add(`${edge.fromId}\0${edge.toId}\0${edge.edgeType}`);
445
+ }
446
+ const emit = (fromId, toId, confidence, properties) => {
447
+ if (!fieldIds.has(toId))
448
+ return;
449
+ const key = `${fromId}\0${toId}\0references`;
450
+ if (seen.has(key))
451
+ return;
452
+ seen.add(key);
453
+ edges.push({
454
+ fromId,
455
+ toId,
456
+ edgeType: "references",
457
+ confidence,
458
+ source: RELATIONSHIP_RESOLVER_SOURCE,
459
+ properties
460
+ });
461
+ };
462
+ for (const node of nodes) {
463
+ if (node.type === "CustomField") {
464
+ const refs = node.properties["formulaRelationshipRefs"];
465
+ const owningObject = objectApiNameOf(node.parentId);
466
+ if (Array.isArray(refs) && owningObject !== null) {
467
+ for (const ref of refs) {
468
+ if (typeof ref !== "string")
469
+ continue;
470
+ const toId = resolveTraversalTarget(ref, owningObject, maps);
471
+ if (toId === null)
472
+ continue;
473
+ emit(node.id, toId, "parsed", {
474
+ referenceKind: "formulaRelationshipTraversal",
475
+ traversalPath: ref
476
+ });
477
+ }
478
+ }
479
+ }
480
+ if (node.type === "FlexiPage") {
481
+ const related = node.properties["relatedListFieldRefs"];
482
+ if (!Array.isArray(related))
483
+ continue;
484
+ for (const entry of related) {
485
+ if (typeof entry !== "object" || entry === null)
486
+ continue;
487
+ const { relatedListApiName, fields } = entry;
488
+ if (typeof relatedListApiName !== "string")
489
+ continue;
490
+ const childObject = maps.childward.get(relatedListApiName.toLowerCase());
491
+ if (childObject === void 0 || !Array.isArray(fields))
492
+ continue;
493
+ for (const field of fields) {
494
+ if (typeof field !== "string" || field.length === 0)
495
+ continue;
496
+ if (field.includes("."))
497
+ continue;
498
+ emit(node.id, `CustomField:${childObject}.${field}`, "parsed", {
499
+ referenceKind: "relatedListFieldAlias",
500
+ relatedListApiName
501
+ });
502
+ }
503
+ }
504
+ }
505
+ }
506
+ };
507
+ }
508
+ });
509
+
360
510
  // ../graph/dist/src/import.js
361
511
  var IMPORT_BATCH_SIZE, INSERT_NODE_SQL, INSERT_EDGE_SQL, NODE_COLUMN_COUNT, EDGE_COLUMN_COUNT, buildMultiRowUpsertSql, canonicalJson, relativizeSourcePath, nodeRowParams, edgeRowParams, insertNode, insertEdge, commitBatched, canonicalizeApexCallEdgeTargets, canonicalizeEdgeTargetsByCase, CUSTOM_FIELD_PREFIX, ACTIVITY_OBJECT_LOWER, ACTIVITY_CHILD_OBJECTS_LOWER, splitFieldId, canonicalizeActivityPolymorphicFieldEdgeTargets, ACTIVITY_POLYMORPHIC_SLOTS, POLYMORPHIC_MIRROR_EDGE_TYPES, ACTIVITY_POLYMORPHIC_SOURCE, mintPolymorphicActivityFieldEdges, canonicalizeFieldEdgeTargets, canonicalizeObjectEdgeTargets, canonicalizeLabelEdgeTargets, canonicalizeResourceEdgeTargets, mintFutureDispatchEdges, importExtractionResults;
362
512
  var init_import = __esm({
363
513
  "../graph/dist/src/import.js"() {
364
514
  "use strict";
365
515
  init_src();
516
+ init_relationship_refs();
366
517
  IMPORT_BATCH_SIZE = 500;
367
518
  INSERT_NODE_SQL = `INSERT OR REPLACE INTO nodes (
368
519
  id, type, api_name, label, parent_id, source_path,
@@ -738,6 +889,7 @@ var init_import = __esm({
738
889
  canonicalizeLabelEdgeTargets(allNodes, allEdges);
739
890
  canonicalizeResourceEdgeTargets(allNodes, allEdges);
740
891
  mintFutureDispatchEdges(allNodes, allEdges);
892
+ mintRelationshipTraversalEdges(allNodes, allEdges);
741
893
  const nodeOutcome = await commitBatched(connection, allNodes, insertNode);
742
894
  if (!nodeOutcome.ok) {
743
895
  return err({
@@ -772,12 +924,19 @@ var init_import = __esm({
772
924
  });
773
925
 
774
926
  // ../graph/dist/src/apply-change-set.js
775
- var componentTypeFromNodeId, INCREMENTAL_DELTA_CAP, changeSetSize, DELETE_NODE_SQL, DELETE_EDGE_SQL, PK_SEP, edgePk, rowKey, readNodeRowKeys, readEdgeRowKeys, computeChangeSet, upsertRowsChunked, scalarCount, applyChangeSet, pruneStaleNodes;
927
+ var SYNTHETIC_MINT_SOURCES, componentTypeFromNodeId, INCREMENTAL_DELTA_CAP, changeSetSize, DELETE_NODE_SQL, DELETE_EDGE_SQL, PK_SEP, edgePk, rowKey, readNodeRowKeys, readEdgeRowKeys, computeChangeSet, upsertRowsChunked, scalarCount, applyChangeSet, pruneStaleNodes;
776
928
  var init_apply_change_set = __esm({
777
929
  "../graph/dist/src/apply-change-set.js"() {
778
930
  "use strict";
779
931
  init_src();
780
932
  init_import();
933
+ init_relationship_refs();
934
+ SYNTHETIC_MINT_SOURCES = /* @__PURE__ */ new Set([
935
+ RELATIONSHIP_RESOLVER_SOURCE,
936
+ // mintFutureDispatchEdges (import.ts) — same hazard, mirrored earlier without
937
+ // the exclusion; correcting both together keeps the two passes consistent.
938
+ "graph-future-dispatch"
939
+ ]);
781
940
  componentTypeFromNodeId = (id) => {
782
941
  const sep6 = id.indexOf(":");
783
942
  if (sep6 < 0)
@@ -854,6 +1013,7 @@ var init_apply_change_set = __esm({
854
1013
  canonicalizeLabelEdgeTargets([...desiredNodes.values()], desiredEdgeList);
855
1014
  canonicalizeResourceEdgeTargets([...desiredNodes.values()], desiredEdgeList);
856
1015
  mintFutureDispatchEdges([...desiredNodes.values()], desiredEdgeList);
1016
+ mintRelationshipTraversalEdges([...desiredNodes.values()], desiredEdgeList);
857
1017
  const desiredEdges = /* @__PURE__ */ new Map();
858
1018
  for (const edge of desiredEdgeList) {
859
1019
  const pk = edgePk(edge.fromId, edge.toId, edge.edgeType, edge.source);
@@ -862,8 +1022,11 @@ var init_apply_change_set = __esm({
862
1022
  }
863
1023
  const finalNodeIds = new Set(desiredNodes.keys());
864
1024
  const reRanSources = /* @__PURE__ */ new Set();
865
- for (const e of desiredEdges.values())
1025
+ for (const e of desiredEdges.values()) {
1026
+ if (SYNTHETIC_MINT_SOURCES.has(e.source))
1027
+ continue;
866
1028
  reRanSources.add(e.source);
1029
+ }
867
1030
  let currentNodeKeys;
868
1031
  let currentEdges;
869
1032
  try {
@@ -3426,6 +3589,7 @@ var init_src2 = __esm({
3426
3589
  "../graph/dist/src/index.js"() {
3427
3590
  "use strict";
3428
3591
  init_import();
3592
+ init_relationship_refs();
3429
3593
  init_apply_change_set();
3430
3594
  init_migrations();
3431
3595
  init_queries();
@@ -8612,6 +8776,27 @@ var init_condition_extractor = __esm({
8612
8776
  conditionIndex: index
8613
8777
  }
8614
8778
  };
8779
+ const isWellFormedFieldId = (id) => {
8780
+ const body = id.startsWith("CustomField:") ? id.slice("CustomField:".length) : "";
8781
+ if (body.length === 0 || body.startsWith("$"))
8782
+ return false;
8783
+ const parts = body.split(".");
8784
+ return parts.length === 2 && parts[0].length > 0 && parts[1].length > 0;
8785
+ };
8786
+ const fieldEdges = fieldRefs.filter(isWellFormedFieldId).map((toId) => ({
8787
+ fromId: conditionContextId,
8788
+ toId,
8789
+ edgeType: "readsFrom",
8790
+ confidence,
8791
+ source: EXTRACTOR_SOURCE,
8792
+ properties: {
8793
+ kind,
8794
+ conditionIndex: index,
8795
+ // The firer the condition belongs to, so a consumer reading the field's
8796
+ // incoming edges can name the Flow / rule without a second hop.
8797
+ firerId: parentId
8798
+ }
8799
+ }));
8615
8800
  const mirror = {
8616
8801
  kind,
8617
8802
  conditionContextId,
@@ -8619,13 +8804,14 @@ var init_condition_extractor = __esm({
8619
8804
  fieldRefs,
8620
8805
  ...sourceName !== null ? { sourceName } : {}
8621
8806
  };
8622
- return { node, edge, mirror };
8807
+ return { node, edge, fieldEdges, mirror };
8623
8808
  };
8624
8809
  extractConditions = (options) => {
8625
8810
  const { parentId, sources, parentSourcePath, parentApiVersion = null, parentObjectApiName = null, indexOffset = 0 } = options;
8626
8811
  const conditionNodes = [];
8627
8812
  const firesWhenEdges = [];
8628
8813
  const conditionsMirror = [];
8814
+ const conditionFieldEdges = [];
8629
8815
  for (let i = 0; i < sources.length; i += 1) {
8630
8816
  const source = sources[i];
8631
8817
  const index = indexOffset + i;
@@ -8693,8 +8879,14 @@ var init_condition_extractor = __esm({
8693
8879
  conditionNodes.push(triple.node);
8694
8880
  firesWhenEdges.push(triple.edge);
8695
8881
  conditionsMirror.push(triple.mirror);
8882
+ conditionFieldEdges.push(...triple.fieldEdges);
8696
8883
  }
8697
- return { conditionNodes, firesWhenEdges, conditionsMirror };
8884
+ return {
8885
+ conditionNodes,
8886
+ firesWhenEdges,
8887
+ conditionsMirror,
8888
+ conditionFieldEdges
8889
+ };
8698
8890
  };
8699
8891
  }
8700
8892
  });
@@ -9157,7 +9349,7 @@ var init_workflow_rule = __esm({
9157
9349
  actions.push(resolved.value);
9158
9350
  }
9159
9351
  const conditionSources = collectWorkflowRuleConditionSources(rule);
9160
- const { conditionNodes, firesWhenEdges, conditionsMirror } = extractConditions({
9352
+ const { conditionNodes, firesWhenEdges, conditionsMirror, conditionFieldEdges } = extractConditions({
9161
9353
  parentId: ruleId,
9162
9354
  sources: conditionSources,
9163
9355
  parentSourcePath: path2,
@@ -9227,7 +9419,7 @@ var init_workflow_rule = __esm({
9227
9419
  for (const action of actions) {
9228
9420
  edges.push(...edgesForAction(action, ruleId, objectApiName, alertTemplateMap, fieldUpdateMap, seen));
9229
9421
  }
9230
- edges.push(...firesWhenEdges);
9422
+ edges.push(...firesWhenEdges, ...conditionFieldEdges);
9231
9423
  return ok({ node, edges, conditionNodes });
9232
9424
  };
9233
9425
  extractWorkflowRule = async (path2) => {
@@ -9775,7 +9967,7 @@ var init_approval_process = __esm({
9775
9967
  }
9776
9968
  }
9777
9969
  }
9778
- const { conditionNodes, firesWhenEdges, conditionsMirror } = extractConditions({
9970
+ const { conditionNodes, firesWhenEdges, conditionsMirror, conditionFieldEdges } = extractConditions({
9779
9971
  parentId: processId,
9780
9972
  sources: entryConditionSources,
9781
9973
  parentSourcePath: path2,
@@ -9899,7 +10091,7 @@ var init_approval_process = __esm({
9899
10091
  properties: { referenceKind: "allowedSubmitter", submitterType }
9900
10092
  });
9901
10093
  }
9902
- edges.push(...firesWhenEdges);
10094
+ edges.push(...firesWhenEdges, ...conditionFieldEdges);
9903
10095
  return ok({ nodes: [node, ...conditionNodes], edges });
9904
10096
  };
9905
10097
  }
@@ -10161,7 +10353,7 @@ var init_assignment_rule = __esm({
10161
10353
  conditionSources.push(source);
10162
10354
  conditionEntryIndices.push(entryIndex);
10163
10355
  }
10164
- const { conditionNodes, firesWhenEdges, conditionsMirror } = extractConditions({
10356
+ const { conditionNodes, firesWhenEdges, conditionsMirror, conditionFieldEdges } = extractConditions({
10165
10357
  parentId: ruleId,
10166
10358
  sources: conditionSources,
10167
10359
  parentSourcePath: path2,
@@ -10245,7 +10437,7 @@ var init_assignment_rule = __esm({
10245
10437
  });
10246
10438
  }
10247
10439
  }
10248
- edges.push(...firesWhenEdges);
10440
+ edges.push(...firesWhenEdges, ...conditionFieldEdges);
10249
10441
  return ok({ node, edges, conditionNodes });
10250
10442
  };
10251
10443
  extractAssignmentRule = async (path2) => {
@@ -10906,7 +11098,7 @@ var init_auto_response_rule = __esm({
10906
11098
  conditionSources.push(entry.conditionSource);
10907
11099
  }
10908
11100
  }
10909
- const { conditionNodes, firesWhenEdges, conditionsMirror } = extractConditions({
11101
+ const { conditionNodes, firesWhenEdges, conditionsMirror, conditionFieldEdges } = extractConditions({
10910
11102
  parentId: ruleId,
10911
11103
  sources: conditionSources,
10912
11104
  parentSourcePath: path2,
@@ -10957,7 +11149,7 @@ var init_auto_response_rule = __esm({
10957
11149
  }
10958
11150
  });
10959
11151
  }
10960
- edges.push(...firesWhenEdges);
11152
+ edges.push(...firesWhenEdges, ...conditionFieldEdges);
10961
11153
  return ok({ node, edges, conditionNodes });
10962
11154
  };
10963
11155
  extractAutoResponseRule = async (path2) => {
@@ -12676,13 +12868,27 @@ var init_custom_application = __esm({
12676
12868
  });
12677
12869
 
12678
12870
  // ../extractors/dist/src/formula-references.js
12679
- var TOKENIZER_SOURCE, PERMISSION_GLOBAL_REFERENCE, buildReferencesEdges;
12871
+ var TOKENIZER_SOURCE, PERMISSION_GLOBAL_REFERENCE, collectRelationshipRefs, buildReferencesEdges;
12680
12872
  var init_formula_references = __esm({
12681
12873
  "../extractors/dist/src/formula-references.js"() {
12682
12874
  "use strict";
12683
12875
  init_src5();
12684
12876
  TOKENIZER_SOURCE = "formula-tokenizer";
12685
12877
  PERMISSION_GLOBAL_REFERENCE = /^\$Permission\.([A-Za-z_][A-Za-z_0-9]*)$/i;
12878
+ collectRelationshipRefs = (formula) => {
12879
+ const tokenized = tokenizeFormula(formula);
12880
+ if (!tokenized.ok)
12881
+ return [];
12882
+ const seen = /* @__PURE__ */ new Set();
12883
+ for (const ref of tokenized.value.references) {
12884
+ if (!ref.path.includes("."))
12885
+ continue;
12886
+ if (ref.path.startsWith("$"))
12887
+ continue;
12888
+ seen.add(ref.path);
12889
+ }
12890
+ return [...seen].sort();
12891
+ };
12686
12892
  buildReferencesEdges = (formula, fromId, parentObjectApiName, tokenizedFromField) => {
12687
12893
  const tokenized = tokenizeFormula(formula);
12688
12894
  if (!tokenized.ok)
@@ -12742,7 +12948,7 @@ var init_formula_references = __esm({
12742
12948
  import { readFile as readFile26 } from "node:fs/promises";
12743
12949
  import { basename as basename4, dirname as dirname11 } from "node:path";
12744
12950
  import { XMLParser as XMLParser17, XMLValidator as XMLValidator17 } from "fast-xml-parser";
12745
- var FIELD_FILE_SUFFIX, ROOT_ELEMENT16, FIELDS_DIR_NAME, PICKLIST_TYPES, FORMULA_ELEMENT_NAME, STANDARD_FIELD_TYPES, unwrapSingle17, coerceBoolean11, toNullableString, toNullableNumber, toBooleanWithDefault, coerceIsActiveDefaultTrue, readAndValidateXml16, validateRoot12, derivePathParts3, extractPicklistValues, extractControllingFieldInfo, extractValueSetName, SUMMARY_DATA_TYPE, extractSummaryInfo, buildProperties, extractCustomField;
12951
+ var FIELD_FILE_SUFFIX, ROOT_ELEMENT16, FIELDS_DIR_NAME, PICKLIST_TYPES, FORMULA_ELEMENT_NAME, ROLLUP_SUMMARY_SOURCE, STANDARD_FIELD_TYPES, unwrapSingle17, coerceBoolean11, toNullableString, toNullableNumber, toBooleanWithDefault, coerceIsActiveDefaultTrue, readAndValidateXml16, validateRoot12, derivePathParts3, extractPicklistValues, extractControllingFieldInfo, extractValueSetName, SUMMARY_DATA_TYPE, extractSummaryInfo, extractSummaryFilterFields, buildProperties, extractCustomField;
12746
12952
  var init_custom_field = __esm({
12747
12953
  "../extractors/dist/src/custom-field.js"() {
12748
12954
  "use strict";
@@ -12754,6 +12960,7 @@ var init_custom_field = __esm({
12754
12960
  FIELDS_DIR_NAME = "fields";
12755
12961
  PICKLIST_TYPES = ["Picklist", "MultiselectPicklist"];
12756
12962
  FORMULA_ELEMENT_NAME = "formula";
12963
+ ROLLUP_SUMMARY_SOURCE = "rollup-summary";
12757
12964
  STANDARD_FIELD_TYPES = {
12758
12965
  Email: "Email",
12759
12966
  Phone: "Phone",
@@ -12932,6 +13139,26 @@ var init_custom_field = __esm({
12932
13139
  summaryOperation: toNullableString(rootObj["summaryOperation"])
12933
13140
  };
12934
13141
  };
13142
+ extractSummaryFilterFields = (rootObj) => {
13143
+ const raw = rootObj["summaryFilterItems"];
13144
+ if (raw === void 0 || raw === null)
13145
+ return [];
13146
+ const items = Array.isArray(raw) ? raw : [raw];
13147
+ const seen = /* @__PURE__ */ new Set();
13148
+ const out = [];
13149
+ for (const item of items) {
13150
+ if (typeof item !== "object" || item === null)
13151
+ continue;
13152
+ const field = toNullableString(item["field"]);
13153
+ if (field === null || field.length === 0)
13154
+ continue;
13155
+ if (seen.has(field))
13156
+ continue;
13157
+ seen.add(field);
13158
+ out.push(field);
13159
+ }
13160
+ return out;
13161
+ };
12935
13162
  buildProperties = (rootObj, dataType) => {
12936
13163
  const isPicklist = PICKLIST_TYPES.includes(dataType);
12937
13164
  const formula = toNullableString(rootObj["formula"]);
@@ -12971,6 +13198,15 @@ var init_custom_field = __esm({
12971
13198
  // `true` keeps stored fields byte-identical while making formula fields
12972
13199
  // self-describing.
12973
13200
  ...isFormula ? { isFormula: true } : {},
13201
+ // OMIT-when-empty: the cross-object traversals in this formula, verbatim
13202
+ // (`Parent__r.Field__c`). Only the import-time relationship resolver can
13203
+ // turn these into edges — it is the only layer that can see the lookup
13204
+ // fields of every OTHER object. Stored rather than dropped so a field read
13205
+ // exclusively through a traversal stops looking orphaned.
13206
+ ...(() => {
13207
+ const relationshipRefs = isFormula ? collectRelationshipRefs(formula ?? "") : [];
13208
+ return relationshipRefs.length > 0 ? { formulaRelationshipRefs: relationshipRefs } : {};
13209
+ })(),
12974
13210
  // OMIT-when-null (unlike the fixed keys above): only GlobalValueSet-driven
12975
13211
  // picklists carry a value-set name, and a `valueSetName: null` row on every
12976
13212
  // CustomField would churn every rendered markdown file in every vault
@@ -13075,6 +13311,28 @@ var init_custom_field = __esm({
13075
13311
  source: "custom-field-extractor",
13076
13312
  properties: { relationshipType }
13077
13313
  }));
13314
+ const summaryInfo = extractSummaryInfo(rootObj, dataType);
13315
+ const rollupTargets = [
13316
+ ["summarizedField", summaryInfo.summarizedField],
13317
+ ["summaryForeignKey", summaryInfo.summaryForeignKey],
13318
+ // A roll-up's FILTER also names child fields, and the platform refuses to
13319
+ // delete those too — the filter would no longer compile. Same file, same
13320
+ // parent-side invisibility as the two above.
13321
+ ...dataType === SUMMARY_DATA_TYPE ? extractSummaryFilterFields(rootObj).map((field) => ["summaryFilterItem", field]) : []
13322
+ ];
13323
+ const rollupEdges = rollupTargets.filter((entry) => entry[1] !== null && entry[1].length > 0).map(([rollupRole, target]) => ({
13324
+ fromId: nodeId,
13325
+ toId: `CustomField:${target}`,
13326
+ edgeType: "references",
13327
+ confidence: "declared",
13328
+ source: ROLLUP_SUMMARY_SOURCE,
13329
+ properties: {
13330
+ rollupRole,
13331
+ // OMIT-when-null so a `count` roll-up (no operation recorded) does not
13332
+ // carry a null row into every stored edge.
13333
+ ...summaryInfo.summaryOperation !== null ? { summaryOperation: summaryInfo.summaryOperation } : {}
13334
+ }
13335
+ }));
13078
13336
  const valueSetName = extractValueSetName(rootObj);
13079
13337
  const valueSetEdges = valueSetName !== null ? [
13080
13338
  {
@@ -13088,7 +13346,13 @@ var init_custom_field = __esm({
13088
13346
  ] : [];
13089
13347
  return ok({
13090
13348
  nodes: [node],
13091
- edges: [parentEdge, ...referencesEdges, ...lookupEdges, ...valueSetEdges]
13349
+ edges: [
13350
+ parentEdge,
13351
+ ...referencesEdges,
13352
+ ...lookupEdges,
13353
+ ...rollupEdges,
13354
+ ...valueSetEdges
13355
+ ]
13092
13356
  });
13093
13357
  };
13094
13358
  }
@@ -14639,7 +14903,7 @@ var init_sharing_rules = __esm({
14639
14903
 
14640
14904
  // ../extractors/dist/src/enterprise-metadata.js
14641
14905
  import { readFile as readFile34 } from "node:fs/promises";
14642
- var EXTRACTOR_SOURCE14, readText, extractXmlValues, USER_CRITERIA_PROFILE_ID_RE, UNRESOLVED_PROFILE_PREFIX, extractUserCriteriaProfileIds, inferReportObjectApiName, RECORD_CONTEXT_PREFIX, rescopeRecordContextField, extractFieldRefs, LIST_VIEW_FILTER_PSEUDO_FIELDS, isLegacyDottedAddress, isWellFormedColumnField, isWellFormedFilterField, extractListViewFilterRefs, REPORT_DETAIL_LIST_CAP, extractReportCriteriaItems, parseReportGroupingBlock, resolveReportFieldToken, extractReportDetail, extractReportTypeDetail, extractTransactionSecurityAction, extractMilestoneDetails, readListViewSharedTo, makeNode2, extractEnterpriseMetadata, extractReport, extractDashboard, extractListView, extractReportType, extractCustomPermission, extractCertificate, extractTransactionSecurityPolicy, FLEXIPAGE_PAGE_TYPES, extractFlexiPagePermissionRefs, extractFlexiPageEmbeddedFlows, extractFlexiPage, extractPermissionSetGroup, extractRestrictionRule, extractScopingRule, extractEntitlementProcess, extractMilestoneType, extractServiceChannel, extractQueueRoutingConfig, extractPresenceUserConfig;
14906
+ var EXTRACTOR_SOURCE14, readText, extractXmlValues, USER_CRITERIA_PROFILE_ID_RE, UNRESOLVED_PROFILE_PREFIX, extractUserCriteriaProfileIds, inferReportObjectApiName, RECORD_CONTEXT_PREFIX, rescopeRecordContextField, extractFieldRefs, LIST_VIEW_FILTER_PSEUDO_FIELDS, isLegacyDottedAddress, isWellFormedColumnField, isWellFormedFilterField, extractListViewFilterRefs, REPORT_DETAIL_LIST_CAP, extractReportCriteriaItems, parseReportGroupingBlock, resolveReportFieldToken, extractReportDetail, extractReportTypeDetail, extractTransactionSecurityAction, extractMilestoneDetails, readListViewSharedTo, makeNode2, extractEnterpriseMetadata, extractReport, extractDashboard, extractListView, extractReportType, extractCustomPermission, extractCertificate, extractTransactionSecurityPolicy, FLEXIPAGE_PAGE_TYPES, extractFlexiPagePermissionRefs, extractFlexiPageEmbeddedFlows, RELATED_LIST_BLOCK_RE, RELATED_LIST_API_NAME_RE, RELATED_LIST_ALIASES_RE, VALUE_RE, extractFlexiPageRelatedListFieldRefs, extractFlexiPage, extractPermissionSetGroup, extractRestrictionRule, extractScopingRule, extractEntitlementProcess, extractMilestoneType, extractServiceChannel, extractQueueRoutingConfig, extractPresenceUserConfig;
14643
14907
  var init_enterprise_metadata = __esm({
14644
14908
  "../extractors/dist/src/enterprise-metadata.js"() {
14645
14909
  "use strict";
@@ -15284,6 +15548,27 @@ var init_enterprise_metadata = __esm({
15284
15548
  }
15285
15549
  return [...seen].sort();
15286
15550
  };
15551
+ RELATED_LIST_BLOCK_RE = /<componentInstance>[\s\S]*?<\/componentInstance>/g;
15552
+ RELATED_LIST_API_NAME_RE = /<name>relatedListApiName<\/name>\s*<value>([^<]+)<\/value>/;
15553
+ RELATED_LIST_ALIASES_RE = /<name>relatedListFieldAliases<\/name>\s*<valueList>([\s\S]*?)<\/valueList>/;
15554
+ VALUE_RE = /<value>([^<]+)<\/value>/g;
15555
+ extractFlexiPageRelatedListFieldRefs = (xml) => {
15556
+ const out = [];
15557
+ for (const block of xml.match(RELATED_LIST_BLOCK_RE) ?? []) {
15558
+ const relatedListApiName = RELATED_LIST_API_NAME_RE.exec(block)?.[1]?.trim();
15559
+ const aliasList = RELATED_LIST_ALIASES_RE.exec(block)?.[1];
15560
+ if (relatedListApiName === void 0 || relatedListApiName.length === 0 || aliasList === void 0) {
15561
+ continue;
15562
+ }
15563
+ const fields = [
15564
+ ...new Set([...aliasList.matchAll(VALUE_RE)].map((m) => m[1]?.trim() ?? "").filter((v) => v.length > 0))
15565
+ ].sort();
15566
+ if (fields.length === 0)
15567
+ continue;
15568
+ out.push({ relatedListApiName, fields });
15569
+ }
15570
+ return out;
15571
+ };
15287
15572
  extractFlexiPage = async (path2) => {
15288
15573
  const text = await readText(path2);
15289
15574
  if (!text.ok)
@@ -15296,6 +15581,7 @@ var init_enterprise_metadata = __esm({
15296
15581
  const fieldRefs = extractFieldRefs(text.value, sobjectType).refs;
15297
15582
  const permissionRefs = extractFlexiPagePermissionRefs(text.value);
15298
15583
  const embeddedFlows = extractFlexiPageEmbeddedFlows(text.value);
15584
+ const relatedListFieldRefs = extractFlexiPageRelatedListFieldRefs(text.value);
15299
15585
  const edges = fieldRefs.map((fieldId) => ({
15300
15586
  fromId: nodeId,
15301
15587
  toId: fieldId,
@@ -15342,7 +15628,8 @@ var init_enterprise_metadata = __esm({
15342
15628
  fieldRefs,
15343
15629
  rawReferenceCount: fieldRefs.length,
15344
15630
  permissionRefs,
15345
- embeddedFlows
15631
+ embeddedFlows,
15632
+ ...relatedListFieldRefs.length > 0 ? { relatedListFieldRefs } : {}
15346
15633
  });
15347
15634
  return ok({ nodes: [node], edges });
15348
15635
  };
@@ -16376,7 +16663,7 @@ var init_escalation_rule = __esm({
16376
16663
  conditionSources.push(entry.conditionSource);
16377
16664
  }
16378
16665
  }
16379
- const { conditionNodes, firesWhenEdges, conditionsMirror } = extractConditions({
16666
+ const { conditionNodes, firesWhenEdges, conditionsMirror, conditionFieldEdges } = extractConditions({
16380
16667
  parentId: ruleId,
16381
16668
  sources: conditionSources,
16382
16669
  parentSourcePath: path2,
@@ -16445,7 +16732,7 @@ var init_escalation_rule = __esm({
16445
16732
  }
16446
16733
  }
16447
16734
  }
16448
- edges.push(...firesWhenEdges);
16735
+ edges.push(...firesWhenEdges, ...conditionFieldEdges);
16449
16736
  return ok({ node, edges, conditionNodes });
16450
16737
  };
16451
16738
  extractEscalationRule = async (path2) => {
@@ -17427,7 +17714,7 @@ var init_flow_dataflow = __esm({
17427
17714
  // ../extractors/dist/src/flow.js
17428
17715
  import { readFile as readFile43 } from "node:fs/promises";
17429
17716
  import { XMLParser as XMLParser33, XMLValidator as XMLValidator33 } from "fast-xml-parser";
17430
- var FLOW_FILE_SUFFIX, ROOT_ELEMENT32, FLOW_XML_PARSER_OPTIONS, REQUIRED_ELEMENTS11, ALLOWED_STATUS, EDGE_SOURCE3, RECORD_TRIGGER_TYPES, RECORD_SCOPED_TRIGGER_TYPES, unwrapSingle34, toArray18, toNullableString3, toNonEmptyString2, readAndValidateXml32, validateRoot23, extractStartProperties, ASYNC_AFTER_COMMIT_PATH, extractScheduledPathTypes, buildStartEdge, buildPlatformEventListensToEdge, buildActionCallEdges, buildSubflowEdges, collectActionCallSummaries, buildRecordLookupEdges, ASSIGNED_VALUE_LITERAL_WRAPPERS, parseAssignedValue, attachDataflowTrace, buildInputAssignmentEdges, buildDataflowReadEdges, buildRecordCreateEdges, WHOLE_RECORD_DISCLOSURE, resolveInputReferenceObject, buildObjectDmlProps, buildRecordUpdateEdges, buildRecordDeleteEdges, BEFORE_SAVE_TRIGGER_TYPE, asRecord, stripMergeWrapper, parseRecordFieldAssignTarget, buildBeforeSaveFieldAssignmentEdges, parseFlowConditionTriplet, buildFlowDecisionSourceName, collectFlowConditionSources, edgeOperation, dedupeAndSortEdges, FAULTABLE_ELEMENTS, analyzeFaultCoverage, FLOW_LABEL_REF_PATTERN, buildLabelReferenceEdges, extractFlow;
17717
+ var FLOW_FILE_SUFFIX, ROOT_ELEMENT32, FLOW_XML_PARSER_OPTIONS, REQUIRED_ELEMENTS11, ALLOWED_STATUS, EDGE_SOURCE3, RECORD_TRIGGER_TYPES, RECORD_SCOPED_TRIGGER_TYPES, unwrapSingle34, toArray18, toNullableString3, toNonEmptyString2, readAndValidateXml32, validateRoot23, extractStartProperties, ASYNC_AFTER_COMMIT_PATH, extractScheduledPathTypes, buildStartEdge, buildPlatformEventListensToEdge, buildActionCallEdges, buildSubflowEdges, collectActionCallSummaries, buildRecordLookupEdges, ASSIGNED_VALUE_LITERAL_WRAPPERS, parseAssignedValue, attachDataflowTrace, buildInputAssignmentEdges, buildDataflowReadEdges, buildRecordCreateEdges, WHOLE_RECORD_DISCLOSURE, resolveInputReferenceObject, buildObjectDmlProps, buildRecordUpdateEdges, buildRecordDeleteEdges, BEFORE_SAVE_TRIGGER_TYPE, asRecord, stripMergeWrapper, parseRecordFieldAssignTarget, buildBeforeSaveFieldAssignmentEdges, pickTripletMember, parseFlowConditionTriplet, buildFlowDecisionSourceName, collectFlowConditionSources, edgeOperation, dedupeAndSortEdges, FAULTABLE_ELEMENTS, analyzeFaultCoverage, FLOW_LABEL_REF_PATTERN, buildLabelReferenceEdges, extractFlow;
17431
17718
  var init_flow = __esm({
17432
17719
  "../extractors/dist/src/flow.js"() {
17433
17720
  "use strict";
@@ -18078,19 +18365,27 @@ var init_flow = __esm({
18078
18365
  }
18079
18366
  return edges;
18080
18367
  };
18368
+ pickTripletMember = (obj, keys) => {
18369
+ for (const key of keys) {
18370
+ const v = unwrapSingle34(obj[key]);
18371
+ if (v !== void 0 && v !== null && v !== "")
18372
+ return v;
18373
+ }
18374
+ return null;
18375
+ };
18081
18376
  parseFlowConditionTriplet = (raw) => {
18082
18377
  if (typeof raw !== "object" || raw === null)
18083
18378
  return null;
18084
18379
  const obj = raw;
18085
- const fieldRaw = unwrapSingle34(obj["leftValueReference"]);
18086
- if (fieldRaw === void 0 || fieldRaw === null || fieldRaw === "") {
18380
+ const fieldRaw = pickTripletMember(obj, ["leftValueReference", "field"]);
18381
+ if (fieldRaw === null) {
18087
18382
  return null;
18088
18383
  }
18089
18384
  const operatorRaw = unwrapSingle34(obj["operator"]);
18090
18385
  if (operatorRaw === void 0 || operatorRaw === null || operatorRaw === "") {
18091
18386
  return null;
18092
18387
  }
18093
- const rightValueRaw = unwrapSingle34(obj["rightValue"]);
18388
+ const rightValueRaw = pickTripletMember(obj, ["rightValue", "value"]);
18094
18389
  let value = null;
18095
18390
  if (typeof rightValueRaw === "object" && rightValueRaw !== null) {
18096
18391
  const wrapper = rightValueRaw;
@@ -18327,14 +18622,14 @@ var init_flow = __esm({
18327
18622
  rawEdges.push(...buildLabelReferenceEdges(flowId, xmlResult.value));
18328
18623
  const edges = dedupeAndSortEdges(rawEdges);
18329
18624
  const conditionSources = collectFlowConditionSources(rootObj);
18330
- const { conditionNodes, firesWhenEdges, conditionsMirror } = extractConditions({
18625
+ const { conditionNodes, firesWhenEdges, conditionsMirror, conditionFieldEdges } = extractConditions({
18331
18626
  parentId: flowId,
18332
18627
  sources: conditionSources,
18333
18628
  parentSourcePath: path2,
18334
18629
  parentApiVersion: apiVersion,
18335
18630
  parentObjectApiName: startProps.triggerObject
18336
18631
  });
18337
- const allEdges = [...edges, ...firesWhenEdges];
18632
+ const allEdges = [...edges, ...firesWhenEdges, ...conditionFieldEdges];
18338
18633
  const node = {
18339
18634
  id: flowId,
18340
18635
  type: "Flow",
@@ -25530,7 +25825,7 @@ var init_validation_rule = __esm({
25530
25825
  const errorMessage = String(unwrapSingle66(rootObj["errorMessage"]));
25531
25826
  const active = coerceBoolean40(unwrapSingle66(rootObj["active"]));
25532
25827
  const conditionSources = errorConditionFormula.length === 0 ? [] : [{ kind: "formula", expression: errorConditionFormula }];
25533
- const { conditionNodes, firesWhenEdges, conditionsMirror } = extractConditions({
25828
+ const { conditionNodes, firesWhenEdges, conditionsMirror, conditionFieldEdges } = extractConditions({
25534
25829
  parentId: nodeId,
25535
25830
  sources: conditionSources,
25536
25831
  parentSourcePath: path2,
@@ -25566,7 +25861,7 @@ var init_validation_rule = __esm({
25566
25861
  const referencesEdges = buildReferencesEdges(errorConditionFormula, nodeId, objectApiName, FORMULA_ELEMENT_NAME2);
25567
25862
  return ok({
25568
25863
  nodes: [node, ...conditionNodes],
25569
- edges: [parentEdge, ...referencesEdges, ...firesWhenEdges]
25864
+ edges: [parentEdge, ...referencesEdges, ...firesWhenEdges, ...conditionFieldEdges]
25570
25865
  });
25571
25866
  };
25572
25867
  }
@@ -26564,7 +26859,7 @@ var init_src6 = __esm({
26564
26859
 
26565
26860
  // ../mcp/dist/src/prompts.js
26566
26861
  import { ErrorCode, GetPromptRequestSchema, ListPromptsRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
26567
- var userMessage, GENERATOR_PREAMBLE, PLAYBOOK_PREAMBLE, PROMPTS, PROMPT_BY_NAME, MCP_PROMPTS, getMcpPrompt, registerPrompts;
26862
+ var userMessage, GENERATOR_PREAMBLE, PLAYBOOK_PREAMBLE, FIELD_AUDIT_PREAMBLE, PROMPTS, PROMPT_BY_NAME, MCP_PROMPTS, getMcpPrompt, registerPrompts;
26568
26863
  var init_prompts = __esm({
26569
26864
  "../mcp/dist/src/prompts.js"() {
26570
26865
  "use strict";
@@ -26583,6 +26878,9 @@ var init_prompts = __esm({
26583
26878
  2. Live steps need the opt-in live plane. If off, run offline steps, then offer \`sfi.live_consent { grant: true }\` and re-run live steps \u2014 never infer live facts from the vault.
26584
26879
  3. Cite canonical component ids; stamp provenance (\`offline_snapshot\` / \`live_org\`) and confidence. Use any tool's \`rendered\` field verbatim when present.
26585
26880
  4. Single-fact questions belong on \`sfi.route_question\`, not a playbook.`;
26881
+ FIELD_AUDIT_PREAMBLE = `You are running an sf-intelligence FIELD AUDIT \u2014 deciding whether fields can be deleted, and what must happen first. This is a document that authorises destruction, so its value is entirely a function of whether the reader can tell your evidence apart from your inference.
26882
+
26883
+ The one principle everything else follows from: **population tells you how much data is in a field; it never tells you what depends on it.** The two agree on busy, obviously-live fields and diverge everywhere else \u2014 formula fields, transient state flags, integration keys, anything frozen. Every serious error in this kind of analysis comes from substituting one for the other.`;
26586
26884
  PROMPTS = [
26587
26885
  {
26588
26886
  meta: {
@@ -26831,6 +27129,66 @@ Playbook \`health\` \u2014 operational pulse (mostly live):
26831
27129
  Report: red/amber/green pulse with specific failing signals named.`)
26832
27130
  ]
26833
27131
  })
27132
+ },
27133
+ {
27134
+ meta: {
27135
+ name: "sfi.field_audit",
27136
+ description: "Field-deletion audit: decide Keep / Review / Deprecate-then-Remove / Remove for a field or an object\u2019s fields, tracing every dependency first. Also validates an existing field-cleanup analysis.",
27137
+ arguments: [
27138
+ {
27139
+ name: "objectApiName",
27140
+ description: "The object whose fields are under audit (e.g. Account or MyObject__c).",
27141
+ required: true
27142
+ },
27143
+ {
27144
+ name: "fieldApiNames",
27145
+ description: "Optional comma-separated field api names to scope the audit. Omit to assess the whole object.",
27146
+ required: false
27147
+ }
27148
+ ]
27149
+ },
27150
+ build: (args) => ({
27151
+ description: "Field-deletion audit with dependency tracing.",
27152
+ messages: [
27153
+ userMessage(`${FIELD_AUDIT_PREAMBLE}
27154
+
27155
+ Object under audit: \`${args?.["objectApiName"] ?? "<objectApiName>"}\`${args?.["fieldApiNames"] !== void 0 && args["fieldApiNames"].length > 0 ? `
27156
+ Fields scoped to: ${args["fieldApiNames"]}` : "\nScope: every custom field on the object."}
27157
+
27158
+ Run in this order. Do not skip step 0 \u2014 it is what makes the rest trustworthy.
27159
+
27160
+ 0. **Orient and calibrate.**
27161
+ - \`sfi.org_card\` for component counts and DECLARED coverage gaps.
27162
+ - \`sfi.coverage_report\`. If Report/Dashboard reads \`pending\`, the report pull was capped (default \`SFI_REPORTS_CAP\`=500, ranked by usage) \u2014 say so, and treat every report count as a floor, not a total.
27163
+ - **Positive control:** pick a field you can already prove is referenced, run the same tools against it, and confirm they return something. A zero from an uncalibrated method is not a finding.
27164
+
27165
+ 1. **Per field, gather \u2014 do not judge yet.**
27166
+ - \`sfi.field_360\` \u2014 full profile across validation, formulas, writers, readers, UI, integrations.
27167
+ - \`sfi.find_field_anywhere\` \u2014 every incoming edge grouped by component type.
27168
+ - \`sfi.safe_to_delete_field\` \u2014 the verdict with its reasoning chain. Use \`format: 'checklist'\` when you want ordered pre-work.
27169
+ - \`sfi.find_formula_references\` for formula referrers.
27170
+
27171
+ 2. **Read the structure before any number.** \`required\` + \`unique\` + \`externalId\` with no in-org writer is an integration upsert key \u2014 the strongest possible Keep, and the exact shape a naive "nothing writes it" reading calls dead. \`trackHistory: true\` means deleting the field destroys history rows permanently; no export of current values recovers them.
27172
+
27173
+ 3. **A formula field has no population figure.** If the body references \`$User\`, \`$UserRole\`, \`$Profile\` or \`TODAY()\`, the percentage measures who ran the query. If it returns \`IF(...,1,0)\` or \`CASESAFEID(Id)\` it can never be null and measures arithmetic. Read the body; strike the number.
27174
+
27175
+ 4. **Measure flow, not just stock** (live plane only, and only with consent): \`sfi.live_field_population\`, then \`MAX(field)\` and a future-dated count via \`sfi.live_aggregate\`. A field with 750,000 values can be frozen; a field at 0% can be load-bearing. Watch \`sfi.live_budget\`; when it runs out, say which figures are carried forward UNVERIFIED.
27176
+
27177
+ 5. **Record ROLE, never count.** For each consumer, what would it do if the field vanished? A field used as a report or list-view **filter** does not empty the report when deleted \u2014 it silently WIDENS it. Nothing errors, so nobody reports it. That is strictly more dangerous than a lost display column.
27178
+
27179
+ Verdict vocabulary \u2014 four values, not three:
27180
+ - **Keep** \u2014 live dependency, integration contract, or clear business value. Name the blocker, never the population.
27181
+ - **Review** \u2014 genuinely ambiguous. INCOMPLETE unless it names the exact question and the named human who answers it.
27182
+ - **Deprecate-then-Remove** \u2014 dead in practice but holds data, history, or cosmetic references. Needs staged retirement.
27183
+ - **Remove** \u2014 no data of value, no live dependency, every surface checked-negative with a proven method.
27184
+
27185
+ Honesty rules, non-negotiable:
27186
+ - Render any \`coverageCaveat\` BEFORE the verdict, never as a footnote.
27187
+ - Separate "checked and found nothing" from "could not check". They look identical in a report and mean opposite things.
27188
+ - Name the blind spots no static analysis closes: dynamically-built SOQL, reflective \`.get('Field__c')\`, field lists stored as ORG DATA in custom settings or custom metadata (invisible to metadata retrieval AND to this vault \u2014 they fail at runtime, not at deploy), external ETL job definitions, managed-package internals, private report folders, and email-template merge fields.
27189
+ - Never present an \`sfi.safe_to_delete_field\` verdict of \`safe\` as permission to delete. It means no modelled dependency was found, which is a statement about coverage as much as about the field.`)
27190
+ ]
27191
+ })
26834
27192
  }
26835
27193
  ];
26836
27194
  PROMPT_BY_NAME = new Map(PROMPTS.map((p) => [p.meta.name, p]));
@@ -38280,6 +38638,15 @@ var init_coverage_trust = __esm({
38280
38638
  "VisualforceComponent",
38281
38639
  "QuickAction",
38282
38640
  "WorkflowRule",
38641
+ // The remaining condition firers wired to extractConditions. Their
38642
+ // ConditionalContext nodes now emit `readsFrom` edges to the fields their
38643
+ // criteria TEST, so a field can carry a `condition` blocker hiding behind
38644
+ // any of these families — and without them listed, buildCoverageCaveat
38645
+ // stayed silent on an unretrieved family while the verdict read clean.
38646
+ "ApprovalProcess",
38647
+ "AssignmentRule",
38648
+ "AutoResponseRule",
38649
+ "EscalationRule",
38283
38650
  "SharingRule",
38284
38651
  "Report",
38285
38652
  "Dashboard",
@@ -42302,7 +42669,7 @@ var init_roster = __esm({
42302
42669
  },
42303
42670
  {
42304
42671
  name: "sfi.safe_to_delete_field",
42305
- description: "Is this field safe to delete, and what's the evidence? The BA's due-diligence check before retiring a field \"no one uses\" \u2014 it gathers every dependency (validation rules, flows, formulas, layouts, Apex, LWC) so you can prove whether anything still relies on it before signing off. Given a CustomField canonical id (`CustomField:{Object}.{Field}`), composes every incoming dependency edge into a confidence-weighted deletion verdict: `safe` (no incoming edges), `risky` (Apex/LWC references that need spot-checking \u2014 Apex evidence includes parsed-confidence AST edges covering dot-access AND fields referenced only inside inline static SOQL or constant-string Database.query literals, plus heuristic regex-scanner matches; string-BUILT dynamic SOQL stays invisible), `blocking` (declared Flow/ValidationRule/Layout/formula dependencies the platform will refuse to drop), `unknown` (only unrecognised edges), or `review` (NOT proven safe \u2014 incomplete coverage, OR a standard / managed-package field with no node of its own but referenced by edges: it is reviewed from those edges with a not-modeled caveat instead of returning component-not-found; B12). Each category in the `reasoning` array carries its referrer count, up to 5 example referrers (full list via `sfi.get_impact`), and a per-category note explaining the honesty boundary. When an incoming edge carries `properties.confirmedByApi` (stamped by `sfi refresh --with-tooling-api` MetadataComponentDependency enrichment), reasoning entries/examples surface `apiConfirmed: true` as additive evidence \u2014 it does not change the verdict cascade; the tool never calls the Tooling API at query time. Pass `format: 'checklist'` to also get a `checklist` \u2014 a \"before you delete X\" Markdown checklist rendered from the verdict + reasoning, with the `coverageCaveat` surfaced FIRST (never footnoted) and removal steps ordered most-severe-first. It PROPOSES a checklist for a human and never deletes or writes to the org. Pass `format: 'proposal'` (Finding #35) to also get a `proposal` \u2014 a deploy-ready, LOCAL artifact for the field: a populated `destructiveChanges.xml` (the field) + an empty `package.xml`, each led by an evidence comment carrying the verdict, every dependency finding, the vault `sourceTreeHash`, and a REVIEW-before-deploy banner. sfi returns the file STRINGS only \u2014 it NEVER deploys, writes, or connects to the org; a human feeds the files to their own deploy tool (Gearset / Copado / `sf project deploy`). Validated against `docs/schemas/proposal.schema.json`. Emitted for EVERY verdict (a `blocking` field's proposal leads with `verdict: blocking` so it never reads as safe). When the vault holds captured data-shape facts, the response embeds the field's sampled fill rate as a stamped `data_snapshot` `dataShape` block \u2014 CONTEXT ONLY: the verdict is computed purely from the metadata graph and never moves toward safe on a sampled observation. When the heuristic PII recognizer classifies the field pii/sensitive, the response carries a `piiCompliance` block (non-verdict-lowering compliance escalation, with classification/category/message) and the checklist surfaces it FIRST, above the coverage caveat and verdict; a PII field never reads as a bland safe. Heuristic \u2014 absence of the block is NOT a clearance. HYBRID (CR-CAP-L5): when the static verdict would be `safe`, pass `liveEnabled: true` (or grant consent) to cross-check the field's LIVE production population before trusting that verdict \u2014 real data despite zero static references may be written by dynamic Apex, an integration, or another blind spot the scanner cannot see. A `populatedCount > 0` DOWNGRADES the verdict from `safe` to `review` and attaches a `livePopulation` evidence block (`objectApiName`/`fieldApiName`/`totalCount`/`populatedCount`/`populationRate`/`liveQueriedAt`); a zero-population result leaves `safe` standing but still attaches the evidence block, confirming the cross-check ran. The live plane is NEVER a hard dependency \u2014 offline stays fully functional; when it is off, unavailable, or the query errors (budget exhausted, org unreachable), the response fails soft to the disclosed static verdict with a `trust.limitations` line naming the missing evidence ('static-only verdict; live population not checked').",
42672
+ description: "Is this field safe to delete, and what's the evidence? The BA's due-diligence check before retiring a field \"no one uses\" \u2014 it gathers every dependency (validation rules, flows, formulas, roll-up summaries, automation conditions, layouts, Apex, LWC) so you can prove whether anything still relies on it before signing off. Given a CustomField canonical id (`CustomField:{Object}.{Field}`), composes every incoming dependency edge into a confidence-weighted deletion verdict: `safe` (no incoming edges), `risky` (Apex/LWC references that need spot-checking \u2014 Apex evidence includes parsed-confidence AST edges covering dot-access AND fields referenced only inside inline static SOQL or constant-string Database.query literals, plus heuristic regex-scanner matches; string-BUILT dynamic SOQL stays invisible), `blocking` (declared Flow/ValidationRule/formula dependencies the platform will refuse to drop, plus two coupling classes declared on OTHER components: `rollup` \u2014 a roll-up summary on the PARENT object that aggregates, anchors on, or filters by this field, with each example naming its own `rollupRole` (`summarizedField` / `summaryForeignKey` / `summaryFilterItem`) so the citation is the coupling that actually exists; and `condition` \u2014 a criterion that TESTS this field, minted from any of the seven wired firer families: Flow entry criteria, Flow decisions, validation-rule conditions, workflow-rule criteria, and approval-process, assignment-rule, auto-response-rule and escalation-rule criteria. Each condition example carries the `firerId` of the component whose criteria it is (the example `id` is a synthetic ConditionalContext node), so an approval-process blocker is cited as an approval process rather than as a Flow. Conditions are listed, never evaluated: the vault does not know whether a runtime record satisfies them. ONE REFERRER IS COUNTED ONCE: a validation rule whose `errorConditionFormula` both references and tests a field reaches it by TWO edges tokenized from that one string, so it is reported once under `validation` (the truthful label \u2014 not `formula`, which means \"another formula FIELD references this\") and the folded row is disclosed on that example as `alsoVia: ['condition']` rather than counted again. Additive conditions never fold: a rule testing a field its direct reference could not resolve, or a Flow that WRITES and separately TESTS one field, each keep their own category, count and citation. Formula examples likewise carry the `traversalPath` (`Parent__r.Field__c`) when the reference was resolved through a cross-object relationship rather than tokenized directly), `unknown` (only unrecognised edges), or `review` (NOT proven safe \u2014 incomplete coverage, OR a standard / managed-package field with no node of its own but referenced by edges: it is reviewed from those edges with a not-modeled caveat instead of returning component-not-found; B12). Each category in the `reasoning` array carries its referrer count, up to 5 example referrers (full list via `sfi.get_impact`), and a per-category note explaining the honesty boundary. When an incoming edge carries `properties.confirmedByApi` (stamped by `sfi refresh --with-tooling-api` MetadataComponentDependency enrichment), reasoning entries/examples surface `apiConfirmed: true` as additive evidence \u2014 it does not change the verdict cascade; the tool never calls the Tooling API at query time. Pass `format: 'checklist'` to also get a `checklist` \u2014 a \"before you delete X\" Markdown checklist rendered from the verdict + reasoning, with the `coverageCaveat` surfaced FIRST (never footnoted) and removal steps ordered most-severe-first. It PROPOSES a checklist for a human and never deletes or writes to the org. Pass `format: 'proposal'` (Finding #35) to also get a `proposal` \u2014 a deploy-ready, LOCAL artifact for the field: a populated `destructiveChanges.xml` (the field) + an empty `package.xml`, each led by an evidence comment carrying the verdict, every dependency finding, the vault `sourceTreeHash`, and a REVIEW-before-deploy banner. sfi returns the file STRINGS only \u2014 it NEVER deploys, writes, or connects to the org; a human feeds the files to their own deploy tool (Gearset / Copado / `sf project deploy`). Validated against `docs/schemas/proposal.schema.json`. Emitted for EVERY verdict (a `blocking` field's proposal leads with `verdict: blocking` so it never reads as safe). When the vault holds captured data-shape facts, the response embeds the field's sampled fill rate as a stamped `data_snapshot` `dataShape` block \u2014 CONTEXT ONLY: the verdict is computed purely from the metadata graph and never moves toward safe on a sampled observation. When the heuristic PII recognizer classifies the field pii/sensitive, the response carries a `piiCompliance` block (non-verdict-lowering compliance escalation, with classification/category/message) and the checklist surfaces it FIRST, above the coverage caveat and verdict; a PII field never reads as a bland safe. Heuristic \u2014 absence of the block is NOT a clearance. HYBRID (CR-CAP-L5): when the static verdict would be `safe`, pass `liveEnabled: true` (or grant consent) to cross-check the field's LIVE production population before trusting that verdict \u2014 real data despite zero static references may be written by dynamic Apex, an integration, or another blind spot the scanner cannot see. A `populatedCount > 0` DOWNGRADES the verdict from `safe` to `review` and attaches a `livePopulation` evidence block (`objectApiName`/`fieldApiName`/`totalCount`/`populatedCount`/`populationRate`/`liveQueriedAt`); a zero-population result leaves `safe` standing but still attaches the evidence block, confirming the cross-check ran. The live plane is NEVER a hard dependency \u2014 offline stays fully functional; when it is off, unavailable, or the query errors (budget exhausted, org unreachable), the response fails soft to the disclosed static verdict with a `trust.limitations` line naming the missing evidence ('static-only verdict; live population not checked'). UPGRADE PATH: the roll-up coupling, condition and resolved formula-traversal edge families were added in 0.3.0, so a vault BUILT by an older sf-intelligence does not hold them and their absence proves nothing. When `manifest.version` is older than the running plugin the response carries a `builderVersionCaveat` (also mirrored into `trust.limitations` and surfaced above the verdict in the checklist) and an otherwise-`safe` verdict is reported as `review` \u2014 NOT proven safe \u2014 until `sfi refresh` rebuilds the vault. The coverage caveat cannot catch this: those metadata families WERE retrieved; the extractor that reads them did not exist yet.",
42306
42673
  inputSchema: SAFE_TO_DELETE_FIELD_INPUT_SCHEMA
42307
42674
  },
42308
42675
  {
@@ -42780,7 +43147,7 @@ var init_roster = __esm({
42780
43147
  // cross-tier field-forensics questions.
42781
43148
  {
42782
43149
  name: "sfi.field_360",
42783
- description: "A complete 360 profile of a single FIELD \u2014 everything that touches that field across validation, formulas, automation, code, UI, integrations, and emails, in one place. (Field forensics \u2014 this is about a field, NOT a user Profile or permissions.) The go-to answer for \"give me the full profile of this field\", \"the full picture of {Object}.{Field}\", \"everything that uses {Object}.{Field}\", \"what touches this field across automation and code\", or a BA's field impact assessment before a change. v3.0 unified field-forensics synthesis tool. Given a CustomField canonical id (or short `<Object>.<Field>` form), composes every prior tier's reads of the field into one structured response with ten optional content sections (`validates`, `formulas`, `writers`, `readers`, `ui`, `integrations`, `automations`, `emails`, `dependencies`, `summary`) plus the v3.0 constitutional honesty axis. Optional `includeSections` narrows the response; `maxRowsPerSection` (default 50, max 200) bounds per-section row counts; `groupBy` (default `'source'`) reshuffles the rendering hint. The `summary` carries per-section unfiltered counts AND a `riskLevel` (`'low' | 'medium' | 'high'`) computed per PLAN-v3.0 \xA74.1 with the specific `riskFactors[]` enumerated; PII risk factors are computed live via the heuristic pii recognizer (EncryptedText / SSN / financial names), not read from a stamped property. The `boundaries[]` array carries the verbatim Q165 disclosure naming which surfaces are composed vs folded elsewhere (list views, reports, dashboards); `dataNotAvailable: ['list-view-filters', 'reports', 'dashboards']` surfaces verbatim regardless of section filter. D3: a Flow decision / record-trigger filter that references this field carries NO `readsFrom` edge (it is a `firesWhen` edge to a ConditionalContext), so such Flow reads are RECONSTRUCTED from the graph's ConditionalContext `fieldRefs` and surfaced in `readers` as heuristic-confidence rows (`source: flow-condition-reads-scan:*`, deduped against real `readsFrom` readers) \u2014 otherwise `readers` would read 0 for a field several Flows filter on; `boundaries[]` also names the referrer classes still NOT composed into any section (roll-up source coupling, layout related-list placement). Top-level `confidence` reports `'mixed'` when sections span tiers (the typical case for any real-org field). Honesty axis (verbatim, ALWAYS): synthesis without omission disclosure is a contract violation; the report is the COMPLETE answer ONLY for extracted axes. Invalid prefix surfaces as `invalid-query`; unknown ids surface as `component-not-found`. When the vault holds captured data-shape facts (`refresh --with-data-shape`), the response embeds a `dataShape` block \u2014 the field's sampled fill rate as a stamped `data_snapshot` (sampled + TTL-checked; context, never a live read).",
43150
+ description: "A complete 360 profile of a single FIELD \u2014 everything that touches that field across validation, formulas, automation, code, UI, integrations, and emails, in one place. (Field forensics \u2014 this is about a field, NOT a user Profile or permissions.) The go-to answer for \"give me the full profile of this field\", \"the full picture of {Object}.{Field}\", \"everything that uses {Object}.{Field}\", \"what touches this field across automation and code\", or a BA's field impact assessment before a change. v3.0 unified field-forensics synthesis tool. Given a CustomField canonical id (or short `<Object>.<Field>` form), composes every prior tier's reads of the field into one structured response with ten optional content sections (`validates`, `formulas`, `writers`, `readers`, `ui`, `integrations`, `automations`, `emails`, `dependencies`, `summary`) plus the v3.0 constitutional honesty axis. A validation rule that reaches the field BOTH ways (a direct `references` edge and its own condition's `readsFrom`, both tokenized from one `errorConditionFormula`) is listed ONCE under `validates` \u2014 `validates` and `automations` never both hold one rule \u2014 with the fold named in `boundaries[]`; the folded rows still count on the automation RISK axis, so `riskLevel` is unaffected by the collapse. Optional `includeSections` narrows the response; `maxRowsPerSection` (default 50, max 200) bounds per-section row counts; `groupBy` (default `'source'`) reshuffles the rendering hint. The `summary` carries per-section unfiltered counts AND a `riskLevel` (`'low' | 'medium' | 'high'`) computed per PLAN-v3.0 \xA74.1 with the specific `riskFactors[]` enumerated; PII risk factors are computed live via the heuristic pii recognizer (EncryptedText / SSN / financial names), not read from a stamped property. The `boundaries[]` array carries the verbatim Q165 disclosure naming which surfaces are composed vs folded elsewhere (list views, reports, dashboards); `dataNotAvailable: ['list-view-filters', 'reports', 'dashboards']` surfaces verbatim regardless of section filter. D3: a Flow decision / record-trigger filter that references this field carries NO `readsFrom` edge (it is a `firesWhen` edge to a ConditionalContext), so such Flow reads are RECONSTRUCTED from the graph's ConditionalContext `fieldRefs` and surfaced in `readers` as heuristic-confidence rows (`source: flow-condition-reads-scan:*`, deduped against real `readsFrom` readers) \u2014 otherwise `readers` would read 0 for a field several Flows filter on; `boundaries[]` also names the referrer classes still NOT composed into any section (roll-up source coupling, layout related-list placement). Top-level `confidence` reports `'mixed'` when sections span tiers (the typical case for any real-org field). Honesty axis (verbatim, ALWAYS): synthesis without omission disclosure is a contract violation; the report is the COMPLETE answer ONLY for extracted axes. Invalid prefix surfaces as `invalid-query`; unknown ids surface as `component-not-found`. When the vault holds captured data-shape facts (`refresh --with-data-shape`), the response embeds a `dataShape` block \u2014 the field's sampled fill rate as a stamped `data_snapshot` (sampled + TTL-checked; context, never a live read).",
42784
43151
  inputSchema: FIELD_360_INPUT_SCHEMA
42785
43152
  },
42786
43153
  {
@@ -56209,18 +56576,35 @@ var init_concept_model = __esm({
56209
56576
  }
56210
56577
  });
56211
56578
  EDGE_SEMANTICS = Object.freeze({
56212
- formulaTokenizer: {
56213
- edgeType: "references",
56214
- source: "formula-tokenizer",
56215
- category: "formula",
56216
- verdict: "blocking"
56217
- },
56579
+ bySource: [
56580
+ {
56581
+ source: "formula-tokenizer",
56582
+ edgeType: "references",
56583
+ fromType: "CustomField",
56584
+ category: "formula",
56585
+ verdict: "blocking"
56586
+ },
56587
+ {
56588
+ source: "rollup-summary",
56589
+ edgeType: "references",
56590
+ category: "rollup",
56591
+ verdict: "blocking"
56592
+ },
56593
+ {
56594
+ source: "relationship-resolver",
56595
+ edgeType: "references",
56596
+ fromType: "CustomField",
56597
+ category: "formula",
56598
+ verdict: "blocking"
56599
+ }
56600
+ ],
56218
56601
  byEdgeType: {
56219
56602
  readsFrom: {
56220
56603
  bySourceType: {
56221
56604
  ApexClass: { category: "apex", verdict: "risky" },
56222
56605
  ApexTrigger: { category: "apex", verdict: "risky" },
56223
56606
  Flow: { category: "flow", verdict: "blocking" },
56607
+ ConditionalContext: { category: "condition", verdict: "blocking" },
56224
56608
  LightningComponentBundle: { category: "frontend", verdict: "risky" },
56225
56609
  AuraDefinitionBundle: { category: "frontend", verdict: "risky" }
56226
56610
  },
@@ -61578,6 +61962,60 @@ var init_flow_field_writers_scan = __esm({
61578
61962
  }
61579
61963
  });
61580
61964
 
61965
+ // ../mcp/dist/src/tools/restated-condition-edges.js
61966
+ var FORMULA_TOKENIZER_SOURCE, CONDITION_EXTRACTOR_SOURCE, FORMULA_CONDITION_KIND, stringProp, isDirectFormulaReference, isFormulaConditionEdge, EMPTY_INDEX, indexRestatedConditionEdges;
61967
+ var init_restated_condition_edges = __esm({
61968
+ "../mcp/dist/src/tools/restated-condition-edges.js"() {
61969
+ "use strict";
61970
+ FORMULA_TOKENIZER_SOURCE = "formula-tokenizer";
61971
+ CONDITION_EXTRACTOR_SOURCE = "condition-extractor";
61972
+ FORMULA_CONDITION_KIND = "formula";
61973
+ stringProp = (edge, key) => {
61974
+ const raw = edge.properties[key];
61975
+ return typeof raw === "string" && raw.length > 0 ? raw : null;
61976
+ };
61977
+ isDirectFormulaReference = (edge) => edge.edgeType === "references" && edge.source === FORMULA_TOKENIZER_SOURCE;
61978
+ isFormulaConditionEdge = (edge) => edge.edgeType === "readsFrom" && edge.source === CONDITION_EXTRACTOR_SOURCE && edge.properties["kind"] === FORMULA_CONDITION_KIND;
61979
+ EMPTY_INDEX = Object.freeze({
61980
+ isRestatingCondition: () => false,
61981
+ isRestatedDirectReference: () => false,
61982
+ suppressedConditionCount: 0
61983
+ });
61984
+ indexRestatedConditionEdges = (edges) => {
61985
+ const directReferrers = /* @__PURE__ */ new Set();
61986
+ for (const edge of edges) {
61987
+ if (isDirectFormulaReference(edge))
61988
+ directReferrers.add(edge.fromId);
61989
+ }
61990
+ if (directReferrers.size === 0)
61991
+ return EMPTY_INDEX;
61992
+ const restatingFirers = /* @__PURE__ */ new Set();
61993
+ let suppressedConditionCount = 0;
61994
+ for (const edge of edges) {
61995
+ if (!isFormulaConditionEdge(edge))
61996
+ continue;
61997
+ const firerId = stringProp(edge, "firerId");
61998
+ if (firerId === null || !directReferrers.has(firerId))
61999
+ continue;
62000
+ restatingFirers.add(firerId);
62001
+ suppressedConditionCount += 1;
62002
+ }
62003
+ if (restatingFirers.size === 0)
62004
+ return EMPTY_INDEX;
62005
+ return {
62006
+ isRestatingCondition: (edge) => {
62007
+ if (!isFormulaConditionEdge(edge))
62008
+ return false;
62009
+ const firerId = stringProp(edge, "firerId");
62010
+ return firerId !== null && restatingFirers.has(firerId);
62011
+ },
62012
+ isRestatedDirectReference: (edge) => isDirectFormulaReference(edge) && restatingFirers.has(edge.fromId),
62013
+ suppressedConditionCount
62014
+ };
62015
+ };
62016
+ }
62017
+ });
62018
+
61581
62019
  // ../mcp/dist/src/tools/field-360.js
61582
62020
  import { z as z52 } from "zod";
61583
62021
  var CUSTOM_FIELD_PREFIX7, DEFAULT_MAX_ROWS_PER_SECTION, HARD_CAP_MAX_ROWS_PER_SECTION, FIELD_360_BYTE_BUDGET, FIELD_360_Q165_DISCLOSURE, SECTION_NAMES, GROUP_BY_VALUES, field360InputSchema, normalizeFieldId, AUTOMATION_NODE_TYPES, INTEGRATION_NODE_TYPES, UI_NODE_TYPES, CODE_NODE_TYPES, buildRow, compareRows, buildSection, resolveEdgeSource, detectIsFormula, extractFieldType, extractReferenceTo, computeOverallConfidence, computeRisk, detectIsPii, emptyBuckets, classifyIncomingEdge, field360Handler;
@@ -61598,6 +62036,7 @@ var init_field_360 = __esm({
61598
62036
  init_phantom_node();
61599
62037
  init_report_dashboard_usage();
61600
62038
  init_resolve_field_or_suggest();
62039
+ init_restated_condition_edges();
61601
62040
  CUSTOM_FIELD_PREFIX7 = "CustomField:";
61602
62041
  DEFAULT_MAX_ROWS_PER_SECTION = 50;
61603
62042
  HARD_CAP_MAX_ROWS_PER_SECTION = 200;
@@ -61606,6 +62045,7 @@ var init_field_360 = __esm({
61606
62045
  SECTION_NAMES = [
61607
62046
  "validates",
61608
62047
  "formulas",
62048
+ "rollups",
61609
62049
  "writers",
61610
62050
  "readers",
61611
62051
  "ui",
@@ -61753,12 +62193,12 @@ var init_field_360 = __esm({
61753
62193
  return "mixed";
61754
62194
  return [...seen][0];
61755
62195
  };
61756
- computeRisk = (perSectionCounts, isPii, isFormula, dependenciesCount) => {
62196
+ computeRisk = (perSectionCounts, isPii, isFormula, dependenciesCount, foldedConditionCount) => {
61757
62197
  const writers = perSectionCounts["writers"] ?? 0;
61758
62198
  const readers = perSectionCounts["readers"] ?? 0;
61759
62199
  const integrations = perSectionCounts["integrations"] ?? 0;
61760
62200
  const emails = perSectionCounts["emails"] ?? 0;
61761
- const automations = perSectionCounts["automations"] ?? 0;
62201
+ const automations = (perSectionCounts["automations"] ?? 0) + foldedConditionCount;
61762
62202
  const factors = [];
61763
62203
  if (integrations >= 2) {
61764
62204
  factors.push(`${integrations}-integrations-exceeds-threshold-2`);
@@ -61802,6 +62242,7 @@ var init_field_360 = __esm({
61802
62242
  emptyBuckets = () => ({
61803
62243
  validates: [],
61804
62244
  formulas: [],
62245
+ rollups: [],
61805
62246
  writers: [],
61806
62247
  readers: [],
61807
62248
  ui: [],
@@ -61817,10 +62258,14 @@ var init_field_360 = __esm({
61817
62258
  buckets.validates.push(row);
61818
62259
  return;
61819
62260
  }
61820
- if (edge.edgeType === "references" && edge.source === "formula-tokenizer") {
62261
+ if (edge.edgeType === "references" && (edge.source === "formula-tokenizer" || edge.source === "relationship-resolver" && source.type === "CustomField")) {
61821
62262
  buckets.formulas.push(row);
61822
62263
  return;
61823
62264
  }
62265
+ if (edge.edgeType === "references" && edge.source === "rollup-summary") {
62266
+ buckets.rollups.push(row);
62267
+ return;
62268
+ }
61824
62269
  if (edge.edgeType === "references" && source.type === "EmailTemplate" && edge.properties["role"] === "body-merge") {
61825
62270
  buckets.emails.push(row);
61826
62271
  return;
@@ -61830,6 +62275,10 @@ var init_field_360 = __esm({
61830
62275
  return;
61831
62276
  }
61832
62277
  if (edge.edgeType === "readsFrom") {
62278
+ if (source.type === "ConditionalContext") {
62279
+ buckets.automations.push(row);
62280
+ return;
62281
+ }
61833
62282
  if (UI_NODE_TYPES.has(source.type) && CODE_NODE_TYPES.has(source.type)) {
61834
62283
  buckets.ui.push(row);
61835
62284
  return;
@@ -61907,6 +62356,7 @@ var init_field_360 = __esm({
61907
62356
  }
61908
62357
  const incoming = incomingResult.value;
61909
62358
  const buckets = emptyBuckets();
62359
+ const resolvedIncoming = [];
61910
62360
  for (const edge of incoming) {
61911
62361
  if (edge.edgeType === "parentOf")
61912
62362
  continue;
@@ -61915,7 +62365,13 @@ var init_field_360 = __esm({
61915
62365
  return sr;
61916
62366
  if (sr.value === null)
61917
62367
  continue;
61918
- classifyIncomingEdge(edge, sr.value, buckets);
62368
+ resolvedIncoming.push({ edge, source: sr.value });
62369
+ }
62370
+ const restated = indexRestatedConditionEdges(resolvedIncoming.map((r) => r.edge));
62371
+ for (const { edge, source } of resolvedIncoming) {
62372
+ if (restated.isRestatingCondition(edge))
62373
+ continue;
62374
+ classifyIncomingEdge(edge, source, buckets);
61919
62375
  }
61920
62376
  const parentObjectApi = fieldNode.parentId !== null && fieldNode.parentId.startsWith("CustomObject:") ? fieldNode.parentId.slice("CustomObject:".length) : fieldId.slice("CustomField:".length).split(".")[0] ?? "";
61921
62377
  const supplementalWriters = await scanSupplementalFlowFieldWriters(ctx, parentObjectApi, fieldNode.apiName);
@@ -61936,6 +62392,11 @@ var init_field_360 = __esm({
61936
62392
  }
61937
62393
  const flowConditionScan = await scanFlowConditionFieldReaders(ctx, fieldId);
61938
62394
  const readerIds = new Set(buckets.readers.map((r) => r.componentId));
62395
+ for (const row of buckets.automations) {
62396
+ const firer = row.properties?.["firerId"];
62397
+ if (typeof firer === "string")
62398
+ readerIds.add(firer);
62399
+ }
61939
62400
  let flowConditionReaderCount = 0;
61940
62401
  for (const fc of flowConditionScan.readers) {
61941
62402
  if (readerIds.has(fc.flowId))
@@ -61986,6 +62447,7 @@ var init_field_360 = __esm({
61986
62447
  const allBuckets = [
61987
62448
  ["validates", buckets.validates],
61988
62449
  ["formulas", buckets.formulas],
62450
+ ["rollups", buckets.rollups],
61989
62451
  ["writers", buckets.writers],
61990
62452
  ["readers", buckets.readers],
61991
62453
  ["ui", buckets.ui],
@@ -62072,7 +62534,7 @@ var init_field_360 = __esm({
62072
62534
  perSectionCounts[name] = rows.length;
62073
62535
  }
62074
62536
  const isPii = detectIsPii(fieldNode);
62075
- const risk = computeRisk(perSectionCounts, isPii, isFormula, buckets.dependencies.length);
62537
+ const risk = computeRisk(perSectionCounts, isPii, isFormula, buckets.dependencies.length, restated.suppressedConditionCount);
62076
62538
  const grantedByCount = incoming.filter((e) => e.edgeType === "grantedBy").length;
62077
62539
  const summary = {
62078
62540
  perSectionCounts,
@@ -62086,6 +62548,9 @@ var init_field_360 = __esm({
62086
62548
  FIELD_360_Q165_DISCLOSURE,
62087
62549
  "list view column AND filter field IDENTITY are composed into the `listViews` section (heuristic regex; a row's `referenceKind` is `fieldRef` for a column, `filterRef` for a filter predicate, or `columnAndFilter` for both) \u2014 but the saved view's runtime filter PREDICATE EVALUATION (whether a given record passes the filter) stays unmodeled and remains in dataNotAvailable as `list-view-filters`"
62088
62550
  ];
62551
+ if (restated.suppressedConditionCount > 0) {
62552
+ boundaries.push(`${restated.suppressedConditionCount} ConditionalContext \`readsFrom\` row(s) were FOLDED into \`validates\`: each restates a validation rule already listed there (the rule's errorConditionFormula is tokenized into BOTH a direct \`references\` edge and a condition edge, so counting both would report one rule as two referrers). Both edges remain in the graph \u2014 query them with sfi.get_edges \u2014 and the folded rows still count toward the automation risk axis, so \`riskLevel\` is unaffected. Conditions are listed but NOT evaluated: sfi does not know whether any record satisfies them.`);
62553
+ }
62089
62554
  const analytics = reportDashboardUsageDetail(fieldNode);
62090
62555
  const analyticsCoverage = summarizeCoverage(ctx.manifest, [
62091
62556
  "Report",
@@ -62637,7 +63102,7 @@ ${comment}
62637
63102
 
62638
63103
  // ../mcp/dist/src/tools/safe-to-delete-field.js
62639
63104
  import { z as z54 } from "zod";
62640
- var CUSTOM_FIELD_PREFIX9, EXAMPLES_PER_CATEGORY_LIMIT, CATEGORY_ORDER, safeToDeleteFieldInputSchema, classifyEdge, CATEGORY_NOTES, compareExamples, buildReasoning, promoteVerdict, aggregateVerdict, buildCoverageCaveat2, applyCoverageToVerdict2, buildPiiCompliance, VERDICT_ORDER, renderDeleteChecklist, buildSafeToDeleteFieldProposal, coreSafeToDeleteFieldHandler, safeToDeleteFieldHandler;
63105
+ var CUSTOM_FIELD_PREFIX9, EXAMPLES_PER_CATEGORY_LIMIT, CATEGORY_ORDER, safeToDeleteFieldInputSchema, classifyEdge, CATEGORY_NOTES, formatExampleCitation, compareExamples, buildReasoning, promoteVerdict, aggregateVerdict, buildCoverageCaveat2, applyCoverageToVerdict2, EDGE_FAMILIES_ADDED_IN, buildBuilderVersionCaveat, buildPiiCompliance, VERDICT_ORDER, renderDeleteChecklist, buildSafeToDeleteFieldProposal, coreSafeToDeleteFieldHandler, safeToDeleteFieldHandler;
62641
63106
  var init_safe_to_delete_field = __esm({
62642
63107
  "../mcp/dist/src/tools/safe-to-delete-field.js"() {
62643
63108
  "use strict";
@@ -62656,15 +63121,18 @@ var init_safe_to_delete_field = __esm({
62656
63121
  init_proposal_artifact();
62657
63122
  init_report_dashboard_usage();
62658
63123
  init_resolve_field_or_suggest();
63124
+ init_restated_condition_edges();
62659
63125
  CUSTOM_FIELD_PREFIX9 = "CustomField:";
62660
63126
  EXAMPLES_PER_CATEGORY_LIMIT = 5;
62661
63127
  CATEGORY_ORDER = [
62662
63128
  "apex",
62663
63129
  "flow",
63130
+ "condition",
62664
63131
  "workflow",
62665
63132
  "validation",
62666
63133
  "layout",
62667
63134
  "formula",
63135
+ "rollup",
62668
63136
  "integration",
62669
63137
  "permission",
62670
63138
  "sharing",
@@ -62702,10 +63170,16 @@ var init_safe_to_delete_field = __esm({
62702
63170
  path: ["fieldId"]
62703
63171
  });
62704
63172
  classifyEdge = (edge, fromNode) => {
62705
- if (edge.edgeType === EDGE_SEMANTICS.formulaTokenizer.edgeType && edge.source === EDGE_SEMANTICS.formulaTokenizer.source) {
63173
+ for (const rule2 of EDGE_SEMANTICS.bySource) {
63174
+ if (edge.edgeType !== rule2.edgeType)
63175
+ continue;
63176
+ if (edge.source !== rule2.source)
63177
+ continue;
63178
+ if (rule2.fromType !== void 0 && fromNode.type !== rule2.fromType)
63179
+ continue;
62706
63180
  return {
62707
- category: EDGE_SEMANTICS.formulaTokenizer.category,
62708
- verdict: EDGE_SEMANTICS.formulaTokenizer.verdict
63181
+ category: rule2.category,
63182
+ verdict: rule2.verdict
62709
63183
  };
62710
63184
  }
62711
63185
  const rule = EDGE_SEMANTICS.byEdgeType[edge.edgeType];
@@ -62718,10 +63192,12 @@ var init_safe_to_delete_field = __esm({
62718
63192
  CATEGORY_NOTES = Object.freeze({
62719
63193
  apex: "Apex classes and triggers reference this field. Parsed-confidence matches (the default-on Apex AST pass \u2014 dot-access plus inline static SOQL SELECT/WHERE/ORDER BY/GROUP BY fields and constant-string Database.query literals) are real references; heuristic-confidence matches (apex-scanner regex fallback) may include false positives \u2014 spot-check those before deleting. String-BUILT dynamic SOQL remains invisible either way.",
62720
63194
  flow: "Flow definitions read or write this field. The Flow XML names the field literally; deleting the field will break the Flow at runtime.",
63195
+ condition: "A condition EVALUATES this field. Seven firer families mint these: Flow entry criteria, Flow decisions, validation-rule conditions, workflow-rule criteria, and approval-process, assignment-rule, auto-response-rule and escalation-rule criteria. Each example carries the `firerId` of the component whose criteria it is, so the citation names the actual rule to go change rather than the family list (the example `id` itself is a synthetic ConditionalContext node). Salesforce refuses to delete a field a live condition tests, so this is a hard blocker even when the field appears on no layout and in no formula. The condition is listed but NOT evaluated: sfi does not know whether any record satisfies it.",
62721
63196
  workflow: "A WorkflowRule field-update action writes this field. The action will fail at runtime if the field is removed.",
62722
- validation: "A Validation Rule formula references this field. The Validation Rule will fail to compile if the field is removed.",
63197
+ validation: 'A Validation Rule formula references this field. The Validation Rule will fail to compile if the field is removed, and Salesforce refuses the delete while the rule is live. The rule\u2019s `errorConditionFormula` is BOTH a formula reference and a condition that evaluates this field; those are two edges from one tokenized string, so the rule is counted ONCE here and the folded row is disclosed on the example as `alsoVia: ["condition"]` rather than reported as a second referrer. As with any condition, it is listed but NOT evaluated: sfi does not know whether any record satisfies it.',
62723
63198
  layout: "This field is placed on one or more page layouts (deleting the field auto-removes it from them \u2014 Salesforce does not block the delete and the layouts keep working, but users of those layouts will no longer see the field) or referenced by a QuickAction (whose create/edit form is affected). Review the UI impact before deleting.",
62724
- formula: "Another formula field references this field via its formula tokenizer. The referencing formula will fail to compile if this field is removed.",
63199
+ formula: "Another formula field references this field \u2014 either directly (tokenized from the formula body) or through a cross-object relationship traversal (`Parent__r.Field__c`) resolved against the org\u2019s lookup fields. Each EXAMPLE carries the referring field id, and a traversal-derived one also carries the `traversalPath` it was resolved from. The referencing formula will fail to compile if this field is removed.",
63200
+ rollup: "A roll-up summary field on the PARENT object depends on this field in one of three declared roles: `summarizedField` (the roll-up aggregates this field), `summaryForeignKey` (the roll-up is anchored on this master-detail field), or `summaryFilterItem` (the roll-up\u2019s filter tests this field). Each example carries its own `rollupRole`, so the citation names the coupling that actually exists rather than listing the possibilities. Salesforce REFUSES the delete outright while the roll-up exists \u2014 delete or repoint the roll-up first. The coupling is declared in the parent object\u2019s metadata, not this field\u2019s, so a search restricted to this object cannot find it.",
62725
63201
  integration: "An integration surface (external data source, external service) references this field. Removing it may break the outbound or inbound contract.",
62726
63202
  permission: "A Profile or Permission Set grants access to this field. Removing the field will drop the permission grant but is not blocked.",
62727
63203
  sharing: "A SharingRule, Restriction Rule, or Scoping Rule criterion references this field. Deleting the field will break the rule.",
@@ -62730,6 +63206,21 @@ var init_safe_to_delete_field = __esm({
62730
63206
  frontend: "A Lightning Web Component, Aura bundle, Visualforce page, or Visualforce component references this field. Heuristic-confidence matches (LWC/Aura scanners) may include false positives; spot-check the bundle source before deleting.",
62731
63207
  unknown: "An incoming dependency edge was found whose source/type combination is not in the v2.0b classification table. Review the impact via sfi.get_impact before deleting."
62732
63208
  });
63209
+ formatExampleCitation = (e) => {
63210
+ const qualifiers = [];
63211
+ if (e.rollupRole !== void 0)
63212
+ qualifiers.push(`as ${e.rollupRole}`);
63213
+ if (e.traversalPath !== void 0)
63214
+ qualifiers.push(`via ${e.traversalPath}`);
63215
+ if (e.firerId !== void 0)
63216
+ qualifiers.push(`fired by ${e.firerId}`);
63217
+ if (e.alsoVia !== void 0 && e.alsoVia.length > 0) {
63218
+ qualifiers.push(`also via ${[...e.alsoVia].join(", ")}`);
63219
+ }
63220
+ if (e.apiConfirmed === true)
63221
+ qualifiers.push("API-confirmed");
63222
+ return qualifiers.length === 0 ? e.id : `${e.id} (${qualifiers.join(", ")})`;
63223
+ };
62733
63224
  compareExamples = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
62734
63225
  buildReasoning = (buckets) => {
62735
63226
  const out = [];
@@ -62796,6 +63287,19 @@ var init_safe_to_delete_field = __esm({
62796
63287
  fireOnUnknownCoverage: true
62797
63288
  });
62798
63289
  applyCoverageToVerdict2 = (verdict, caveat) => applyCoverageToVerdict(verdict, caveat, "safe", "review");
63290
+ EDGE_FAMILIES_ADDED_IN = "0.3.0";
63291
+ buildBuilderVersionCaveat = (ctx) => {
63292
+ const runningVersion = process.env["SFI_PLUGIN_VERSION"];
63293
+ const builtByVersion = ctx.manifest.version;
63294
+ if (runningVersion === void 0 || runningVersion === "")
63295
+ return void 0;
63296
+ if (typeof builtByVersion !== "string" || builtByVersion === "") {
63297
+ return void 0;
63298
+ }
63299
+ if (!compareVersions(builtByVersion, runningVersion))
63300
+ return void 0;
63301
+ return `This vault was built by sf-intelligence ${builtByVersion}; you are running ${runningVersion}. Roll-up coupling, condition (Flow / validation-rule / workflow-rule / approval-process / assignment-rule / auto-response-rule / escalation-rule criteria) and resolved formula-traversal dependency edges were added in ${EDGE_FAMILIES_ADDED_IN} and are ABSENT until you re-run \`sfi refresh\` \u2014 a field whose only dependency is one of those cannot be seen here. A verdict of "safe" is therefore reported as "review" (NOT proven safe) on this vault.`;
63302
+ };
62799
63303
  buildPiiCompliance = (node) => {
62800
63304
  const { piiClassification, piiCategory } = detectPiiClassification(node);
62801
63305
  if (!isRegulatedPiiClassification(piiClassification)) {
@@ -62822,6 +63326,9 @@ var init_safe_to_delete_field = __esm({
62822
63326
  if (out.coverageCaveat !== void 0) {
62823
63327
  lines.push(`> \u26A0\uFE0F **Coverage caveat (${out.coverageCaveat.status}):** ${out.coverageCaveat.message}`, "");
62824
63328
  }
63329
+ if (out.builderVersionCaveat !== void 0) {
63330
+ lines.push(`> \u26A0\uFE0F **Stale vault:** ${out.builderVersionCaveat}`, "");
63331
+ }
62825
63332
  lines.push(`**Verdict: ${out.verdict.toUpperCase()}**`, "");
62826
63333
  const ordered = [...out.reasoning].sort((a, b) => VERDICT_ORDER[a.verdict] - VERDICT_ORDER[b.verdict]);
62827
63334
  if (ordered.length === 0) {
@@ -62836,7 +63343,7 @@ var init_safe_to_delete_field = __esm({
62836
63343
  r.category,
62837
63344
  r.verdict,
62838
63345
  r.count,
62839
- r.examples.map((e) => e.apiConfirmed === true ? `${e.id} (API-confirmed)` : e.id).join(", ") || "\u2014"
63346
+ r.examples.map(formatExampleCitation).join("; ") || "\u2014"
62840
63347
  ])));
62841
63348
  }
62842
63349
  lines.push("", "_Proposed from the vault's last refresh \u2014 verify against your org before deleting. This never deploys or modifies the org._");
@@ -62850,7 +63357,7 @@ var init_safe_to_delete_field = __esm({
62850
63357
  }));
62851
63358
  }
62852
63359
  reasons.push(...out.reasoning.map((r) => {
62853
- const examples = r.examples.map((e) => e.apiConfirmed === true ? `${e.id} (API-confirmed)` : e.id).join(", ");
63360
+ const examples = r.examples.map(formatExampleCitation).join("; ");
62854
63361
  const apiTag = r.apiConfirmed === true ? ", API-confirmed" : "";
62855
63362
  return `${r.category} (${r.verdict}, ${r.count}${apiTag})${examples ? `: ${examples}` : ""} \u2014 ${r.note}`;
62856
63363
  }));
@@ -63011,6 +63518,7 @@ var init_safe_to_delete_field = __esm({
63011
63518
  }
63012
63519
  const buckets = /* @__PURE__ */ new Map();
63013
63520
  let flsGrantCount = 0;
63521
+ const resolvedEdges = [];
63014
63522
  for (const edge of edgesResult.value) {
63015
63523
  if (edge.edgeType === "parentOf")
63016
63524
  continue;
@@ -63029,13 +63537,31 @@ var init_safe_to_delete_field = __esm({
63029
63537
  if (fromNode === null) {
63030
63538
  continue;
63031
63539
  }
63540
+ resolvedEdges.push({ edge, fromNode });
63541
+ }
63542
+ const restated = indexRestatedConditionEdges(resolvedEdges.map((r) => r.edge));
63543
+ for (const { edge, fromNode } of resolvedEdges) {
63544
+ if (restated.isRestatingCondition(edge))
63545
+ continue;
63032
63546
  const { category, verdict: verdict2 } = classifyEdge(edge, fromNode);
63033
63547
  const apiConfirmed = edge.properties["confirmedByApi"] === true;
63548
+ const edgeString = (key) => {
63549
+ const raw = edge.properties[key];
63550
+ return typeof raw === "string" && raw.length > 0 ? raw : void 0;
63551
+ };
63552
+ const traversalPath = edgeString("traversalPath");
63553
+ const rollupRole = edgeString("rollupRole");
63554
+ const firerId = edgeString("firerId");
63555
+ const alsoVia = restated.isRestatedDirectReference(edge) ? ["condition"] : void 0;
63034
63556
  const example = {
63035
63557
  id: fromNode.id,
63036
63558
  type: fromNode.type,
63037
63559
  apiName: fromNode.apiName,
63038
- ...apiConfirmed ? { apiConfirmed: true } : {}
63560
+ ...apiConfirmed ? { apiConfirmed: true } : {},
63561
+ ...traversalPath !== void 0 ? { traversalPath } : {},
63562
+ ...rollupRole !== void 0 ? { rollupRole } : {},
63563
+ ...firerId !== void 0 ? { firerId } : {},
63564
+ ...alsoVia !== void 0 ? { alsoVia } : {}
63039
63565
  };
63040
63566
  const existing = buckets.get(category);
63041
63567
  if (existing === void 0) {
@@ -63077,7 +63603,9 @@ var init_safe_to_delete_field = __esm({
63077
63603
  const reasoning = buildReasoning(buckets);
63078
63604
  const coverageCaveat = buildCoverageCaveat2(ctx);
63079
63605
  const piiCompliance = buildPiiCompliance(nodeResult.value);
63080
- const staticVerdict = applyCoverageToVerdict2(aggregateVerdict(reasoning), coverageCaveat);
63606
+ const builderVersionCaveat = buildBuilderVersionCaveat(ctx);
63607
+ const coverageVerdict = applyCoverageToVerdict2(aggregateVerdict(reasoning), coverageCaveat);
63608
+ const staticVerdict = builderVersionCaveat !== void 0 && coverageVerdict === "safe" ? "review" : coverageVerdict;
63081
63609
  const dataShape = await readFactBlock(ctx, fieldId, "fillRate");
63082
63610
  const baseConfidence = reasoning.some((r) => r.verdict === "risky") ? "heuristic" : "declared";
63083
63611
  const baseCompleteness = {
@@ -63087,6 +63615,10 @@ var init_safe_to_delete_field = __esm({
63087
63615
  const baseLimitations = [
63088
63616
  "Dependency evidence comes from the last offline vault refresh. String-built dynamic SOQL, reflective Apex, and runtime metadata access remain invisible to static analysis; inline static SOQL and constant-string Database.query field references ARE resolved (parsed-confidence Apex AST edges). A dot-access read/write to a shared Activity custom field through a Task or Event receiver (someTask.Field__c) is NOT a direct parsed edge on the Activity field \u2014 it is attached by a name-based polymorphic import alias (see next limitation).",
63089
63617
  "Polymorphic Activity attribution: a shared Activity custom field can appear as up to three nodes (CustomField:Activity/Task/Event.<field>) that are ONE physical field. A read/write keyed on one representation is attached to the others at import by a name-based alias \u2014 re-pointed onto the Activity base when it exists, otherwise mirrored across the Task/Event describe-snapshot siblings (Task and Event share the custom fields defined on Activity). This is a heuristic name match applied at import, not a declared parent relationship \u2014 an admin should still confirm the referrer before deleting.",
63618
+ // Unconditional when it fires — a `blocking` verdict on a stale vault is
63619
+ // still a verdict computed from incomplete edge families, so the reader is
63620
+ // told even though the verdict did not move.
63621
+ ...builderVersionCaveat !== void 0 ? [builderVersionCaveat] : [],
63090
63622
  REPORT_DASHBOARD_USAGE_CAVEAT,
63091
63623
  ...flsGrantCount > 0 ? [
63092
63624
  `${flsGrantCount} Profile/PermissionSet FLS grant(s) exist on this field (access, not usage) \u2014 excluded from the verdict; see \`sfi.field_access_audit\` or \`sfi.unused_fields_deep\`. Deleting the field drops grants automatically.`
@@ -63130,6 +63662,7 @@ var init_safe_to_delete_field = __esm({
63130
63662
  ...dataShape !== void 0 ? { dataShape } : {},
63131
63663
  reasoning,
63132
63664
  ...coverageCaveat !== void 0 ? { coverageCaveat } : {},
63665
+ ...builderVersionCaveat !== void 0 ? { builderVersionCaveat } : {},
63133
63666
  ...piiCompliance !== void 0 ? { piiCompliance } : {},
63134
63667
  ...flsGrantCount > 0 ? { flsGrantCount } : {},
63135
63668
  ...livePopulation !== void 0 ? { livePopulation } : {},
@@ -68795,7 +69328,7 @@ var init_fleet_find = __esm({
68795
69328
 
68796
69329
  // ../mcp/dist/src/tools/generate-data-dictionary.js
68797
69330
  import { z as z76 } from "zod";
68798
- var Q125_FRESHNESS_DISCLOSURE, STRUCTURAL_DISCLOSURE, INHERITED_CONFIDENCE_DISCLOSURE, ERD_SCOPE_DISCLOSURE, CUSTOM_OBJECT_PREFIX6, generateDataDictionaryInputSchema, stringProp, boolProp, escapeCell, renderFieldRow, compareByApiName, FIELDS_CSV_HEADER, csvRowForField, renderFieldsSection, renderRelationshipsSection, collectOutgoingErdRelationships, CUSTOM_FIELD_PREFIX18, parseCustomFieldId, renderErdSection, renderValidationRulesSection, renderPageLayoutsSection, renderTriggersAndFlowsSection, renderFooter, GENERATED_DOC_ENVELOPE_RESERVE_BYTES, GENERATED_DOC_BUDGET_FLOOR_BYTES, COMPONENT_IDS_SAMPLE_KEEP, GENERATED_DOC_MAX_RESPONSE_BYTES, GENERATED_DOC_RESPONSE_BUDGET_DEFAULT_BYTES, resolveResponseBudgetBytes, generatedDocByteBudget, byteLenOf, splitBodyIntoSections, sectionHeading, renderTruncationNote, fitDocumentToBudget, generateDataDictionaryHandler;
69331
+ var Q125_FRESHNESS_DISCLOSURE, STRUCTURAL_DISCLOSURE, INHERITED_CONFIDENCE_DISCLOSURE, ERD_SCOPE_DISCLOSURE, CUSTOM_OBJECT_PREFIX6, generateDataDictionaryInputSchema, stringProp2, boolProp, escapeCell, renderFieldRow, compareByApiName, FIELDS_CSV_HEADER, csvRowForField, renderFieldsSection, renderRelationshipsSection, collectOutgoingErdRelationships, CUSTOM_FIELD_PREFIX18, parseCustomFieldId, renderErdSection, renderValidationRulesSection, renderPageLayoutsSection, renderTriggersAndFlowsSection, renderFooter, GENERATED_DOC_ENVELOPE_RESERVE_BYTES, GENERATED_DOC_BUDGET_FLOOR_BYTES, COMPONENT_IDS_SAMPLE_KEEP, GENERATED_DOC_MAX_RESPONSE_BYTES, GENERATED_DOC_RESPONSE_BUDGET_DEFAULT_BYTES, resolveResponseBudgetBytes, generatedDocByteBudget, byteLenOf, splitBodyIntoSections, sectionHeading, renderTruncationNote, fitDocumentToBudget, generateDataDictionaryHandler;
68799
69332
  var init_generate_data_dictionary = __esm({
68800
69333
  "../mcp/dist/src/tools/generate-data-dictionary.js"() {
68801
69334
  "use strict";
@@ -68813,19 +69346,19 @@ var init_generate_data_dictionary = __esm({
68813
69346
  objectId: z76.string().min(1),
68814
69347
  format: z76.enum(["markdown", "csv"]).optional()
68815
69348
  });
68816
- stringProp = (properties, key, fallback) => {
69349
+ stringProp2 = (properties, key, fallback) => {
68817
69350
  const v = properties[key];
68818
69351
  return typeof v === "string" && v.length > 0 ? v : fallback;
68819
69352
  };
68820
69353
  boolProp = (properties, key) => properties[key] === true;
68821
69354
  escapeCell = (raw) => raw.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
68822
69355
  renderFieldRow = (field) => {
68823
- const label = escapeCell(stringProp(field.properties, "label", field.label ?? field.apiName));
69356
+ const label = escapeCell(stringProp2(field.properties, "label", field.label ?? field.apiName));
68824
69357
  const apiName = escapeCell(field.apiName);
68825
- const dataType = stringProp(field.properties, "dataType", "Unknown");
68826
- const isFormula = stringProp(field.properties, "formula", "") !== "";
69358
+ const dataType = stringProp2(field.properties, "dataType", "Unknown");
69359
+ const isFormula = stringProp2(field.properties, "formula", "") !== "";
68827
69360
  const type = escapeCell(isFormula ? `${dataType} (formula)` : dataType);
68828
- const description = escapeCell(stringProp(field.properties, "description", ""));
69361
+ const description = escapeCell(stringProp2(field.properties, "description", ""));
68829
69362
  const required = boolProp(field.properties, "required") ? "yes" : "no";
68830
69363
  return `| ${label} | \`${apiName}\` | ${type} | ${description} | ${required} |`;
68831
69364
  };
@@ -68840,10 +69373,10 @@ var init_generate_data_dictionary = __esm({
68840
69373
  "required"
68841
69374
  ];
68842
69375
  csvRowForField = (objectApiName, field) => {
68843
- const label = stringProp(field.properties, "label", field.label ?? field.apiName);
68844
- const dataType = stringProp(field.properties, "dataType", "Unknown");
68845
- const isFormula = stringProp(field.properties, "formula", "") !== "";
68846
- const description = stringProp(field.properties, "description", "");
69376
+ const label = stringProp2(field.properties, "label", field.label ?? field.apiName);
69377
+ const dataType = stringProp2(field.properties, "dataType", "Unknown");
69378
+ const isFormula = stringProp2(field.properties, "formula", "") !== "";
69379
+ const description = stringProp2(field.properties, "description", "");
68847
69380
  const required = boolProp(field.properties, "required");
68848
69381
  return [objectApiName, label, field.apiName, dataType, isFormula, description, required];
68849
69382
  };
@@ -68863,10 +69396,10 @@ var init_generate_data_dictionary = __esm({
68863
69396
  renderRelationshipsSection = (fields) => {
68864
69397
  const rels = [];
68865
69398
  for (const field of fields) {
68866
- const dataType = stringProp(field.properties, "dataType", "");
69399
+ const dataType = stringProp2(field.properties, "dataType", "");
68867
69400
  if (dataType !== "Lookup" && dataType !== "MasterDetail")
68868
69401
  continue;
68869
- const referenceTo = stringProp(field.properties, "referenceTo", "Unknown");
69402
+ const referenceTo = stringProp2(field.properties, "referenceTo", "Unknown");
68870
69403
  rels.push({
68871
69404
  fieldApiName: field.apiName,
68872
69405
  relationshipType: dataType,
@@ -68889,7 +69422,7 @@ var init_generate_data_dictionary = __esm({
68889
69422
  collectOutgoingErdRelationships = (objectApiName, fields) => {
68890
69423
  const rels = [];
68891
69424
  for (const field of fields) {
68892
- const dataType = stringProp(field.properties, "dataType", "");
69425
+ const dataType = stringProp2(field.properties, "dataType", "");
68893
69426
  if (dataType !== "Lookup" && dataType !== "MasterDetail")
68894
69427
  continue;
68895
69428
  const referenceTo = field.properties["referenceTo"];
@@ -68931,7 +69464,7 @@ var init_generate_data_dictionary = __esm({
68931
69464
  }
68932
69465
  const sorted = [...rules].sort(compareByApiName);
68933
69466
  const items = sorted.map((r) => {
68934
- const description = stringProp(r.properties, "description", stringProp(r.properties, "errorMessage", ""));
69467
+ const description = stringProp2(r.properties, "description", stringProp2(r.properties, "errorMessage", ""));
68935
69468
  const desc = description.length > 0 ? ` \u2014 ${escapeCell(description)}` : "";
68936
69469
  return `- \`${escapeCell(r.apiName)}\`${desc}`;
68937
69470
  });
@@ -73819,7 +74352,7 @@ var init_role_hierarchy = __esm({
73819
74352
 
73820
74353
  // ../mcp/dist/src/tools/generate-sharing-summary.js
73821
74354
  import { z as z90 } from "zod";
73822
- var OBJECT_SCAN_CAP, TYPE_SCAN_CAP2, UNMODELED_SHARING_DIMENSIONS_DISCLOSURE, generateSharingSummaryInputSchema, CUSTOM_OBJECT_ID_PREFIX, objectApiNameFromComponentId, escapeCell6, stringProp2, criteriaPredicate, PREDICATE_RULE_TYPES, ruleCriteriaCell, buildRecipientCell, renderObjectSection, renderRoleHierarchySection, sharingRuleObjectApiName, buildSharingRulesIndex, tallyGrants, generateSharingSummaryHandler;
74355
+ var OBJECT_SCAN_CAP, TYPE_SCAN_CAP2, UNMODELED_SHARING_DIMENSIONS_DISCLOSURE, generateSharingSummaryInputSchema, CUSTOM_OBJECT_ID_PREFIX, objectApiNameFromComponentId, escapeCell6, stringProp3, criteriaPredicate, PREDICATE_RULE_TYPES, ruleCriteriaCell, buildRecipientCell, renderObjectSection, renderRoleHierarchySection, sharingRuleObjectApiName, buildSharingRulesIndex, tallyGrants, generateSharingSummaryHandler;
73823
74356
  var init_generate_sharing_summary = __esm({
73824
74357
  "../mcp/dist/src/tools/generate-sharing-summary.js"() {
73825
74358
  "use strict";
@@ -73850,7 +74383,7 @@ var init_generate_sharing_summary = __esm({
73850
74383
  return componentId.startsWith(CUSTOM_OBJECT_ID_PREFIX) ? componentId.slice(CUSTOM_OBJECT_ID_PREFIX.length) : componentId;
73851
74384
  };
73852
74385
  escapeCell6 = (raw) => raw.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
73853
- stringProp2 = (properties, key, fallback) => {
74386
+ stringProp3 = (properties, key, fallback) => {
73854
74387
  const v = properties[key];
73855
74388
  return typeof v === "string" && v.length > 0 ? v : fallback;
73856
74389
  };
@@ -73938,8 +74471,8 @@ var init_generate_sharing_summary = __esm({
73938
74471
  lines.push("| --- | --- | --- | --- | --- |");
73939
74472
  const sorted = [...entry.sharingRules].sort((a, b) => a.apiName < b.apiName ? -1 : a.apiName > b.apiName ? 1 : 0);
73940
74473
  for (const rule of sorted) {
73941
- const ruleType = stringProp2(rule.properties, "ruleType", "unknown");
73942
- const access = stringProp2(rule.properties, "accessLevel", "Unknown");
74474
+ const ruleType = stringProp3(rule.properties, "ruleType", "unknown");
74475
+ const access = stringProp3(rule.properties, "accessLevel", "Unknown");
73943
74476
  const criteria = ruleCriteriaCell(ruleType, rule.properties);
73944
74477
  const recipient = entry.ruleRecipients.get(rule.id) ?? "\u2014";
73945
74478
  lines.push(`| \`${escapeCell6(rule.apiName)}\` | ${escapeCell6(ruleType)} | ${escapeCell6(recipient)} | ${escapeCell6(access)} | ${escapeCell6(criteria)} |`);
@@ -73953,9 +74486,9 @@ var init_generate_sharing_summary = __esm({
73953
74486
  lines.push("| --- | --- | --- | --- | --- |");
73954
74487
  const sortedRr = [...entry.restrictionRules].sort((a, b) => a.apiName < b.apiName ? -1 : a.apiName > b.apiName ? 1 : 0);
73955
74488
  for (const rule of sortedRr) {
73956
- const enforcement = stringProp2(rule.properties, "enforcementType", "Unknown");
73957
- const active = stringProp2(rule.properties, "active", "unknown");
73958
- const recordFilter = stringProp2(rule.properties, "recordFilter", "\u2014");
74489
+ const enforcement = stringProp3(rule.properties, "enforcementType", "Unknown");
74490
+ const active = stringProp3(rule.properties, "active", "unknown");
74491
+ const recordFilter = stringProp3(rule.properties, "recordFilter", "\u2014");
73959
74492
  lines.push(`| \`${escapeCell6(rule.apiName)}\` | ${escapeCell6(rule.type)} | ${escapeCell6(enforcement)} | ${escapeCell6(active)} | ${escapeCell6(recordFilter)} |`);
73960
74493
  }
73961
74494
  }
@@ -73976,7 +74509,7 @@ var init_generate_sharing_summary = __esm({
73976
74509
  lines.push(` ${safe}["${escapeCell6(role.apiName)}"]`);
73977
74510
  }
73978
74511
  for (const role of sorted) {
73979
- const parent = stringProp2(role.properties, "parentRoleId", "");
74512
+ const parent = stringProp3(role.properties, "parentRoleId", "");
73980
74513
  if (parent.length === 0)
73981
74514
  continue;
73982
74515
  const safeChild = role.apiName.replace(/[^A-Za-z0-9_]/g, "_");
@@ -73995,7 +74528,7 @@ var init_generate_sharing_summary = __esm({
73995
74528
  const dot = rule.apiName.indexOf(".");
73996
74529
  if (dot > 0)
73997
74530
  return rule.apiName.slice(0, dot);
73998
- return stringProp2(rule.properties, "sObjectType", "");
74531
+ return stringProp3(rule.properties, "sObjectType", "");
73999
74532
  };
74000
74533
  buildSharingRulesIndex = (rules) => {
74001
74534
  const index = /* @__PURE__ */ new Map();
@@ -74109,7 +74642,7 @@ var init_generate_sharing_summary = __esm({
74109
74642
  const grantsResult = await tallyGrants(ctx, object);
74110
74643
  if (!grantsResult.ok)
74111
74644
  return err(grantsResult.error);
74112
- const owd = stringProp2(object.properties, "sharingModel", "Unknown");
74645
+ const owd = stringProp3(object.properties, "sharingModel", "Unknown");
74113
74646
  const externalOwdRaw = object.properties["externalSharingModel"];
74114
74647
  const externalOwd = typeof externalOwdRaw === "string" && externalOwdRaw.length > 0 ? externalOwdRaw : null;
74115
74648
  const rules = sharingIndex.get(object.apiName) ?? [];
@@ -74750,7 +75283,7 @@ var init_get_subgraph = __esm({
74750
75283
 
74751
75284
  // ../mcp/dist/src/tools/guest-exposure-report.js
74752
75285
  import { z as z94 } from "zod";
74753
- var CUSTOM_SITE_PREFIX, NETWORK_PREFIX, PROFILE_PREFIX, OBJECT_PREFIX, FIELD_PREFIX, APEX_PREFIX2, SHARING_RULE_PREFIX, DEFAULT_LIMIT13, MAX_LIMIT14, FINDINGS_BYTE_BUDGET, SCAN_PAGE_SIZE3, guestExposureReportInputSchema, SEVERITY_RANK2, OBJECT_FLAGS3, flag, stringProp3, boolProp2, objectOfFieldId2, findingObjectApiName, drainType, guestProfileIdForSite, guestExposureReportHandler;
75286
+ var CUSTOM_SITE_PREFIX, NETWORK_PREFIX, PROFILE_PREFIX, OBJECT_PREFIX, FIELD_PREFIX, APEX_PREFIX2, SHARING_RULE_PREFIX, DEFAULT_LIMIT13, MAX_LIMIT14, FINDINGS_BYTE_BUDGET, SCAN_PAGE_SIZE3, guestExposureReportInputSchema, SEVERITY_RANK2, OBJECT_FLAGS3, flag, stringProp4, boolProp2, objectOfFieldId2, findingObjectApiName, drainType, guestProfileIdForSite, guestExposureReportHandler;
74754
75287
  var init_guest_exposure_report = __esm({
74755
75288
  "../mcp/dist/src/tools/guest-exposure-report.js"() {
74756
75289
  "use strict";
@@ -74831,7 +75364,7 @@ var init_guest_exposure_report = __esm({
74831
75364
  "modifyAllRecords"
74832
75365
  ];
74833
75366
  flag = (p, k) => p[k] === true;
74834
- stringProp3 = (p, k) => typeof p[k] === "string" && p[k].length > 0 ? p[k] : null;
75367
+ stringProp4 = (p, k) => typeof p[k] === "string" && p[k].length > 0 ? p[k] : null;
74835
75368
  boolProp2 = (p, k) => typeof p[k] === "boolean" ? p[k] : null;
74836
75369
  objectOfFieldId2 = (fieldId) => {
74837
75370
  if (!fieldId.startsWith(FIELD_PREFIX))
@@ -74867,8 +75400,8 @@ var init_guest_exposure_report = __esm({
74867
75400
  return ok(all);
74868
75401
  };
74869
75402
  guestProfileIdForSite = (site) => {
74870
- const declared = stringProp3(site.properties, "guestProfileName");
74871
- const label = stringProp3(site.properties, "masterLabel") ?? site.apiName;
75403
+ const declared = stringProp4(site.properties, "guestProfileName");
75404
+ const label = stringProp4(site.properties, "masterLabel") ?? site.apiName;
74872
75405
  return `${PROFILE_PREFIX}${declared ?? guestProfileNameForSite(label)}`;
74873
75406
  };
74874
75407
  guestExposureReportHandler = async (ctx, input2) => {
@@ -74990,7 +75523,7 @@ var init_guest_exposure_report = __esm({
74990
75523
  }
74991
75524
  const networkBySiteApiName = /* @__PURE__ */ new Map();
74992
75525
  for (const net of allNetworks) {
74993
- const siteName = stringProp3(net.properties, "site");
75526
+ const siteName = stringProp4(net.properties, "site");
74994
75527
  if (siteName !== null)
74995
75528
  networkBySiteApiName.set(siteName, net);
74996
75529
  }
@@ -75000,7 +75533,7 @@ var init_guest_exposure_report = __esm({
75000
75533
  scopedSites = allSites.filter((s) => s.id === communityId);
75001
75534
  } else {
75002
75535
  const net = allNetworks.find((n) => n.id === communityId);
75003
- const siteName = net !== void 0 ? stringProp3(net.properties, "site") : null;
75536
+ const siteName = net !== void 0 ? stringProp4(net.properties, "site") : null;
75004
75537
  scopedSites = siteName !== null ? allSites.filter((s) => s.apiName === siteName) : [];
75005
75538
  }
75006
75539
  if (scopedSites.length === 0) {
@@ -75029,9 +75562,9 @@ var init_guest_exposure_report = __esm({
75029
75562
  scanTruncated = true;
75030
75563
  const guestRulesBySite = /* @__PURE__ */ new Map();
75031
75564
  for (const rule of rulesResult.value) {
75032
- if (stringProp3(rule.properties, "ruleType") !== "guest")
75565
+ if (stringProp4(rule.properties, "ruleType") !== "guest")
75033
75566
  continue;
75034
- const siteName = stringProp3(rule.properties, "siteName");
75567
+ const siteName = stringProp4(rule.properties, "siteName");
75035
75568
  if (siteName === null)
75036
75569
  continue;
75037
75570
  const bucket = guestRulesBySite.get(siteName) ?? [];
@@ -75045,7 +75578,7 @@ var init_guest_exposure_report = __esm({
75045
75578
  for (const site of scopedSites) {
75046
75579
  const network = networkBySiteApiName.get(site.apiName) ?? null;
75047
75580
  const guestProfileId = guestProfileIdForSite(site);
75048
- const siteLabel = stringProp3(site.properties, "masterLabel") ?? site.apiName;
75581
+ const siteLabel = stringProp4(site.properties, "masterLabel") ?? site.apiName;
75049
75582
  const profileResult = await getNodeById(ctx.graph, guestProfileId);
75050
75583
  if (!profileResult.ok) {
75051
75584
  return err({ kind: "internal", message: `graph query failed: ${profileResult.error.message}` });
@@ -75053,7 +75586,7 @@ var init_guest_exposure_report = __esm({
75053
75586
  const guestProfileResolved = profileResult.value !== null;
75054
75587
  if (guestProfileResolved)
75055
75588
  guestProfilesResolved += 1;
75056
- const experienceBundleId = network !== null && stringProp3(network.properties, "picassoSite") !== null ? `ExperienceBundle:${stringProp3(network.properties, "picassoSite")}` : null;
75589
+ const experienceBundleId = network !== null && stringProp4(network.properties, "picassoSite") !== null ? `ExperienceBundle:${stringProp4(network.properties, "picassoSite")}` : null;
75057
75590
  const communityFindings = [];
75058
75591
  if (guestProfileResolved) {
75059
75592
  const edgesResult = await listEdges(ctx.graph, guestProfileId, {
@@ -75192,7 +75725,7 @@ var init_guest_exposure_report = __esm({
75192
75725
  severity: "low",
75193
75726
  nodeId: rule.id,
75194
75727
  label: rule.apiName,
75195
- detail: `guest sharing rule ${rule.id} (accessLevel ${stringProp3(rule.properties, "accessLevel") ?? "Read"}) shares records with the '${name}' site guest user \u2014 record-level applicability requires org data`,
75728
+ detail: `guest sharing rule ${rule.id} (accessLevel ${stringProp4(rule.properties, "accessLevel") ?? "Read"}) shares records with the '${name}' site guest user \u2014 record-level applicability requires org data`,
75196
75729
  grantConfidence: "declared",
75197
75730
  guestLinkageConfidence: "heuristic"
75198
75731
  });
@@ -75205,10 +75738,10 @@ var init_guest_exposure_report = __esm({
75205
75738
  communities.push({
75206
75739
  communityId: site.id,
75207
75740
  label: siteLabel,
75208
- siteType: stringProp3(site.properties, "siteType"),
75741
+ siteType: stringProp4(site.properties, "siteType"),
75209
75742
  active: boolProp2(site.properties, "active"),
75210
75743
  networkId: network?.id ?? null,
75211
- status: network !== null ? stringProp3(network.properties, "status") : null,
75744
+ status: network !== null ? stringProp4(network.properties, "status") : null,
75212
75745
  selfRegistration: network !== null ? boolProp2(network.properties, "selfRegistration") : null,
75213
75746
  enableGuestFileAccess: network !== null ? boolProp2(network.properties, "enableGuestFileAccess") : null,
75214
75747
  experienceBundleId,
@@ -75223,7 +75756,7 @@ var init_guest_exposure_report = __esm({
75223
75756
  if (communityId === void 0) {
75224
75757
  const modeledSiteApiNames = new Set(allSites.map((s) => s.apiName));
75225
75758
  for (const net of allNetworks) {
75226
- const siteName = stringProp3(net.properties, "site");
75759
+ const siteName = stringProp4(net.properties, "site");
75227
75760
  if (siteName !== null && !modeledSiteApiNames.has(siteName))
75228
75761
  orphanNetworks.push(net.id);
75229
75762
  }
@@ -101960,7 +102493,7 @@ var init_group_membership = __esm({
101960
102493
 
101961
102494
  // ../mcp/dist/src/tools/who-can-access-object.js
101962
102495
  import { z as z162 } from "zod";
101963
- var CUSTOM_OBJECT_PREFIX10, GROUP_MEMBER_PREFIX, DEFAULT_LIMIT25, MAX_LIMIT25, PUBLIC_OWD_READ, whoCanAccessObjectInputBaseSchema, whoCanAccessObjectInputSchema, BLIND_SPOTS, flag3, stringProp4, sharingRuleObjectApiName2, ruleAccessToOp, whoCanAccessObjectHandler;
102496
+ var CUSTOM_OBJECT_PREFIX10, GROUP_MEMBER_PREFIX, DEFAULT_LIMIT25, MAX_LIMIT25, PUBLIC_OWD_READ, whoCanAccessObjectInputBaseSchema, whoCanAccessObjectInputSchema, BLIND_SPOTS, flag3, stringProp5, sharingRuleObjectApiName2, ruleAccessToOp, whoCanAccessObjectHandler;
101964
102497
  var init_who_can_access_object = __esm({
101965
102498
  "../mcp/dist/src/tools/who-can-access-object.js"() {
101966
102499
  "use strict";
@@ -102002,7 +102535,7 @@ var init_who_can_access_object = __esm({
102002
102535
  "Manual sharing, Apex-managed sharing, account/opportunity/case teams, and sharing sets are record/runtime-level and are not modeled."
102003
102536
  ]);
102004
102537
  flag3 = (p, k) => p[k] === true;
102005
- stringProp4 = (p, k) => typeof p[k] === "string" ? p[k] : "";
102538
+ stringProp5 = (p, k) => typeof p[k] === "string" ? p[k] : "";
102006
102539
  sharingRuleObjectApiName2 = (rule) => {
102007
102540
  const parentId = rule.parentId;
102008
102541
  if (typeof parentId === "string" && parentId.startsWith("CustomObject:")) {
@@ -102011,7 +102544,7 @@ var init_who_can_access_object = __esm({
102011
102544
  const dot = rule.apiName.indexOf(".");
102012
102545
  if (dot > 0)
102013
102546
  return rule.apiName.slice(0, dot);
102014
- return stringProp4(rule.properties, "sObjectType");
102547
+ return stringProp5(rule.properties, "sObjectType");
102015
102548
  };
102016
102549
  ruleAccessToOp = (accessLevel) => accessLevel === "Edit" || accessLevel === "ReadWrite" ? "edit" : "read";
102017
102550
  whoCanAccessObjectHandler = async (ctx, input2) => {
@@ -102036,9 +102569,9 @@ var init_who_can_access_object = __esm({
102036
102569
  });
102037
102570
  }
102038
102571
  const objectNode = objectResult.value;
102039
- const owd = stringProp4(objectNode.properties, "sharingModel") || "Unknown";
102572
+ const owd = stringProp5(objectNode.properties, "sharingModel") || "Unknown";
102040
102573
  const owdGrantsAllInternalUsers = PUBLIC_OWD_READ.has(owd);
102041
- const externalOwd = stringProp4(objectNode.properties, "externalSharingModel") ?? null;
102574
+ const externalOwd = stringProp5(objectNode.properties, "externalSharingModel") ?? null;
102042
102575
  const restrictionRulesResult = await listNodesByType(ctx.graph, "RestrictionRule", {
102043
102576
  limit: 500
102044
102577
  });
@@ -102128,11 +102661,11 @@ var init_who_can_access_object = __esm({
102128
102661
  for (const rule of rulesResult.value) {
102129
102662
  if (sharingRuleObjectApiName2(rule) !== objectApiName)
102130
102663
  continue;
102131
- const ruleType = stringProp4(rule.properties, "ruleType");
102132
- const accessLevel = stringProp4(rule.properties, "accessLevel") || "Read";
102664
+ const ruleType = stringProp5(rule.properties, "ruleType");
102665
+ const accessLevel = stringProp5(rule.properties, "accessLevel") || "Read";
102133
102666
  const via = ruleType === "criteria" ? "criteria-sharing-rule" : ruleType === "guest" ? "guest-sharing-rule" : ruleType === "territory" || ruleType === "territoryGroup" ? "territory-sharing-rule" : "owner-sharing-rule";
102134
- const predicate = ruleType === "owner" ? "" : stringProp4(rule.properties, "booleanFilter");
102135
- const siteName = ruleType === "guest" ? stringProp4(rule.properties, "siteName") : "";
102667
+ const predicate = ruleType === "owner" ? "" : stringProp5(rule.properties, "booleanFilter");
102668
+ const siteName = ruleType === "guest" ? stringProp5(rule.properties, "siteName") : "";
102136
102669
  const targetsResult = await listEdges(ctx.graph, rule.id, {
102137
102670
  direction: "out",
102138
102671
  edgeType: "sharedWith"
@@ -102401,7 +102934,7 @@ var init_who_can_run = __esm({
102401
102934
 
102402
102935
  // ../mcp/dist/src/tools/why-cant-user-see-record.js
102403
102936
  import { z as z164 } from "zod";
102404
- var OWD_RESTRICTED, ACCESS_RANK, owdRank, grantSatisfiesObjectRead, grantSatisfiesObjectLevel, grantSatisfiesRecordVisible, systemPermsForLevel, ruleAccessSatisfiesLevel, ALL_INTERNAL_USERS_GROUP_ID, ROLE_HIERARCHY_MAX_DEPTH, whyCantUserSeeRecordInputSchema, LIVE_ACCESS_RESOLVER_NOTE, step2, evaluateOWD, evaluatePermissionGrants, noObjectFlags, SYSTEM_BYPASS_PERMS, objectFlagsSatisfyLevel, systemPermsSatisfyLevel, levelRelevantObjectFlags, computeMutedObjectAccess, evaluateSystemPermissions, walkRoleHierarchy, evaluateRoleHierarchy, buildUserMembership, fetchSharingRules, SHARED_TO_ROLE_PREFIX, SUBORDINATE_INHERITANCE, ownerRuleMatches, evaluateOwnerSharingRules, evaluateCriteriaSharingRules, TERRITORY_GUEST_RULE_TYPES, evaluateTerritoryAndGuestRules, listObjectChildRules, evaluateRestrictionRules, evaluateScopingRules, evaluatePermissionSetGroups, UNKNOWN_TAIL, aggregateVerdict8, CUSTOM_OBJECT_PREFIX11, PROFILE_PREFIX6, PERMISSION_SET_PREFIX5, PERMISSION_SET_GROUP_PREFIX2, ROLE_PREFIX2, GROUP_PREFIX2, QUEUE_PREFIX, coerceGroupId, coerceUserContext, evaluateModifyAllDataForCreate, objectApiNameOf, evaluateRecordTypeForCreate, evaluateCreateAccess, whyCantUserSeeRecordHandler;
102937
+ var OWD_RESTRICTED, ACCESS_RANK, owdRank, grantSatisfiesObjectRead, grantSatisfiesObjectLevel, grantSatisfiesRecordVisible, systemPermsForLevel, ruleAccessSatisfiesLevel, ALL_INTERNAL_USERS_GROUP_ID, ROLE_HIERARCHY_MAX_DEPTH, whyCantUserSeeRecordInputSchema, LIVE_ACCESS_RESOLVER_NOTE, step2, evaluateOWD, evaluatePermissionGrants, noObjectFlags, SYSTEM_BYPASS_PERMS, objectFlagsSatisfyLevel, systemPermsSatisfyLevel, levelRelevantObjectFlags, computeMutedObjectAccess, evaluateSystemPermissions, walkRoleHierarchy, evaluateRoleHierarchy, buildUserMembership, fetchSharingRules, SHARED_TO_ROLE_PREFIX, SUBORDINATE_INHERITANCE, ownerRuleMatches, evaluateOwnerSharingRules, evaluateCriteriaSharingRules, TERRITORY_GUEST_RULE_TYPES, evaluateTerritoryAndGuestRules, listObjectChildRules, evaluateRestrictionRules, evaluateScopingRules, evaluatePermissionSetGroups, UNKNOWN_TAIL, aggregateVerdict8, CUSTOM_OBJECT_PREFIX11, PROFILE_PREFIX6, PERMISSION_SET_PREFIX5, PERMISSION_SET_GROUP_PREFIX2, ROLE_PREFIX2, GROUP_PREFIX2, QUEUE_PREFIX, coerceGroupId, coerceUserContext, evaluateModifyAllDataForCreate, objectApiNameOf2, evaluateRecordTypeForCreate, evaluateCreateAccess, whyCantUserSeeRecordHandler;
102405
102938
  var init_why_cant_user_see_record = __esm({
102406
102939
  "../mcp/dist/src/tools/why-cant-user-see-record.js"() {
102407
102940
  "use strict";
@@ -103231,7 +103764,7 @@ var init_why_cant_user_see_record = __esm({
103231
103764
  }
103232
103765
  return ok(step2("SystemPermission", "visible", `Modify All Data grants create on every object: ${holders.join(", ")}`));
103233
103766
  };
103234
- objectApiNameOf = (objectNode, componentId) => typeof objectNode.apiName === "string" && objectNode.apiName.length > 0 ? objectNode.apiName : componentId.replace(/^CustomObject:/, "");
103767
+ objectApiNameOf2 = (objectNode, componentId) => typeof objectNode.apiName === "string" && objectNode.apiName.length > 0 ? objectNode.apiName : componentId.replace(/^CustomObject:/, "");
103235
103768
  evaluateRecordTypeForCreate = async (ctx, componentId, objectNode, userContext) => {
103236
103769
  const edgesResult = await listEdges(ctx.graph, componentId, {
103237
103770
  direction: "out",
@@ -103243,7 +103776,7 @@ var init_why_cant_user_see_record = __esm({
103243
103776
  if (objectRecordTypes.length === 0) {
103244
103777
  return ok(step2("RecordType", "visible", "object has no record types; create is not record-type-gated"));
103245
103778
  }
103246
- const objectApiName = objectApiNameOf(objectNode, componentId);
103779
+ const objectApiName = objectApiNameOf2(objectNode, componentId);
103247
103780
  const granterIds = [];
103248
103781
  if (userContext.profileId !== void 0)
103249
103782
  granterIds.push(userContext.profileId);
@@ -103387,7 +103920,7 @@ var init_why_cant_user_see_record = __esm({
103387
103920
  const createProfileOrPermSet = rawUserContext.profileId !== void 0 || rawUserContext.permissionSetIds !== void 0 && rawUserContext.permissionSetIds.length > 0;
103388
103921
  let createNetAccess = null;
103389
103922
  if (createProfileOrPermSet) {
103390
- const objApi = objectApiNameOf(nodeResult.value, componentId);
103923
+ const objApi = objectApiNameOf2(nodeResult.value, componentId);
103391
103924
  const naResult = await computeMutedObjectAccess(ctx, componentId, objApi, rawUserContext, "create");
103392
103925
  if (!naResult.ok) {
103393
103926
  return err({ kind: "internal", message: `graph query failed: ${naResult.error}` });
@@ -103436,7 +103969,7 @@ var init_why_cant_user_see_record = __esm({
103436
103969
  let mutedByForDeny = [];
103437
103970
  let netAccess = null;
103438
103971
  if (profileOrPermSetSupplied) {
103439
- const objectApiName = objectApiNameOf(nodeResult.value, componentId);
103972
+ const objectApiName = objectApiNameOf2(nodeResult.value, componentId);
103440
103973
  const netAccessResult = await computeMutedObjectAccess(ctx, componentId, objectApiName, rawUserContext, level);
103441
103974
  if (!netAccessResult.ok) {
103442
103975
  return err({
@@ -104833,8 +105366,8 @@ var init_server = __esm({
104833
105366
  init_tools();
104834
105367
  SERVER_NAME = "sf-intelligence";
104835
105368
  resolveServerVersion = () => {
104836
- if ("0.2.4") {
104837
- return "0.2.4";
105369
+ if ("0.2.5") {
105370
+ return "0.2.5";
104838
105371
  }
104839
105372
  for (const rel of ["../package.json", "../../package.json"]) {
104840
105373
  try {
@@ -105700,7 +106233,7 @@ var init_package_version = __esm({
105700
106233
  "use strict";
105701
106234
  readCliPackageVersion = () => {
105702
106235
  if (true)
105703
- return "0.2.4";
106236
+ return "0.2.5";
105704
106237
  for (const rel of ["../package.json", "../../package.json"]) {
105705
106238
  try {
105706
106239
  const raw = readFileSync6(fileURLToPath4(new URL(rel, import.meta.url)), "utf8");
@@ -106648,7 +107181,7 @@ var init_setup_audit_trail = __esm({
106648
107181
  // dist/src/source-reconcile.js
106649
107182
  import { cp, readdir as readdir7, rm as rm3, stat as stat8 } from "node:fs/promises";
106650
107183
  import { basename as basename16, join as join38, relative as relative3, sep as sep4 } from "node:path";
106651
- var BUNDLE_PARENT_DIRS2, KNOWN_SIDECAR_SUFFIXES2, isKnownSidecar2, primaryRelPathForSidecar, walkSourceEntries, authoritativePathSet, reconcileSourceDeletions, syncAuthoritativeRetrieveIntoSource;
107184
+ var BUNDLE_PARENT_DIRS2, KNOWN_SIDECAR_SUFFIXES2, isKnownSidecar2, primaryRelPathForSidecar, walkSourceEntries, DISPATCH_PROBE_ROOT, dispatchTailLength, strippedDxWrapper, comparisonKey, authoritativeIndex, RECONCILE_MAX_DELETE_FRACTION, RECONCILE_GUARD_MIN_CONSIDERED, layoutMismatchRefusal, reconcileSourceDeletions, syncAuthoritativeRetrieveIntoSource;
106652
107185
  var init_source_reconcile = __esm({
106653
107186
  "dist/src/source-reconcile.js"() {
106654
107187
  "use strict";
@@ -106723,43 +107256,103 @@ var init_source_reconcile = __esm({
106723
107256
  };
106724
107257
  await walkDir2(sourceRoot);
106725
107258
  };
106726
- authoritativePathSet = async (authoritativeRoot) => {
107259
+ DISPATCH_PROBE_ROOT = join38(sep4, "__sfi-reconcile-dispatch-probe__");
107260
+ dispatchTailLength = (segments, isDirectory) => {
107261
+ for (let length = 1; length <= segments.length; length += 1) {
107262
+ const tail = segments.slice(segments.length - length);
107263
+ const type = componentTypeFromSourcePath(DISPATCH_PROBE_ROOT, join38(DISPATCH_PROBE_ROOT, ...tail), isDirectory);
107264
+ if (type !== null)
107265
+ return length;
107266
+ }
107267
+ return null;
107268
+ };
107269
+ strippedDxWrapper = (segments) => {
107270
+ for (let i = 0; i <= 1 && i + 1 < segments.length; i += 1) {
107271
+ if (segments[i] === "main" && segments[i + 1] === "default")
107272
+ return segments.slice(i + 2);
107273
+ }
107274
+ return segments;
107275
+ };
107276
+ comparisonKey = (relPath, isDirectory) => {
107277
+ const segments = relPath.split(sep4);
107278
+ const fileName = segments[segments.length - 1] ?? relPath;
107279
+ const primaryRel = primaryRelPathForSidecar(relPath, fileName);
107280
+ const probeSegments = primaryRel === null ? segments : primaryRel.split(sep4);
107281
+ const length = dispatchTailLength(probeSegments, isDirectory);
107282
+ const tail = length === null ? strippedDxWrapper(segments) : segments.slice(segments.length - length);
107283
+ return tail.join("/");
107284
+ };
107285
+ authoritativeIndex = async (authoritativeRoot, typesToReconcile) => {
106727
107286
  const entries = [];
106728
107287
  await walkSourceEntries(authoritativeRoot, entries);
106729
- return new Set(entries.map((entry) => entry.relPath));
107288
+ const keys = /* @__PURE__ */ new Set();
107289
+ let inScopeCount = 0;
107290
+ for (const entry of entries) {
107291
+ keys.add(comparisonKey(entry.relPath, entry.isDirectory));
107292
+ if (typesToReconcile.has(entry.type))
107293
+ inScopeCount += 1;
107294
+ }
107295
+ return { keys, inScopeCount };
107296
+ };
107297
+ RECONCILE_MAX_DELETE_FRACTION = 0.5;
107298
+ RECONCILE_GUARD_MIN_CONSIDERED = 20;
107299
+ layoutMismatchRefusal = (deleteCount, consideredCount, authoritative) => {
107300
+ if (deleteCount === 0 || consideredCount === 0)
107301
+ return null;
107302
+ const reason = (detail) => `Refusing to reconcile source deletions: ${detail}. This is the signature of a source-layout mismatch between the vault tree and the authoritative retrieve output, not of components deleted in the org \u2014 a reconcile that misreads the layout finds nothing in common and proposes to delete everything. The vault was left untouched; re-run the refresh once the layouts agree, or delete the stale files deliberately if the org really did lose them.`;
107303
+ if (deleteCount === consideredCount && authoritative.inScopeCount > 0) {
107304
+ return reason(`all ${consideredCount} in-scope vault file(s) would be deleted even though the retrieve returned ${authoritative.inScopeCount} component file(s) of those same types`);
107305
+ }
107306
+ if (consideredCount >= RECONCILE_GUARD_MIN_CONSIDERED && deleteCount > consideredCount * RECONCILE_MAX_DELETE_FRACTION) {
107307
+ return reason(`${deleteCount} of ${consideredCount} in-scope vault file(s) would be deleted, over the ${Math.round(RECONCILE_MAX_DELETE_FRACTION * 100)}% ceiling for a single refresh`);
107308
+ }
107309
+ return null;
106730
107310
  };
106731
107311
  reconcileSourceDeletions = async (sourceDir, authoritativeDir, typesToReconcile) => {
106732
107312
  if (typesToReconcile.size === 0) {
106733
- return { deletedPaths: [], deletedCount: 0 };
107313
+ return { deletedPaths: [], deletedCount: 0, consideredCount: 0, refused: false };
106734
107314
  }
106735
- const authoritative = await authoritativePathSet(authoritativeDir);
107315
+ const authoritative = await authoritativeIndex(authoritativeDir, typesToReconcile);
106736
107316
  const sourceEntries = [];
106737
107317
  await walkSourceEntries(sourceDir, sourceEntries);
106738
- const deletedPaths = [];
106739
- const deletedBundleDirs = /* @__PURE__ */ new Set();
106740
- for (const entry of sourceEntries) {
106741
- if (!typesToReconcile.has(entry.type))
106742
- continue;
107318
+ const considered = sourceEntries.filter((entry) => typesToReconcile.has(entry.type));
107319
+ const candidates = [];
107320
+ const candidateBundleDirs = /* @__PURE__ */ new Set();
107321
+ for (const entry of considered) {
106743
107322
  const fileName = basename16(entry.absPath);
106744
107323
  const primaryRel = primaryRelPathForSidecar(entry.relPath, fileName);
106745
- const authoritativeKey = primaryRel ?? entry.relPath;
106746
- if (authoritative.has(authoritativeKey) || authoritative.has(entry.relPath))
107324
+ const ownKey = comparisonKey(entry.relPath, entry.isDirectory);
107325
+ const primaryKey = primaryRel === null ? null : comparisonKey(primaryRel, entry.isDirectory);
107326
+ if (authoritative.keys.has(ownKey))
106747
107327
  continue;
106748
- if (entry.isDirectory) {
106749
- deletedBundleDirs.add(entry.absPath);
106750
- deletedPaths.push(entry.relPath);
107328
+ if (primaryKey !== null && authoritative.keys.has(primaryKey))
106751
107329
  continue;
106752
- }
106753
- const parentBundle = [...deletedBundleDirs].find((dir) => entry.absPath.startsWith(`${dir}${sep4}`));
106754
- if (parentBundle !== void 0)
106755
- continue;
106756
- await rm3(entry.absPath, { force: true });
106757
- deletedPaths.push(entry.relPath);
107330
+ if (entry.isDirectory)
107331
+ candidateBundleDirs.add(entry.absPath);
107332
+ candidates.push(entry);
106758
107333
  }
106759
- for (const bundleDir of deletedBundleDirs) {
106760
- await rm3(bundleDir, { recursive: true, force: true });
107334
+ const doomed = candidates.filter((entry) => entry.isDirectory || ![...candidateBundleDirs].some((dir) => entry.absPath.startsWith(`${dir}${sep4}`)));
107335
+ const refusalReason = layoutMismatchRefusal(doomed.length, considered.length, authoritative);
107336
+ if (refusalReason !== null) {
107337
+ return {
107338
+ deletedPaths: [],
107339
+ deletedCount: 0,
107340
+ consideredCount: considered.length,
107341
+ refused: true,
107342
+ refusalReason
107343
+ };
107344
+ }
107345
+ const deletedPaths = [];
107346
+ for (const entry of doomed) {
107347
+ await rm3(entry.absPath, { recursive: entry.isDirectory, force: true });
107348
+ deletedPaths.push(entry.relPath);
106761
107349
  }
106762
- return { deletedPaths, deletedCount: deletedPaths.length };
107350
+ return {
107351
+ deletedPaths,
107352
+ deletedCount: deletedPaths.length,
107353
+ consideredCount: considered.length,
107354
+ refused: false
107355
+ };
106763
107356
  };
106764
107357
  syncAuthoritativeRetrieveIntoSource = async (sourceDir, authoritativeDir) => {
106765
107358
  try {
@@ -107934,19 +108527,26 @@ var init_staged_refresh = __esm({
107934
108527
  // dist/src/commands/refresh.js
107935
108528
  var refresh_exports = {};
107936
108529
  __export(refresh_exports, {
108530
+ FOLD_ERASED_COVERAGE_TYPES: () => FOLD_ERASED_COVERAGE_TYPES,
107937
108531
  PROFILE_COBATCH_GROUP: () => PROFILE_COBATCH_GROUP,
107938
108532
  PROFILE_GRANT_DISCLOSURE: () => PROFILE_GRANT_DISCLOSURE,
108533
+ REPORT_PULL_DISCLOSURE: () => REPORT_PULL_DISCLOSURE,
108534
+ REPORT_RETRIEVE_BATCH_SIZE: () => REPORT_RETRIEVE_BATCH_SIZE,
107939
108535
  appendStandardObjectDescribeFields: () => appendStandardObjectDescribeFields,
107940
108536
  assessProfileGrantIntegrity: () => assessProfileGrantIntegrity,
108537
+ buildCoverageEntries: () => buildCoverageEntries2,
107941
108538
  buildFolderedReportManifest: () => buildFolderedReportManifest,
107942
108539
  buildPackageXml: () => buildPackageXml,
107943
108540
  buildRefreshPulse: () => buildRefreshPulse,
108541
+ chunkFolderedReportManifest: () => chunkFolderedReportManifest,
107944
108542
  classifyForDemandRetrieve: () => classifyForDemandRetrieve,
107945
108543
  classifyRetrieveError: () => classifyRetrieveError,
107946
108544
  computeChangeSummary: () => computeChangeSummary,
107947
108545
  computeProfileGrantStats: () => computeProfileGrantStats,
107948
108546
  countLandedReportMembers: () => countLandedReportMembers,
107949
108547
  decorateProfileGrantCoverage: () => decorateProfileGrantCoverage,
108548
+ decorateReportPullCoverage: () => decorateReportPullCoverage,
108549
+ decorateReportsCapCoverage: () => decorateReportsCapCoverage,
107950
108550
  formatDemandRetrieveSummary: () => formatDemandRetrieveSummary,
107951
108551
  formatRefreshSummary: () => formatRefreshSummary,
107952
108552
  formatReportsCapSummary: () => formatReportsCapSummary,
@@ -107971,7 +108571,7 @@ __export(refresh_exports, {
107971
108571
  import { appendFile as appendFile6, mkdir as mkdir15, readdir as readdir9, readFile as readFile109, rename as rename6, rm as rm5, stat as stat10, writeFile as writeFile14 } from "node:fs/promises";
107972
108572
  import { tmpdir as tmpdir2 } from "node:os";
107973
108573
  import { join as join43, resolve as resolve8 } from "node:path";
107974
- var SF_API_VERSION3, EMPTY_COUNTS, EMPTY_SKIPPED, failed, PACKAGE_VERSION, extractCachePath, loadExtractCache, saveExtractCache, applyApexAstEdges, runRefresh, preserveFactsForSideBuild, aggregateFailuresByType, computeProfileGrantStats, PROFILE_GRANT_DISCLOSURE, PRIOR_GRANTED_BY_FLOOR, assessProfileGrantIntegrity, decorateProfileGrantCoverage, buildCoverageEntries2, decoratePendingCoverage, decorateReportsCapCoverage, countGraphTotals, fullRebuild, importGraph, persistResolveIndexBestEffort, runWithOpenGraph, appendRefreshHistory, appendRiskScores, writeOnboardingDoc, writeOrgCard, diffCounts, sumCounts, metricCounts, signedDelta, findAnnotationOrphans, buildRefreshPulse, computeChangeSummary, TOOLING_API_ENRICHED_TYPES, ENRICH_CANDIDATE_PAGE_SIZE, runToolingApiEnrichment, loadVaultConfig, METADATA_API_NAME2, toApiName, SF_MAX_BUFFER, SF_RETRIEVE_TIMEOUT_MS, SF_QUERY_TIMEOUT_MS, runSf, STANDARD_OBJECTS_TO_MODEL, manifestMembersForType, appendStandardObjectDescribeFields, AUTOMATION_EDGE_TYPES, objectsToExpandManifest, buildPackageXml, getOrgSupportedTypes, getOrgObjectNames, selectManifestTypes, classifyRetrieveError, PROFILE_COBATCH_GROUP, NO_ATOMIC_GROUP, toAtomicUnits, splitTypeBatch, salientErrorLine, summarizeRetrieveFailures, retrieveWithFallback, retrieveTypeBatch, runSfRetrieve, computeReconciledTypes, retrieveAdditiveManifest, runSfRetrieveObjects, buildFolderedReportManifest, REPORT_META_SUFFIXES, REPORT_DIR_MARKERS, countLandedReportMembers, collectReportMetaFiles, runSfRetrieveFolderedReports, reportsCap, runSfRetrieveSmartReports, formatFailure, formatBlock, formatChangeSummary, formatRefreshSummary, formatReportsCapSummary, SKIPPED_TOP_N, KNOWN_UNMODELED_SKIP_DIRS, formatSkippedWarning, formatToolingApiSummary, demandCoverageStatusOf, DEMAND_REFUSAL_REASON, classifyForDemandRetrieve, markDemandQueueDrains, runDemandRetrieve, formatDemandRetrieveSummary, registerRefreshCommand;
108574
+ var SF_API_VERSION3, REPORT_PULL_DISCLOSURE, EMPTY_COUNTS, EMPTY_SKIPPED, failed, PACKAGE_VERSION, extractCachePath, loadExtractCache, saveExtractCache, applyApexAstEdges, runRefresh, preserveFactsForSideBuild, aggregateFailuresByType, computeProfileGrantStats, PROFILE_GRANT_DISCLOSURE, PRIOR_GRANTED_BY_FLOOR, assessProfileGrantIntegrity, decorateProfileGrantCoverage, FOLD_ERASED_COVERAGE_TYPES, buildCoverageEntries2, decoratePendingCoverage, decorateReportsCapCoverage, decorateReportPullCoverage, countGraphTotals, fullRebuild, importGraph, persistResolveIndexBestEffort, runWithOpenGraph, appendRefreshHistory, appendRiskScores, writeOnboardingDoc, writeOrgCard, diffCounts, sumCounts, metricCounts, signedDelta, findAnnotationOrphans, buildRefreshPulse, computeChangeSummary, TOOLING_API_ENRICHED_TYPES, ENRICH_CANDIDATE_PAGE_SIZE, runToolingApiEnrichment, loadVaultConfig, METADATA_API_NAME2, toApiName, SF_MAX_BUFFER, SF_RETRIEVE_TIMEOUT_MS, SF_QUERY_TIMEOUT_MS, runSf, STANDARD_OBJECTS_TO_MODEL, manifestMembersForType, appendStandardObjectDescribeFields, AUTOMATION_EDGE_TYPES, objectsToExpandManifest, buildPackageXml, getOrgSupportedTypes, getOrgObjectNames, selectManifestTypes, classifyRetrieveError, PROFILE_COBATCH_GROUP, NO_ATOMIC_GROUP, toAtomicUnits, splitTypeBatch, salientErrorLine, summarizeRetrieveFailures, retrieveWithFallback, retrieveTypeBatch, runSfRetrieve, computeReconciledTypes, retrieveAdditiveManifest, runSfRetrieveObjects, buildFolderedReportManifest, REPORT_RETRIEVE_BATCH_SIZE, folderedReportPackageXml, chunkFolderedReportManifest, retrieveFolderedReportBatches, REPORT_META_SUFFIXES, REPORT_DIR_MARKERS, countLandedReportMembers, collectReportMetaFiles, runSfRetrieveFolderedReports, reportsCap, runSfRetrieveSmartReports, formatFailure, formatBlock, formatChangeSummary, formatRefreshSummary, formatReportsCapSummary, SKIPPED_TOP_N, KNOWN_UNMODELED_SKIP_DIRS, formatSkippedWarning, formatToolingApiSummary, demandCoverageStatusOf, DEMAND_REFUSAL_REASON, classifyForDemandRetrieve, markDemandQueueDrains, runDemandRetrieve, formatDemandRetrieveSummary, registerRefreshCommand;
107975
108575
  var init_refresh = __esm({
107976
108576
  "dist/src/commands/refresh.js"() {
107977
108577
  "use strict";
@@ -107994,6 +108594,7 @@ var init_refresh = __esm({
107994
108594
  init_refresh_preflight();
107995
108595
  init_snapshot2();
107996
108596
  SF_API_VERSION3 = "62.0";
108597
+ REPORT_PULL_DISCLOSURE = "report/dashboard pull did not complete \u2014 Report/Dashboard coverage is UNPROVEN, not empty";
107997
108598
  EMPTY_COUNTS = { components: {}, edges: {} };
107998
108599
  EMPTY_SKIPPED = Object.freeze({});
107999
108600
  failed = (startedMs, fatalError, errors, counts = EMPTY_COUNTS, skippedDirectories = EMPTY_SKIPPED) => ({
@@ -108205,6 +108806,7 @@ var init_refresh = __esm({
108205
108806
  let pullManifestTypes = null;
108206
108807
  let confirmedTypes = null;
108207
108808
  let sourceReconcileDeleted = 0;
108809
+ let reconcileRefusals = [];
108208
108810
  let retrieveFailures = [];
108209
108811
  if (!opts.noPull) {
108210
108812
  const priorManifest = await loadManifest(configResult.value.vaultRoot);
@@ -108227,6 +108829,18 @@ var init_refresh = __esm({
108227
108829
  if (sourceReconcileDeleted > 0) {
108228
108830
  progress(`Reconciled source: removed ${sourceReconcileDeleted} stale file(s) deleted in the org.`);
108229
108831
  }
108832
+ reconcileRefusals = pulled.value.reconcileRefusals ?? [];
108833
+ if (reconcileRefusals.length > 0) {
108834
+ process.stdout.write(`
108835
+ SOURCE RECONCILE REFUSED (${reconcileRefusals.length}) \u2014 stale files were KEPT, not deleted.
108836
+ This is the layout-mismatch guard: a deletion set that large is far more likely to mean the
108837
+ retrieve layout changed than that the org really dropped that much metadata. The vault is
108838
+ SAFE but may now carry entries for components that no longer exist.
108839
+ ` + reconcileRefusals.map((r) => ` - ${r}
108840
+ `).join("") + `If the org genuinely purged this much, re-run after confirming; otherwise this is a bug.
108841
+
108842
+ `);
108843
+ }
108230
108844
  if (retrieveFailures.length > 0) {
108231
108845
  progress(`Partial retrieve: ${retrieveFailures.length} metadata type(s) failed and were skipped \u2014 the vault is built from what landed. ${summarizeRetrieveFailures(retrieveFailures)}`);
108232
108846
  }
@@ -108250,14 +108864,32 @@ var init_refresh = __esm({
108250
108864
  }
108251
108865
  }
108252
108866
  let reportsCapStats;
108867
+ let reportPull;
108253
108868
  if (opts.withReports === true && !opts.noPull) {
108254
108869
  progress("Pulling ALL folder-based Reports / Dashboards (--with-reports)...");
108255
108870
  const pulledReports = await runSfRetrieveFolderedReports(targetOrg, paths.source);
108256
108871
  if (!pulledReports.ok) {
108257
- progress(`--with-reports pull skipped (non-fatal): ${pulledReports.error}`);
108258
- } else if (pulledReports.value.reports + pulledReports.value.dashboards > 0) {
108259
- progress(`Requested ${pulledReports.value.reports} report(s) + ${pulledReports.value.dashboards} dashboard(s); re-extracting...`);
108260
- walked = await walkAndExtract(paths.source, requestedTypes, prevCache);
108872
+ reportPull = {
108873
+ mode: "full",
108874
+ outcome: "failed",
108875
+ error: pulledReports.error,
108876
+ attemptedAt: (/* @__PURE__ */ new Date()).toISOString()
108877
+ };
108878
+ progress(`--with-reports pull FAILED (non-fatal, recorded): ${pulledReports.error}`);
108879
+ } else {
108880
+ if (pulledReports.value.batchErrors.length > 0) {
108881
+ reportPull = {
108882
+ mode: "full",
108883
+ outcome: "partial",
108884
+ error: pulledReports.value.batchErrors.join("; "),
108885
+ attemptedAt: (/* @__PURE__ */ new Date()).toISOString()
108886
+ };
108887
+ progress(`--with-reports pull lost ${pulledReports.value.batchErrors.length} batch(es) (non-fatal, recorded): ${pulledReports.value.batchErrors[0] ?? ""}`);
108888
+ }
108889
+ if (pulledReports.value.reports + pulledReports.value.dashboards > 0) {
108890
+ progress(`Requested ${pulledReports.value.reports} report(s) + ${pulledReports.value.dashboards} dashboard(s); re-extracting...`);
108891
+ walked = await walkAndExtract(paths.source, requestedTypes, prevCache);
108892
+ }
108261
108893
  }
108262
108894
  } else if (opts.withReports === void 0 && !opts.noPull && requestedTypes === null) {
108263
108895
  const cap2 = reportsCap();
@@ -108265,8 +108897,23 @@ var init_refresh = __esm({
108265
108897
  progress(`Pulling top ${cap2} Reports / Dashboards by usage (default; --no-reports to skip)...`);
108266
108898
  const smart = await runSfRetrieveSmartReports(targetOrg, paths.source, cap2);
108267
108899
  if (!smart.ok) {
108268
- progress(`smart report pull skipped (non-fatal): ${smart.error}`);
108900
+ reportPull = {
108901
+ mode: "smart",
108902
+ outcome: "failed",
108903
+ error: smart.error,
108904
+ attemptedAt: (/* @__PURE__ */ new Date()).toISOString()
108905
+ };
108906
+ progress(`smart report pull FAILED (non-fatal, recorded): ${smart.error}`);
108269
108907
  } else {
108908
+ if (smart.value.batchErrors.length > 0) {
108909
+ reportPull = {
108910
+ mode: "smart",
108911
+ outcome: "partial",
108912
+ error: smart.value.batchErrors.join("; "),
108913
+ attemptedAt: (/* @__PURE__ */ new Date()).toISOString()
108914
+ };
108915
+ progress(`smart report pull lost ${smart.value.batchErrors.length} batch(es) (non-fatal, recorded): ${smart.value.batchErrors[0] ?? ""}`);
108916
+ }
108270
108917
  reportsCapStats = {
108271
108918
  reports: { total: smart.value.totals.reports, requested: smart.value.reports, retrieved: smart.value.landed.reports },
108272
108919
  dashboards: { total: smart.value.totals.dashboards, requested: smart.value.dashboards, retrieved: smart.value.landed.dashboards }
@@ -108331,7 +108978,8 @@ var init_refresh = __esm({
108331
108978
  retrieveFailures,
108332
108979
  ...reconciledTypes !== null ? { reconciledTypes } : {},
108333
108980
  ...apexAstStats !== void 0 ? { apexAstStats } : {},
108334
- ...reportsCapStats !== void 0 ? { reportsCapStats } : {}
108981
+ ...reportsCapStats !== void 0 ? { reportsCapStats } : {},
108982
+ ...reportPull !== void 0 ? { reportPull } : {}
108335
108983
  };
108336
108984
  if (!renameOver) {
108337
108985
  try {
@@ -108455,6 +109103,10 @@ var init_refresh = __esm({
108455
109103
  return { ...rest, errored: true, errorReason: disclosure };
108456
109104
  });
108457
109105
  };
109106
+ FOLD_ERASED_COVERAGE_TYPES = /* @__PURE__ */ new Set([
109107
+ "Report",
109108
+ "Dashboard"
109109
+ ]);
108458
109110
  buildCoverageEntries2 = (counts, skippedDirectories, requestedTypes, sourceRoot, failures, confirmedTypes) => {
108459
109111
  const failuresByType = aggregateFailuresByType(sourceRoot, failures);
108460
109112
  const entries = [];
@@ -108462,7 +109114,8 @@ var init_refresh = __esm({
108462
109114
  const requested = requestedTypes === null || requestedTypes.has(type);
108463
109115
  const failureInfo = failuresByType.get(type);
108464
109116
  const errored = failureInfo !== void 0 && failureInfo.count > 0;
108465
- const retrieveConfirmed = confirmedTypes !== null && confirmedTypes.has(type) && !errored;
109117
+ const foldErased = FOLD_ERASED_COVERAGE_TYPES.has(type);
109118
+ const retrieveConfirmed = confirmedTypes !== null && confirmedTypes.has(type) && !errored && !foldErased;
108466
109119
  entries.push({
108467
109120
  type,
108468
109121
  requested,
@@ -108470,6 +109123,7 @@ var init_refresh = __esm({
108470
109123
  errored,
108471
109124
  ...errored ? { errorReason: failureInfo.sampleReason } : {},
108472
109125
  neverModeled: false,
109126
+ ...foldErased ? { pending: true } : {},
108473
109127
  ...retrieveConfirmed ? { retrieveConfirmed: true } : {}
108474
109128
  });
108475
109129
  }
@@ -108493,21 +109147,44 @@ var init_refresh = __esm({
108493
109147
  decorateReportsCapCoverage = (entries, stats) => {
108494
109148
  if (stats === void 0)
108495
109149
  return entries;
108496
- const capped = /* @__PURE__ */ new Map();
108497
- if (stats.reports.total > stats.reports.retrieved)
108498
- capped.set("Report", stats.reports);
108499
- if (stats.dashboards.total > stats.dashboards.retrieved)
108500
- capped.set("Dashboard", stats.dashboards);
108501
- if (capped.size === 0)
108502
- return entries;
108503
- const out = entries.map((entry) => capped.has(entry.type) ? { ...entry, requested: true, pending: true } : entry);
108504
- for (const [type, c] of capped) {
108505
- if (!out.some((entry) => entry.type === type)) {
108506
- out.push({ type, requested: true, retrieved: c.retrieved, errored: false, neverModeled: false, pending: true });
109150
+ const landed = /* @__PURE__ */ new Map([
109151
+ ["Report", stats.reports],
109152
+ ["Dashboard", stats.dashboards]
109153
+ ]);
109154
+ const proven = (c) => c.retrieved > 0 && c.total <= c.retrieved;
109155
+ const decorated = (c) => ({
109156
+ requested: true,
109157
+ // This is the ONLY place a fold-erased row gets a truthful `retrieved`:
109158
+ // the files the retrieve actually landed on disk. Overwriting the
109159
+ // structurally-zero node count is the point (FOLD_ERASED_COVERAGE_TYPES).
109160
+ retrieved: c.retrieved,
109161
+ ...proven(c) ? { retrieveConfirmed: true } : { pending: true }
109162
+ });
109163
+ const out = entries.map((entry) => {
109164
+ const c = landed.get(entry.type);
109165
+ if (c === void 0 || entry.neverModeled)
109166
+ return entry;
109167
+ const { pending: _wasPending, retrieveConfirmed: _wasConfirmed, ...rest } = entry;
109168
+ return { ...rest, ...decorated(c) };
109169
+ });
109170
+ for (const [type, c] of landed) {
109171
+ if (!out.some((entry) => entry.type === type && !entry.neverModeled)) {
109172
+ out.push({ type, errored: false, neverModeled: false, ...decorated(c) });
108507
109173
  }
108508
109174
  }
108509
109175
  return [...out].sort((a, b) => a.type < b.type ? -1 : a.type > b.type ? 1 : 0);
108510
109176
  };
109177
+ decorateReportPullCoverage = (entries, disclosure) => {
109178
+ if (disclosure === void 0)
109179
+ return entries;
109180
+ const reason = `${REPORT_PULL_DISCLOSURE} (${disclosure.mode} pull, ${disclosure.outcome} at ${disclosure.attemptedAt}): ${disclosure.error}`;
109181
+ return entries.map((entry) => {
109182
+ if (!FOLD_ERASED_COVERAGE_TYPES.has(entry.type) || entry.neverModeled)
109183
+ return entry;
109184
+ const { retrieveConfirmed: _dropped, ...rest } = entry;
109185
+ return { ...rest, requested: true, pending: true, errored: true, errorReason: reason };
109186
+ });
109187
+ };
108511
109188
  countGraphTotals = async (store) => {
108512
109189
  try {
108513
109190
  const components = {};
@@ -108649,14 +109326,18 @@ var init_refresh = __esm({
108649
109326
  return failed(started, `countGraphTotals: ${counted.error}`, walked.failures, EMPTY_COUNTS, walked.skippedDirectories);
108650
109327
  }
108651
109328
  return {
108652
- status: walked.failures.length === 0 && args.retrieveFailures.length === 0 ? "success" : "partial",
109329
+ // A report-pull failure forces `partial` for the same reason a
109330
+ // profile-grant disclosure does: the vault built, but a coverage axis it
109331
+ // attempted is unproven. `success` here is what let the failure vanish.
109332
+ status: walked.failures.length === 0 && args.retrieveFailures.length === 0 && args.reportPull === void 0 ? "success" : "partial",
108653
109333
  counts: counted.value,
108654
109334
  skippedDirectories: walked.skippedDirectories,
108655
109335
  errors: walked.failures,
108656
109336
  durationMs: Date.now() - started,
108657
109337
  ...args.retrieveFailures.length > 0 ? { retrieveFailures: args.retrieveFailures } : {},
108658
109338
  ...toolingApiSummary !== void 0 ? { toolingApi: toolingApiSummary } : {},
108659
- ...args.reportsCapStats !== void 0 ? { reportsCap: args.reportsCapStats } : {}
109339
+ ...args.reportsCapStats !== void 0 ? { reportsCap: args.reportsCapStats } : {},
109340
+ ...args.reportPull !== void 0 ? { reportPull: args.reportPull } : {}
108660
109341
  };
108661
109342
  }
108662
109343
  const midBuild = opts.stagedMarker !== void 0;
@@ -108696,7 +109377,7 @@ var init_refresh = __esm({
108696
109377
  profileGrantStats = computeProfileGrantStats(walked.results);
108697
109378
  profileGrantDisclosure = assessProfileGrantIntegrity(profileGrantStats, previousManifest?.edges["grantedBy"] ?? null, counts.edges["grantedBy"] ?? 0);
108698
109379
  }
108699
- const coverage = decorateProfileGrantCoverage(decorateReportsCapCoverage(decoratePendingCoverage(buildCoverageEntries2(counts, walked.skippedDirectories, requestedTypes, paths.source, walked.failures, confirmedTypes), opts.stagedMarker), args.reportsCapStats), profileGrantDisclosure);
109380
+ const coverage = decorateProfileGrantCoverage(decoratePendingCoverage(decorateReportPullCoverage(decorateReportsCapCoverage(buildCoverageEntries2(counts, walked.skippedDirectories, requestedTypes, paths.source, walked.failures, confirmedTypes), args.reportsCapStats), args.reportPull), opts.stagedMarker), profileGrantDisclosure);
108700
109381
  let phantomSummary;
108701
109382
  if (!midBuild && opts.stagedMarker === void 0) {
108702
109383
  try {
@@ -108722,6 +109403,7 @@ var init_refresh = __esm({
108722
109403
  ...opts.stagedMarker !== void 0 ? { staged: opts.stagedMarker } : {},
108723
109404
  ...args.apexAstStats !== void 0 ? { apexAst: args.apexAstStats } : {},
108724
109405
  ...args.reportsCapStats !== void 0 ? { reportsCap: args.reportsCapStats } : {},
109406
+ ...args.reportPull !== void 0 ? { reportPull: args.reportPull } : {},
108725
109407
  ...profileGrantDisclosure !== null && profileGrantStats !== null ? {
108726
109408
  profileGrantIntegrity: {
108727
109409
  degraded: true,
@@ -108847,7 +109529,11 @@ var init_refresh = __esm({
108847
109529
  return {
108848
109530
  // A profile-grant integrity disclosure forces `partial`: the vault built,
108849
109531
  // but its permission graph is untrustworthy — never report clean success.
108850
- status: walked.failures.length === 0 && args.retrieveFailures.length === 0 && profileGrantDisclosure === null ? "success" : "partial",
109532
+ // A report-pull failure forces it for the same reason: the pull was
109533
+ // attempted and did not deliver, so Report/Dashboard coverage is unproven.
109534
+ // (`sfi refresh` exits non-zero on non-success, which is the point — the
109535
+ // shipped defect was a clean exit over a pull that had errored.)
109536
+ status: walked.failures.length === 0 && args.retrieveFailures.length === 0 && profileGrantDisclosure === null && args.reportPull === void 0 ? "success" : "partial",
108851
109537
  counts,
108852
109538
  skippedDirectories: walked.skippedDirectories,
108853
109539
  errors: walked.failures,
@@ -108858,6 +109544,7 @@ var init_refresh = __esm({
108858
109544
  ...pulse !== void 0 ? { pulse } : {},
108859
109545
  ...toolingApiSummary !== void 0 ? { toolingApi: toolingApiSummary } : {},
108860
109546
  ...args.reportsCapStats !== void 0 ? { reportsCap: args.reportsCapStats } : {},
109547
+ ...args.reportPull !== void 0 ? { reportPull: args.reportPull } : {},
108861
109548
  ...auditTrailSummary !== void 0 ? { auditTrail: auditTrailSummary } : {}
108862
109549
  };
108863
109550
  };
@@ -109554,6 +110241,7 @@ ${card.body}
109554
110241
  const succeeded = [];
109555
110242
  const failures = [];
109556
110243
  let deletedCount = 0;
110244
+ const reconcileRefusals = [];
109557
110245
  const attempt = async (types) => {
109558
110246
  if (types.length === 0)
109559
110247
  return;
@@ -109561,6 +110249,9 @@ ${card.body}
109561
110249
  if (result.ok) {
109562
110250
  succeeded.push(...types);
109563
110251
  deletedCount += result.value.deletedCount;
110252
+ if (result.value.reconcileRefused === true) {
110253
+ reconcileRefusals.push(result.value.reconcileRefusalReason ?? `reconcile refused for ${types.join(", ")} (suspected layout mismatch)`);
110254
+ }
109564
110255
  return;
109565
110256
  }
109566
110257
  if (toAtomicUnits(types, atomicGroup).length <= 1 || classifyRetrieveError(result.error) === "global") {
@@ -109573,7 +110264,7 @@ ${card.body}
109573
110264
  await attempt(right);
109574
110265
  };
109575
110266
  await attempt(allTypes);
109576
- return { succeeded, deletedCount, failures };
110267
+ return { succeeded, deletedCount, failures, reconcileRefusals };
109577
110268
  };
109578
110269
  retrieveTypeBatch = async (targetOrg, sourceDir, types, orgObjects) => {
109579
110270
  const stamp = `${Date.now()}-${types.length}-${types[0] ?? "batch"}`;
@@ -109588,7 +110279,13 @@ ${card.body}
109588
110279
  await runSf(["project", "retrieve", "start", "--manifest", manifestPath, "--target-org", targetOrg], { maxBuffer: SF_MAX_BUFFER, cwd: projectDir, timeout: SF_RETRIEVE_TIMEOUT_MS });
109589
110280
  const reconcile2 = await reconcileSourceDeletions(sourceDir, pkgDir, new Set(types));
109590
110281
  await syncAuthoritativeRetrieveIntoSource(sourceDir, pkgDir);
109591
- return ok({ deletedCount: reconcile2.deletedCount });
110282
+ return ok({
110283
+ deletedCount: reconcile2.deletedCount,
110284
+ ...reconcile2.refused === true ? {
110285
+ reconcileRefused: true,
110286
+ ...reconcile2.refusalReason !== void 0 ? { reconcileRefusalReason: reconcile2.refusalReason } : {}
110287
+ } : {}
110288
+ });
109592
110289
  } catch (cause) {
109593
110290
  return err(cause instanceof Error ? cause.message : String(cause));
109594
110291
  } finally {
@@ -109646,6 +110343,7 @@ ${card.body}
109646
110343
  await writeFile14(join43(projectDir, "sfdx-project.json"), `{"packageDirectories":[{"path":"force-app","default":true}],"sourceApiVersion":"${SF_API_VERSION3}"}
109647
110344
  `, "utf8");
109648
110345
  await writeFile14(manifestPath, args.manifestXml, "utf8");
110346
+ const pkgDir = join43(projectDir, "force-app");
109649
110347
  try {
109650
110348
  await runSfFn([
109651
110349
  "project",
@@ -109654,10 +110352,9 @@ ${card.body}
109654
110352
  "--manifest",
109655
110353
  manifestPath,
109656
110354
  "--target-org",
109657
- args.targetOrg,
109658
- "--output-dir",
109659
- args.outputDir
110355
+ args.targetOrg
109660
110356
  ], { maxBuffer: SF_MAX_BUFFER, cwd: projectDir, timeout: SF_RETRIEVE_TIMEOUT_MS });
110357
+ await syncAuthoritativeRetrieveIntoSource(args.outputDir, pkgDir);
109661
110358
  } finally {
109662
110359
  await rm5(manifestPath, { force: true }).catch(() => {
109663
110360
  });
@@ -109728,6 +110425,45 @@ ${typesXml}
109728
110425
  `;
109729
110426
  return { membersByType, reports, dashboards, manifestXml };
109730
110427
  };
110428
+ REPORT_RETRIEVE_BATCH_SIZE = 500;
110429
+ folderedReportPackageXml = (type, members) => `<?xml version="1.0" encoding="UTF-8"?>
110430
+ <Package xmlns="http://soap.sforce.com/2006/04/metadata">
110431
+ <types>
110432
+ ${members.map((m) => ` <members>${m}</members>`).join("\n")}
110433
+ <name>${type}</name>
110434
+ </types>
110435
+ <version>${SF_API_VERSION3}</version>
110436
+ </Package>
110437
+ `;
110438
+ chunkFolderedReportManifest = (membersByType, size = REPORT_RETRIEVE_BATCH_SIZE) => {
110439
+ const batchSize = Number.isFinite(size) && size > 0 ? Math.floor(size) : REPORT_RETRIEVE_BATCH_SIZE;
110440
+ const out = [];
110441
+ for (const type of ["Report", "Dashboard"]) {
110442
+ const members = membersByType[type];
110443
+ for (let i = 0; i < members.length; i += batchSize) {
110444
+ const slice = members.slice(i, i + batchSize);
110445
+ out.push({ type, members: slice, manifestXml: folderedReportPackageXml(type, slice) });
110446
+ }
110447
+ }
110448
+ return out;
110449
+ };
110450
+ retrieveFolderedReportBatches = async (args, runSfFn = runSf) => {
110451
+ const batches = chunkFolderedReportManifest(args.membersByType);
110452
+ const batchErrors = [];
110453
+ for (const [index, batch] of batches.entries()) {
110454
+ try {
110455
+ await retrieveAdditiveManifest({
110456
+ targetOrg: args.targetOrg,
110457
+ outputDir: args.outputDir,
110458
+ manifestXml: batch.manifestXml,
110459
+ tempLabel: `${args.tempLabel}-${index + 1}`
110460
+ }, runSfFn);
110461
+ } catch (cause) {
110462
+ batchErrors.push(`${batch.type} batch ${index + 1}/${batches.length} (${batch.members.length} member(s)) failed: ${cause instanceof Error ? cause.message : String(cause)}`);
110463
+ }
110464
+ }
110465
+ return { batches: batches.length, batchErrors };
110466
+ };
109731
110467
  REPORT_META_SUFFIXES = {
109732
110468
  Report: ".report-meta.xml",
109733
110469
  Dashboard: ".dashboard-meta.xml"
@@ -109794,19 +110530,18 @@ ${typesXml}
109794
110530
  return [];
109795
110531
  }
109796
110532
  };
109797
- const { reports, dashboards, manifestXml } = buildFolderedReportManifest({
110533
+ const { reports, dashboards, manifestXml, membersByType } = buildFolderedReportManifest({
109798
110534
  folders,
109799
110535
  reports: await queryRecords("Report"),
109800
110536
  dashboards: await queryRecords("Dashboard")
109801
110537
  });
109802
110538
  if (manifestXml === null)
109803
- return ok({ reports: 0, dashboards: 0 });
109804
- try {
109805
- await retrieveAdditiveManifest({ targetOrg, outputDir: sourceDir, manifestXml, tempLabel: "reports" }, runSfFn);
109806
- return ok({ reports, dashboards });
109807
- } catch (cause) {
109808
- return err(`report/dashboard retrieve failed: ${cause instanceof Error ? cause.message : String(cause)}`);
110539
+ return ok({ reports: 0, dashboards: 0, batchErrors: [] });
110540
+ const outcome = await retrieveFolderedReportBatches({ targetOrg, outputDir: sourceDir, tempLabel: "reports", membersByType }, runSfFn);
110541
+ if (outcome.batches > 0 && outcome.batchErrors.length === outcome.batches) {
110542
+ return err(`report/dashboard retrieve failed: all ${outcome.batches} batch(es) errored \u2014 ${outcome.batchErrors[0] ?? ""}`);
109809
110543
  }
110544
+ return ok({ reports, dashboards, batchErrors: outcome.batchErrors });
109810
110545
  };
109811
110546
  reportsCap = () => {
109812
110547
  const raw = Number(process.env["SFI_REPORTS_CAP"]);
@@ -109854,24 +110589,31 @@ ${typesXml}
109854
110589
  dashboards: await ranked("Dashboard", "LastViewedDate")
109855
110590
  });
109856
110591
  if (manifestXml === null) {
109857
- return ok({ reports: 0, dashboards: 0, totals, landed: { reports: 0, dashboards: 0 }, missing: [] });
109858
- }
109859
- try {
109860
- await retrieveAdditiveManifest({ targetOrg, outputDir: sourceDir, manifestXml, tempLabel: "smart-reports" }, runSfFn);
109861
- const fidelity = countLandedReportMembers(membersByType, await collectReportMetaFiles(sourceDir));
109862
110592
  return ok({
109863
- reports,
109864
- dashboards,
110593
+ reports: 0,
110594
+ dashboards: 0,
109865
110595
  totals,
109866
- landed: { reports: fidelity.Report.landed, dashboards: fidelity.Dashboard.landed },
109867
- missing: [
109868
- ...fidelity.Report.missing.map((m) => `Report:${m}`),
109869
- ...fidelity.Dashboard.missing.map((m) => `Dashboard:${m}`)
109870
- ]
110596
+ landed: { reports: 0, dashboards: 0 },
110597
+ missing: [],
110598
+ batchErrors: []
109871
110599
  });
109872
- } catch (cause) {
109873
- return err(`smart report retrieve failed: ${cause instanceof Error ? cause.message : String(cause)}`);
109874
110600
  }
110601
+ const outcome = await retrieveFolderedReportBatches({ targetOrg, outputDir: sourceDir, tempLabel: "smart-reports", membersByType }, runSfFn);
110602
+ if (outcome.batches > 0 && outcome.batchErrors.length === outcome.batches) {
110603
+ return err(`smart report retrieve failed: all ${outcome.batches} batch(es) errored \u2014 ${outcome.batchErrors[0] ?? ""}`);
110604
+ }
110605
+ const fidelity = countLandedReportMembers(membersByType, await collectReportMetaFiles(sourceDir));
110606
+ return ok({
110607
+ reports,
110608
+ dashboards,
110609
+ totals,
110610
+ landed: { reports: fidelity.Report.landed, dashboards: fidelity.Dashboard.landed },
110611
+ missing: [
110612
+ ...fidelity.Report.missing.map((m) => `Report:${m}`),
110613
+ ...fidelity.Dashboard.missing.map((m) => `Dashboard:${m}`)
110614
+ ],
110615
+ batchErrors: outcome.batchErrors
110616
+ });
109875
110617
  };
109876
110618
  formatFailure = (failure) => ` ${failure.path}: ${failure.error.kind} \u2014 ${failure.error.message}`;
109877
110619
  formatBlock = (label, counts) => {
@@ -109942,6 +110684,9 @@ ${typesXml}
109942
110684
  if (result.reportsCap !== void 0) {
109943
110685
  lines.push("", ...formatReportsCapSummary(result.reportsCap));
109944
110686
  }
110687
+ if (result.reportPull !== void 0) {
110688
+ lines.push("", `WARNING \u2014 ${REPORT_PULL_DISCLOSURE}.`, ` ${result.reportPull.mode} pull ${result.reportPull.outcome} at ${result.reportPull.attemptedAt}: ${result.reportPull.error}`, " Report/Dashboard coverage rows are marked errored + pending, and this run is recorded on the manifest as `reportPull`.", ' A "0 reports" answer from this vault means NOT CHECKED, not none. Re-run `sfi refresh` (or `--with-reports`) to prove coverage.');
110689
+ }
109945
110690
  const skippedWarning = formatSkippedWarning(result.skippedDirectories);
109946
110691
  if (skippedWarning !== null) {
109947
110692
  lines.push("", ...skippedWarning);
@@ -110155,7 +110900,7 @@ ${typesXml}
110155
110900
  `;
110156
110901
  };
110157
110902
  registerRefreshCommand = (program) => {
110158
- program.command("refresh").description("Refresh the vault from a live Salesforce org").option("--target-org <alias>", "Salesforce org alias to retrieve from (overrides config.json)").option("--no-pull", "Skip `sf project retrieve` and use the existing source tree").option("--types <list>", "Comma-separated metadata types to restrict the refresh to").option("--with-data-shape", "After the refresh, capture a small budgeted set of record-DATA observations (approximate per-object record counts + recent-sample field fill rates) into the graph facts table. OPT-IN twice over: requires live consent for the org (sfi.live_consent / SFI_LIVE_PLANE_ENABLED) \u2014 without it the capture skips honestly and the refresh stays offline. Read-only; at most SFI_DATA_SHAPE_BUDGET (default 60) API calls.").option("--with-tooling-api", "After the offline refresh completes, run the v1.7 Tooling API enrichment pass to hydrate `lastModifiedDate` / `lastModifiedBy` / `apiVersion` on enrichable nodes, and now also confirms declared dependencies (stamps `confirmedByApi` on matching edges and appends `dependsOnFromApi` edges from MetadataComponentDependency). Requires `sf` CLI installed and the target org alias authenticated.").option("--with-audit-trail", "During refresh, query SetupAuditTrail and append new rows (deduped by Id) to meta/setup-audit-trail.jsonl so sfi.component_change_attribution can answer who-changed-this offline. Opt-in \u2014 touches the org and adds latency. First run pulls LAST_N_DAYS:180; later runs are incremental. Additive-only: persisted history survives after Salesforce drops rows from the live 180-day window.").option("--incremental", "Reuse the previous refresh's per-file extraction cache (`meta/extract-cache.json`): source files whose mtime + size are unchanged skip re-extraction. The graph is still rebuilt in full, so results are identical to a cold refresh \u2014 only faster. The cache is invalidated automatically on a version upgrade.").option("--incremental-graph", "Re-import ONLY the changed nodes/edges into the graph (a transactional change-set diff against the current graph) instead of rebuilding it in full, when a prior non-empty graph exists. Provably byte-identical to a cold rebuild; falls back to a full rebuild on an empty/largely-changed graph or any apply error. Independent of `--incremental` (the parse cache); combine both for the largest win. Off by default.").option("--components <list>", "Demand-retrieve: comma-separated component ids (e.g. `CustomObject:Foo__c`) to pull on demand. Only AUTOMATION-CRITICAL phantoms are retrieved; grant-only / managed / standard / blindspot ids are refused with the reason. Generalizes the automatic B29 expansion to a user-triggered pull.").option("--no-reports", "Skip the report/dashboard pull entirely. DEFAULT (neither flag): pull the top SFI_REPORTS_CAP (500) reports+dashboards ranked by actual usage (LastRunDate / LastViewedDate, fallback LastModifiedDate) and fold their field references onto fields; when the org holds more than the cap, Report/Dashboard coverage reads `pending` so absence claims stay qualified.").option("--with-reports", "Also pull folder-based Report / Dashboard metadata and fold their field usage onto the referenced fields, so a field used only in a report column / dashboard component stops reading as unused. Off by default \u2014 slow on large orgs (enumerates folders + pulls every report/dashboard).").option("--staged", "Build the vault in tiers for fast time-to-first-insight: T0 skeleton (read-only COUNT queries, no retrieve) \u2192 T1 priority types (objects/fields/automation/Apex/permissions; Markdown render deferred) \u2192 T2 full refresh (transactional side-build \u2014 a mid-tier failure leaves the prior state servable) \u2192 optional T3 with --with-reports. Mid-build, health_check reports degraded (building tier i/n) and queued types read as pending coverage, so absence claims stay qualified. Resumable: a re-run skips completed tiers. The final tier is a plain monolithic refresh, so the end state is identical to a non-staged run.").option("--no-apex-ast", "Skip the parser-grade Apex pass (ON by default): every class/trigger is re-parsed with the vendored ANTLR grammar adding `confidence: 'parsed'`, `source: 'apex-ast'` readsFrom/writesTo/callsApex edges \u2014 including SOQL field-level reads and constant-string Database.query literals. Exact-duplicate heuristic scanner edges are dropped (no double-counting); scanner-only edges are kept. A file that fails to parse falls back to scanner-only (counted in the manifest's apexAst block).").option("--drain-demand-queue", "Drain the phantom demand queue (`meta/demand-queue.jsonl`): retrieve every QUEUED automation-critical component that MCP consumers hit as phantoms (recorded by sfi.get_component), through the same gate as --components \u2014 grant-only / managed / standard / blindspot ids are refused with the reason. Dedup and idempotency are structural: N hits on one id drain once, and a re-drain of an already-drained id is a no-op. Drained entries are marked in the queue with their outcome.").action(async (flags) => {
110903
+ program.command("refresh").description("Refresh the vault from a live Salesforce org").option("--target-org <alias>", "Salesforce org alias to retrieve from (overrides config.json)").option("--no-pull", "Skip `sf project retrieve` and use the existing source tree").option("--types <list>", "Comma-separated metadata types to restrict the refresh to").option("--with-data-shape", "After the refresh, capture a small budgeted set of record-DATA observations (approximate per-object record counts + recent-sample field fill rates) into the graph facts table. OPT-IN twice over: requires live consent for the org (sfi.live_consent / SFI_LIVE_PLANE_ENABLED) \u2014 without it the capture skips honestly and the refresh stays offline. Read-only; at most SFI_DATA_SHAPE_BUDGET (default 60) API calls.").option("--with-tooling-api", "After the offline refresh completes, run the v1.7 Tooling API enrichment pass to hydrate `lastModifiedDate` / `lastModifiedBy` / `apiVersion` on enrichable nodes, and now also confirms declared dependencies (stamps `confirmedByApi` on matching edges and appends `dependsOnFromApi` edges from MetadataComponentDependency). Requires `sf` CLI installed and the target org alias authenticated.").option("--with-audit-trail", "During refresh, query SetupAuditTrail and append new rows (deduped by Id) to meta/setup-audit-trail.jsonl so sfi.component_change_attribution can answer who-changed-this offline. Opt-in \u2014 touches the org and adds latency. First run pulls LAST_N_DAYS:180; later runs are incremental. Additive-only: persisted history survives after Salesforce drops rows from the live 180-day window.").option("--incremental", "Reuse the previous refresh's per-file extraction cache (`meta/extract-cache.json`): source files whose mtime + size are unchanged skip re-extraction. The graph is still rebuilt in full, so results are identical to a cold refresh \u2014 only faster. The cache is invalidated automatically on a version upgrade.").option("--incremental-graph", "Re-import ONLY the changed nodes/edges into the graph (a transactional change-set diff against the current graph) instead of rebuilding it in full, when a prior non-empty graph exists. Provably byte-identical to a cold rebuild; falls back to a full rebuild on an empty/largely-changed graph or any apply error. Independent of `--incremental` (the parse cache); combine both for the largest win. Off by default.").option("--components <list>", "Demand-retrieve: comma-separated component ids (e.g. `CustomObject:Foo__c`) to pull on demand. Only AUTOMATION-CRITICAL phantoms are retrieved; grant-only / managed / standard / blindspot ids are refused with the reason. Generalizes the automatic B29 expansion to a user-triggered pull.").option("--no-reports", "Skip the report/dashboard pull entirely. DEFAULT (neither flag): pull the top SFI_REPORTS_CAP (500) reports+dashboards ranked by actual usage (LastRunDate / LastViewedDate, fallback LastModifiedDate) and fold their field references onto fields; when the org holds more than the cap, Report/Dashboard coverage reads `pending` so absence claims stay qualified. Report/Dashboard nodes are folded onto fields and never persisted, so their coverage row can only ever be proven by the pull itself \u2014 it reads `pending` (not checked), never a confirmed zero. A pull that errors is non-fatal but NOT silent: it is recorded on the manifest as `reportPull`, marks those rows errored, and exits non-zero.").option("--with-reports", "Also pull folder-based Report / Dashboard metadata and fold their field usage onto the referenced fields, so a field used only in a report column / dashboard component stops reading as unused. Off by default \u2014 slow on large orgs (enumerates folders + pulls every report/dashboard).").option("--staged", "Build the vault in tiers for fast time-to-first-insight: T0 skeleton (read-only COUNT queries, no retrieve) \u2192 T1 priority types (objects/fields/automation/Apex/permissions; Markdown render deferred) \u2192 T2 full refresh (transactional side-build \u2014 a mid-tier failure leaves the prior state servable) \u2192 optional T3 with --with-reports. Mid-build, health_check reports degraded (building tier i/n) and queued types read as pending coverage, so absence claims stay qualified. Resumable: a re-run skips completed tiers. The final tier is a plain monolithic refresh, so the end state is identical to a non-staged run.").option("--no-apex-ast", "Skip the parser-grade Apex pass (ON by default): every class/trigger is re-parsed with the vendored ANTLR grammar adding `confidence: 'parsed'`, `source: 'apex-ast'` readsFrom/writesTo/callsApex edges \u2014 including SOQL field-level reads and constant-string Database.query literals. Exact-duplicate heuristic scanner edges are dropped (no double-counting); scanner-only edges are kept. A file that fails to parse falls back to scanner-only (counted in the manifest's apexAst block).").option("--drain-demand-queue", "Drain the phantom demand queue (`meta/demand-queue.jsonl`): retrieve every QUEUED automation-critical component that MCP consumers hit as phantoms (recorded by sfi.get_component), through the same gate as --components \u2014 grant-only / managed / standard / blindspot ids are refused with the reason. Dedup and idempotency are structural: N hits on one id drain once, and a re-drain of an already-drained id is a no-op. Drained entries are marked in the queue with their outcome.").action(async (flags) => {
110159
110904
  if (flags.drainDemandQueue === true) {
110160
110905
  const configResult = await loadVaultConfig(process.cwd());
110161
110906
  if (!configResult.ok) {
@@ -111357,7 +112102,7 @@ var makeShutdownOnce = (ctx) => {
111357
112102
 
111358
112103
  // dist/src/commands/demo.js
111359
112104
  init_refresh();
111360
- var buildVersion = () => true ? "0.2.4" : "dev";
112105
+ var buildVersion = () => true ? "0.2.5" : "dev";
111361
112106
  var SHUTDOWN_SIGNALS2 = ["SIGINT", "SIGTERM"];
111362
112107
  var resolveDemoSource = () => {
111363
112108
  let dir = dirname25(fileURLToPath5(import.meta.url));
@@ -112812,7 +113557,7 @@ init_vault_git();
112812
113557
  init_watch();
112813
113558
  var readVersion = () => {
112814
113559
  if (true)
112815
- return "0.2.4";
113560
+ return "0.2.5";
112816
113561
  const pkgUrl = new URL("../../package.json", import.meta.url);
112817
113562
  const raw = readFileSync10(fileURLToPath6(pkgUrl), "utf8");
112818
113563
  const parsed = JSON.parse(raw);