Repository navigation
Releases
v0.8.0
0.8.0: trust the verdict
No new tools. Every change makes an answer your assistant already gives about your org more correct or more honest.
Before this release we tested ourselves. Plain Claude Code reading a real org's raw metadata vs the same session with sf-intelligence attached, the same eight admin questions. Plain Claude got 8/8; with the product, 6/8. The two misses were ours. After this release: 8/8 and 8/8, no fabrications in either arm, and the product answered in a median 46 s against 61 s, with cited component ids and quantified coverage ("499 of 14,018 reports retrieved") that grep cannot produce. The full write-up is in the release report.
What you will notice
- What happens when I save this record? is right on orgs that run a trigger framework: framework-registered handler classes appear as conditional steps, and each flow step shows its real entry criteria instead of its first inner decision.
- Can I safely delete this field? stops inventing confidence. Lightning page visibility rules and placements, report-type and field-set columns, dynamically loaded classes and Change Data Capture subscriptions count as use. A custom field is never "definitely dead" on heuristic evidence; usage rows carry each referrer's status, so an obsolete flow never reads as a live writer.
- Who can see these records? shows the sharing rule's actual filter.
- Caveats print only when the check could not run; summary sentences are computed from their rows.
- The regex router is gone. Your assistant chooses the tool; the semantic funnel offers a shortlist, tolerates a one-letter typo and finds the deletion and custom-metadata tools.
- First-run fixes from a stranger's walk-through, including: setup mode no longer sends your list of authenticated org aliases to the model.
Upgrade
npm install -g sf-intelligence@0.8.0
sfi refreshRefresh once: the new references are read at refresh time. If a script read route_question's route.intent, read toolCandidates instead. Details in the upgrading guide and the release post.
Full changelog
Changed
fast-xml-parser4.5 → 5.11, clearing the moderate advisory (GHSA-gh4j-gqv2-49f6) thatnpm installreported on a fresh install.- The "layout placement" caveat is gone from the referrer-coverage table rather than gated on an edge count: a vault that did not retrieve Layouts is already named by the coverage caveat (Layout is a required family).
get_impactrows are untouched Nodes (theirproperties.status/activeis the raw value), and a CustomField root on a vault with no recorded builder version now readscomplete: truebecause every field referrer kind is an edge at the current tier.object_360,safe_to_delete_field's referrer run state andautomation_collisionsread activation through the same helper; an ApexTrigger whose status isDeletedno longer counts as a live firer.
Fixed
- Apex that writes a field through a named-argument constructor (
new Obj__c(Id = recId, Field__c = v)followed byinsert,updateorDatabase.update, alone or inside a list literal) now mints a parsedwritesToedge likerecord.Field__c = v;always did. Before, a class whose writes were all constructor-shaped appeared in the graph as a reader only, so "which fields does automation write" and "who writes this field" missed it. sfi.list_analysesfinds a tool from any of the words in a question. Thequeryfilter required every word to appear in a name or summary, so "fields written by automation" and "external endpoints callouts" returned zero analyses while "callout" alone foundsfi.endpoint_catalog, and the host read Apex by hand instead. The query is now tokenized with the funnel's own tokenizer (one stop-word list, one plural stem) and analyses rank by how many terms they match (a match in the name breaks ties, an exact tool name goes to the top); the top 8 come back, each row with itsmatchedTerms. A query that matches nothing, or is only stop words, returns an empty list with the catalog guidance, never the whole catalog.- Change Data Capture subscriptions are a real edge.
apex-edges.tspromised asubscribesToChangemodeling path for CDC subscribers and never emitted it, sosfi.cdc_subscriberssaw a CDC trigger only through its object-lifecycletriggersOnedge and never saw anEventBus.subscribeto a Change Event at all, whilesfi.event_subscribersdescribed the same registration as recognized. NewEdgeTypesubscribesToChange(23 → 24): atrigger X on {X}ChangeEventheader mints itdeclared, andEventBus.subscribe('<X>ChangeEvent' | '/data/<X>ChangeEvent', ...)mints itheuristic, both into the same syntheticCustomObject:{X}ChangeEventid every CDC reader already scans. A runtime-built channel mints no edge and is counted on the class or trigger asproperties.unresolvedSubscriptions.sfi.cdc_subscribersandsfi.event_topologyread the new edge first and list a subscriber once even when it also carriestriggersOn; both tools' caveats andsfi.event_subscribers' now say only what is still unseen. TheisChangeEventApiNamerule moved to@sf-intelligence/contractsso the extractors, the graph and the tools share one copy. what_happens_on_saveandorder_of_executionno longer print a fixed "not listed: approval-process field updates, time-triggered workflow updates, Process Builder processes (absent from this save order)" on every answer.reentry.notModelednow counts each of those in the vault first and names it only when there is at least one, with the count; the closing sentence states this tool's own analysis limits so a host cannot read them as a snapshot gap.permission_risk_reportreads the org security toggles that change what a god-mode grant means (SecuritySettings:defaultlogin-as-any-user, login-access and password policy;SessionSettings:defaulttimeout, IP lock, MFA) and carries the relevant ones as evidence rows on every Modify All Data / View All Data / Manage Users finding, with every toggle read listed once insecurityContext. A caveat about org settings is printed only when neither settings node is in the vault.sfi.unused_componentsjudges an ApexClass with zero inbound edges through the same liveness predicate assfi.find_dead_code(entryKindsForplus the static type-name grep, now one exportedstaticallyReferencedApexinapex-reachability.ts). A REST resource, a scheduled / batch / queueable job, a framework subclass, a Callable or namespaced-interface implementation registered in a Custom Metadata cell, or a class named only by a static type reference is listed under a newuncertainarray with itsreasonsinstead of as unused, andbyType.ApexClass(the counttech_debt_scorereads) counts only the classes the predicate clears. On a reference org 14 of 23 "unused" classes wereuncertainin the sibling tool; now 0 diverge.- Resolvable dynamic Apex is an edge, not a caveat.
Type.forName('Demo_Handler')named its class as plainly asnew Demo_Handler()and still minted nothing, so a class reached only that way readuncertain(or dead) insfi.find_dead_codeandsfi.unused_components, and the caveat said dynamic dispatch was invisible. A string-literal operand (one-argument, or the two-argument same-namespace form) now mints a heuristicreferencesedge withmechanism: Type.forNamefrom the comment-stripped, string-kept source, on the AST path and the scanner-fallback path alike. A dispatcher whose operand is a variable fed from a Custom Metadata type it reads (Type.forName(row.Handler__c)) is joined at refresh to every class the type's records name through the cell resolution 0.5.0 already had, one edge per class (via: CustomMetadataRecord). Any other variable operand is never guessed: it is counted on the dispatching class asproperties.unresolvedDynamicDispatch.find_dead_codereads the resolved edge before the unproven-registration shape, so a framework subclass orCallabledispatched this way from production is "in use via resolved dynamic dispatch" rather than "zero inbound is expected"; a dispatch only from a test class still counts for nothing. Both tools' caveats,docs/POSITIONING.mdand the reachability skill now say what is resolved and what is not. downstream_effectsnow reports acallouteffect for everycallout:{NamedCredential}reference the Apex extractor already minted, with the credential as the target and asummary.calloutcount. Before, the tool said HTTP callouts were not recognized while the edges sat in the same vault. A callout to a literal or runtime-built URL still mints no edge and the disclosure still says so.endpoint_catalognow lists URL-shaped values stored in Custom Metadata and Custom Setting rows understoredEndpoints(record id, field, URL), read with the same readerlookup_recorduses, and reports how many records it read instoredEndpointsScan. Before, a caveat said the catalog did not scan those rows. The remaining caveat covers only rows written at runtime, which the vault does not hold.explain_fieldreads record values through that one shared reader too.- Lightning pages count as use. A FlexiPage visibility rule
{!Record.Field}now reads the field on the page's object (readsFrom,referenceKind: 'visibilityRule'),{!Record.Rel__r.Field}in a visibility rule or a dynamic-forms field item resolves through the relationship map at import time instead of minting aCustomField:Record.*id that names no node, and every custom LWC or Aura bundle a page places bycomponentNamegets a declaredreferencesedge (componentPlacement) onto the bundle node that e...
v0.7.0
No new tools and no envelope change. The second real-org defect from the October review is closed, a dangling reference from 0.6.0 is fixed, and the demo org shows what the extractors can see.
Added
- The demo org shows the three 0.6.0 reference families: a screen flow with
ObjectProvidedfields, a Lightning web component withlightning-record-formfield bindings, and a Project layout related list with Installation columns, each with a demo question whose answer cites it.
Changed
verify-doc-syncnow fails when a relative Markdown link in README, CONTRIBUTING, RELEASING, SECURITY,llms-install.md, ordocs/**points at a path that does not exist./sfi-field-auditnow checks first whether its two agents (salesforce-field-auditor,salesforce-field-refuter) are available in the session; without them it runs the per-field audit and the three refutation lenses inline and says so once. The plugin ships no agents, so this is the path plugin users get. Maintainer-only agents moved to.claude/agents/maintainer/.
Fixed
- Flow screen components now link to the Lightning web component they show. The
c:prefix on the component name was kept in the link target, so every screen-to-component reference pointed at an id no component had and the component listed no usages. The flow, LWC and Aura extractors now build bundle ids through one shared helper, so the two sides cannot drift apart again; quick actions that open a Lightning component go through the same helper, so ac:prefix there cannot mint a dangling link either. - OmniScript save-for-later URL patterns (
saveURLPatternson the script's top-level settings) now show up in field where-used: each pattern key that names a field on the object the script declares for saving gets awritesTolink to that field. If the script does not declare which object is saved, the pattern is listed on the OmniScript as unresolved instead of guessing from the key name.
Verification scope
Windows: partially verified. The hard-gated tier (core, tooling-api, renderers, parsers, patterns, vault, extractors) passed. Advisory tiers were NOT green: WIN-1=failure, WIN-2=failure — those suites did not report a pass on Windows for this build and were not allowed to block it. See CI run https://github.com/PranavNagrecha/Salesforce-Intelligence/actions/runs/39053974665.
CI run: https://github.com/PranavNagrecha/Salesforce-Intelligence/actions/runs/39053974665
v0.6.0
Changed
-
Scorecard in the gate. The build now measures the default tool count, the size of the tool list, the median answer size on the demo org, the where-used and dead-code counts on real orgs, the number of known test failures carried, and the number of "keep in sync" comments in the code; a change that makes any of them worse fails. 0.5.0 → 0.6.0: median demo answer 8,294 → 8,038 bytes; known test failures carried 4 → 0.
-
README and trust signals. The README opens with what the tool answers and one cited demo answer; every public surface says open source (MIT).
-
Docs for admins. Three new guides: upgrading, troubleshooting and a CLI reference; a decision record for the 16-tool default (ADR-010); the README is a third shorter and opens with what the tool answers; the shipped skills and commands no longer tell an assistant to call tools it cannot reach directly; internal version labels are gone from tool output, skills and docs.
Fixed
- LWC and Aura field usage in component markup —
lightning-input-field,lightning-output-field,lightning-record-*-formfield lists, Auraforce:recordData, andlightning:inputField/lightning:outputFieldnow produce graph edges when the object can be derived fromobject-api-name/sobjectType(or when the attribute namesObject.Fielddirectly). Usage and impact tools can see fields bound only in templates, not only in JavaScript imports. Unqualified bare field names with no object context are listed on the bundle asunresolvedFieldBindingsand do not invent aCustomFieldedge. coverage_reportandhealth_checknow derive the same not modeled type list fromsummarizeCoverage().notModeledTypes, fixing cross-tool honesty drift on real vaults (scorecard d/e).sfi.what_happens_on_saveandsfi.order_of_executionshare one disclosure and one trigger-bypass annotation, so both report the same "this trigger may be switched off" warning.sfi.tests_for_changeandsfi.test_coverage_for_methodshare one test-selection walk; direct coverers and trigger-mediated candidates stay in their separate tiers.- Flow extractor: Screen
<fields>withobjectFieldReferencenow emitreadsFrom/writesToedges to the boundCustomField;extensionNameon a screen component emits areferencesedge to the LWC or Aura bundle. Non-Apex<actionCalls>(emailAlert,quickAction,externalService,flow) emitreferencesedges toWorkflowAlert,QuickAction,ExternalService, orFlowtargets so dependency walks and usage tools see screen-only field bindings and action dependencies. - Classic page layout related-list columns now resolve through the same child-relationship map as Lightning dynamic related lists. When the relationship is known, each column becomes a
usedInLayoutedge on the child object's field; standard or unretrieved relationships are recorded on the layout node instead of minting a wrong-object field id. - Fix dispatch arg normalization so
objectApiNameis not renamed ontocomponentIdfor Apex-scoped tools such assfi.crud_fls_audit(regression a security-audit question in the regression bank). - Move the org-metadata untrusted-data disclosure from per-response
contentPolicyobjects to MCPSERVER_INSTRUCTIONS; success envelopes now stampcontentPolicy: "org-metadata"only. - Removed the last Commons Clause wording: the README's license summary, the website FAQ and the consultants page said a commercial licence was needed to sell; it is not, under MIT.
CITATION.cffis now pinned to the shipped version (it had said 0.2.4 since July) and names the MIT license.
Verification scope
Windows: partially verified. The hard-gated tier (core, tooling-api, renderers, parsers, patterns, vault, extractors) passed. Advisory tiers were NOT green: WIN-1=failure, WIN-2=failure — those suites did not report a pass on Windows for this build and were not allowed to block it. See CI run https://github.com/PranavNagrecha/Salesforce-Intelligence/actions/runs/38956848457.
CI run: https://github.com/PranavNagrecha/Salesforce-Intelligence/actions/runs/38956848457
v0.5.0
Added
- License: plain MIT. The Commons Clause condition that earlier releases carried is removed;
LICENSE,NOTICE, every package manifest and the website say MIT and nothing else. RELEASING.md: the release cadence (a minor on the first Tuesday of each month, with batched patches for user-facing or security bugs) and the exact release order, with the known traps.pnpm release:check(scripts/release-check.mjs): runs every step of CI'sunit-testandbuild-testjobs locally, read from.github/workflows/ci.ymlso it cannot drift. It fails closed when the org-leak blocklist is missing..github/workflows/release-reminder.yml: opens a "Release vX.Y.0 due" issue on the first Tuesday of each month, with the checklist and the commits since the last tag.
Changed
- The default (
core) tool profile now advertises 16 tools instead of 25: the spine (sfi.resolve,sfi.search_components,sfi.get_component,sfi.health_check,sfi.list_analyses,sfi.describe_analysis,sfi.run_analysis,sfi.live_consent) plus the eight tools that answer the questions the package page advertises:sfi.find_component_usages,sfi.safe_to_delete_field,sfi.what_happens_on_save,sfi.find_dead_code,sfi.why_cant_user_see_record,sfi.effective_permissions,sfi.flow_graph,sfi.org_overview. Where-used, dead code and flow graph were previously not directly callable by default.sfi.route_question,sfi.synthesize_answer,sfi.capabilities,sfi.guidance,sfi.org_card,sfi.org_history,sfi.get_edges,sfi.get_impact,sfi.list_components,sfi.order_of_execution,sfi.who_can_access_objectandsfi.field_access_auditremain available throughsfi.run_analysiswith byte-identical output, or directly underSFI_TOOL_PROFILE=full. - The skill-gateway check (
scripts/check-skill-gateway.mjs) now derives the core roster fromeval/product-manifest.jsoninstead of a hand-mirrored copy that had drifted silently. - Shorter contracts for the default tools:
sfi.effective_permissions,sfi.why_cant_user_see_recordandsfi.safe_to_delete_fielddescriptions trimmed by a fifth to a quarter; the long form stays available throughsfi.describe_analysis { detail: 'full' }.sfi.org_overviewnow points atsfi.org_cardthroughsfi.run_analysis.sfi.what_happens_on_savetrimmed by a third and points atsfi.order_of_executionthroughsfi.run_analysis. vaultState.freshness.warningis one sentence under 200 bytes (builder version, running version, age in days,run sfi refresh to update) instead of enumerating every edge family added since the vault builder. Machine fieldsbuiltBy,runningVersion,ageDays, andstalestay on the stamp. Edge-family detail moves tosfi.health_check(freshness.nudge+missingEdgeFamilies) — measured onpackages/cli/demo-sourceviasfi.get_componentonCustomObject:Project__c: warning 1044 → 94 bytes.conceptReasoningis now opt-in (includeConceptReasoning: true) onsfi.get_component,sfi.field_360,sfi.what_happens_on_save, andsfi.explain_apex_method. Default responses carryconceptReasoningAvailable: trueplus a one-line hint naming the flag instead of ~5 KB of nested completeness/coverage/trust around a few short claims.- Root
pnpm testbuilds all packages before running unit tests;pnpm test:alladds the integration suite. - CONTRIBUTING documents the real tool registration path (
roster.ts/tool-dispatch.ts) and the full new-tool fan-out, without private-repo pointers. - Changelog fragment filenames use a short descriptive slug, not a private backlog id.
pnpm guard(release-guard.mjs) fails closed locally when the org blocklist is missing unlessSFI_ALLOW_NO_BLOCKLIST=1.
Fixed
find_dead_codenow greps production Apex source before reporting a classlikely_dead(not onlydefinitely_dead). A class reached only from tests but still named in production code as a type (List<Other>, a static field, orJSON.deserialize(..., List<Other>.class)) is downgraded touncertain, with the referencing file named in the reasoning, instead of looking safe to delete.- Custom Metadata value cells whose field name ends in
Class__c,ClassName__c,Handler__c,HandlerClass__c, orApexClass__cnow mint a heuristicreferencesedge to the Apex class named in the cell. A handler registered only through configuration no longer looks unused to dead-code and usage tools. - Quick-action Visualforce
referencesedges now targetVisualforcePage:{name}, matching the VisualforcePage extractor node id (previouslyApexPage:{name}, which left the edge dangling). unused_components: EmailTemplate, Group, and StaticResource notes no longer claim blind spots the extractors already cover (sendsEmail,hasMember, declared resource edges).what_if_change_method_signature: disclosure now says callers come from modeledcallsApexedges (AST when present), not a regex-only / no-AST scanner.cdc_subscribersandevent_subscribers: both now state the same EventBus truth — Platform Event subscribe islistensTo; Change Event subscribe is parsed but not yet an edge.endpoint_catalog:notCoveredno longer contradicts itself on Custom Setting / Custom Metadata values (vault holds them; this catalog does not yet scan those rows for URLs).
Verification scope
Windows: partially verified. The hard-gated tier (core, tooling-api, renderers, parsers, patterns, vault, extractors) passed. Advisory tiers were NOT green: WIN-1=failure, WIN-2=failure — those suites did not report a pass on Windows for this build and were not allowed to block it. See CI run https://github.com/PranavNagrecha/Salesforce-Intelligence/actions/runs/38784116429.
CI run: https://github.com/PranavNagrecha/Salesforce-Intelligence/actions/runs/38784116429
v0.4.0
Added
safe_to_delete_fieldlists name-only source mentions (nameOnlyMatches). Some Apex and OmniStudio components name a field without saying which object it belongs to: a field-list string spliced into dynamic SOQL, or an Integration Procedure or OmniScript JSON path such asalias:Field__c. These never become edges, so the verdict could readsafewhile they would break at runtime. The retrieved Apex and OmniStudio source is now scanned for the custom field's API name as a whole token. Each match is reported with its file, line and snippet, and moves asafeverdict toreview. The object is not verified, and the platform does not block the delete on these matches. The scan has its ownname-matchrow incheckedCategories. A match from a component that already references a same-named field on another object names it inalsoModeledOn, since the mention is probably that field.why_field_changedlists workflow field updates that nothing fires (unreferencedFieldUpdates): a<fieldUpdates>definition that sets the field but that no workflow rule, approval process or other component names. It never runs, so it is not counted as a writer, but it is no longer invisible (a host had read the silence as "workflow field updates were not retrieved"). Cross-object updates whose target object is not resolved offline are markedname-only.safe_to_delete_fieldlists them too: an exact match blocks the delete (it must be removed first), aname-onlymatch isreview.safe_to_delete_fieldreturnscheckedCategories: every category it checked, including the empty ones, withfound/none-found/not-checked(a coverage gap behind it), so "checked, none" and "not checked" are no longer the same silence. The coverage families behind the delete categories are now the same set the coverage caveat checks (OmniStudio, DLRS custom metadata, buttons and links, restriction and scoping rules were missing from it), so a failed retrieve of any of them shows asnot-checkedand is named incoverageCaveat. The name scan row isnot-checkedfor a family the vault holds but whose source it cannot read (e.g. a DataPack export).why_field_changedlists DLRS rollups as writers, with arollupblock naming the child object whose saves recalculate the field; an inactive rollup is listed as non-runnable.- Save order now shows DLRS rollup recalculation.
what_happens_on_save,order_of_executionand record-delete impact list each active DLRS rollup whose child object is being saved, saying it runs from the DLRS child trigger (the Apex trigger phase, not the platform roll-up phase), only through a Process Builder or Flow invocable in Process Builder mode, or queued in Scheduled mode; before, the rollup phase only knew native roll-up summary fields and read as zero. - DLRS rollups are modeled. Each
dlrs__LookupRollupSummary2custom metadata record becomes awritesToedge into its target field andreadsFromedges onto the child fields it aggregates, filters, relates through and orders by, each stamped withactiveandcalculationMode. omitted-sharing-on-entry-pointcode-quality rule. It flags a top-level REST, SOAP, remote-action or invocable class (high: the platform caller passes no sharing context), or an@AuraEnabled-only controller (low: Lightning runs it with sharing), that declares no sharing keyword.apex_structure's parsed check now reports the same rule id with the same severity model, replacingno-sharing-declared-on-entry-point(which called a Lightning-only controllerhigh), and the catalog's regex copy is not shown beside it. Guest-exposure findings name it as a security rule.endpoint_catalog.apexCallouts. Lists ApexsetEndpoint(...)call sites read from source, each marked as a named credential, a literal host or a dynamic target. A literal host is matched to the active Remote Site Setting that authorizes it, andsummary.unauthorizedCalloutHostslists hosts that no active remote site covers. Mock endpoints in@isTestclasses are skipped.- "Is this deployment risky?" can now be answered from a conversation.
sfi.review_changetakes the change set the way you have it:packageXmlanddestructiveChangesXml(the manifest bodies; destructive members are deletions),sourcePaths(changedforce-app/...files, orgit diff --name-statuslines such asD\tforce-app/...), orcomponentsgiven as ids, bare api names, or rows with an optionalchangeKind. Before this, the tool accepted only hand-built{ type, apiName, changeKind }rows, and the package.xml and git-diff parsing existed only in the CLI. Names resolve only on an exact api-name match. A changed file the vault holds maps to the component built from that file, so dotted, hyphenated and foldered names (custom metadata records, quick actions, layouts, reports, dashboards, path assistants) resolve exactly. Manifest names map through the inverse ofexport_manifest's type table (ApexPage,ApexComponent,CustomMetadata,Settings, and theLayoutmember form). An ambiguous or unknown name, a wildcard manifest member, a container file or member (labels, sharing / workflow / assignment rules), and metadata the vault does not model are listed in the newinputResolution.unresolvedblock and are not reviewed; nothing is guessed. A deleted container is reviewed as every rule the vault holds in it. A rename is reviewed as the old component deleted plus the new one added, and a file deleted inside a bundle modifies the bundle. Docs, READMEs and project config are listed but do not count against the decision. A row without achangeKindis reviewed asmodifiedand counted. deployDecisionon every review. It isno-go,review-firstorgo, with the reasons behind it, and it is derived from the review's own tallies. It is nevergowhile something named in the change set was left unreviewed or unchecked: an unresolved name, a container or unmodelled metadata file, a wildcard, a family the vault does not fully cover, undetermined test coverage, added Apex that no test in the vault reaches, or a collision check turned off withincludeCollisions: false.automationCollisionson the review. It reusessfi.automation_collisionson the objects the change set touches (at most 5) and keeps the field-write collisions and save-recursion cycles that involve a changed component. Turn it off withincludeCollisions: false.who_can_access_objectnow says what its filters left out. WithaccessLevelorprincipalTypeset, the response carriesexcludedByFilter(count,byReason, up to 5examplesper reason) for principals that still reach the object:read-only(Read / View All only),record-sharing-only,other-capabilities(for example Create without Edit) andother-principal-type. TheboundaryNotenames the counts. Before this, "which permission sets can edit X" dropped a View-All-only permission set without a word.endpoint_catalogApex callouts carryasyncContextandcredentialCandidates. A call site inside a@futuremethod is markedfutureat method granularity, withallowsCalloutsread fromcallout=true. A site in aQueueableorDatabase.Batchableclass is marked at class granularity, withallowsCalloutsread fromDatabase.AllowsCallouts. Adynamicsite in a class that builds'callout:' + namenow lists the catalog's named credentials whose name is a quoted literal in that class (same-component) or in an Apex class or trigger with a graph edge into it (direct-caller). These are alwaysheuristic: the literal is there, but the call that passes it is not traced.what_happens_on_savewithevent: 'upsert'labels each step withfiresOn(insert,updateor both). The labels come from the same matchers that admitted the step, so a Create-only flow reads['insert']and no longer looks like it runs on every save. Assignment, escalation, approval and trigger-dispatched async steps carry no label, because the metadata does not tie them to an event. Auto-response rules are['insert'].sfi.object_360format: "handbook": a one-object brief for a new team member. Ask "document the Case object for a new hire" and you get one compact Markdown brief. It covers what the object is (description, kind, sharing defaults), its key fields ranked by declared required / external id / unique flags and then by how many components reference them (with help text), active picklist values, record types with their business processes, parent lookups (business lookups separate from platform system lookups) and child objects (master-detail cascades are marked), what runs on save in documented order with each step tagged by the event it fires on (insert, update or both) and each validation rule's error message, who can read, create, edit, delete, view all or modify all, integrations touching the object (outbound messages, integration components, Apex REST resources, Change Data Capture), risk signals with their evidence, and the next tools to call. Every list is ranked and then capped, and states its true total; a wide object is tightened to fit a 24 KB budget, with the trimming disclosed. Empty sections say what was checked. The save sequence comes fromwhat_happens_on_saveand is not re-derived: phase counts are the save tool's own full counts, a phase it had to shrink is fetched again in full, and any phase still short is shown as "at least N". Delete and undelete automation gets its own line. A lookup whose target was not captured (common on standard objects rebuilt offline) is listed as "target not captured" rather than left out.includeSections/maxRowsPerSectionwith this format is a namedinvalid-query, because they shape the defaultsectionsformat only.what_happens_on_saveandorder_of_executionreturn areentryblock, the "why did my flow run twice" an...
v0.3.3
Added
- OmniStudio is modeled from the inside. Every OmniScript, Integration Procedure, FlexCard and DataMapper is parsed into its elements through one element catalog (canonical types, every alternative spelling, runtime roles — generated into
docs/omnistudio/element-catalog.md). New edges: any element naming aremoteClasscalls that Apex class (callsApex, with methods and call sites — Apex called only from OmniStudio no longer looks dead); DataMappers read and write individual fields (Extract, Turbo, Load — a Load's object write says create or upsert); an Integration Procedure Delete Action deletes the objects it names; a FlexCard SOQL data source reads its object and fields. Cross-references now land on the version that runs (an IP'sisActiveis the runtime switch; a DataMapper'sactiveflag is not), so impact and dependency walks cross from a screen into its server code. - Ten OmniStudio tools:
omni_model,omni_save_trace(does each answer reach a field?),omni_prefill_trace(does it come back on reopen?),omni_dead_references,omni_version_diff,omni_edit_block_audit,omni_completion_audit,omni_form_spec,omni_path_simulator(now withDEAD_END_SECTION: answers that add a section while its entry step hides itself, found by a satisfiability search over the step's rule expanded through the script's formulas) andomni_changed_since(semantic diff of what changed since a snapshot). - Four audit tools:
record_delete_impact(cascade / orphan / restrict per lookup, delete guards, who can delete — including generic Apex delete helpers resolved at their call sites),persona_unused_grants(object and field permissions nothing a persona can run uses; never UNUSED when it cannot tell),live_verify(re-check one vault claim against the org, read-only and consent-gated) andhow_to_see(the Setup click path to see a component). sfi run <tool> --json --<arg> <value> … --vault <path>calls any tool from a script through the MCP dispatch path; arguments are typed from the tool's own schema.--vault <path>means one thing everywhere —run,status,doctor,selftest,snapshot,refresh,feedback export,gaps report,review-change— and accepts theorg-kbfolder or the folder that holds it.gaps report --vaultcounts only that vault's gaps.- App scope for counting tools.
picklist_integrity_scananddoc_coverage_reportacceptscope{namePrefixes, namespaces} (default:org-kb/config/app-scope.json, else the OmniStudioappScope) and answer with the scoped number, the org-wide number beside it as a labelled contrast.picklist_integrity_scannow reports how every picklist is restricted (global value set / inline restricted / inline unrestricted / unknown). - The field extractor records a picklist's
restrictedflag and a lookup'sdeleteConstraint; snapshots record runtime switches and a hash of each OmniStudio source file. - Managed-package (Vlocity) OmniStudio. A Vlocity Build Tool DataPack export placed under
org-kb/source/vlocity/(OmniScript, IntegrationProcedure, DataRaptor and VlocityCard DataPacks, with their sibling files; the%vlocity_namespace%placeholder or a realvlocity_cmt/vlocity_ins/vlocity_ps/omnistudioprefix) is converted to the native component's shape and extracted by the same code as native metadata — same node ids, edges, graph resolution, and every OmniStudio tool. Nodes carrysourceFormat: vlocity-datapackand cite the DataPack file; a snapshot fingerprints the whole DataPack folder, soomni_changed_sincesees an edit to a sibling file; other DataPack types are disclosed as skipped, and the refresh never deletes an export. OmniStudio tools say intrust.limitationswhen an installed Vlocity package has no export (its components are missing, not absent) and how many components came from one. Export recipe indocs/configuration.md. - Apex that runs OmniStudio is a caller in the graph.
<ns>.IntegrationProcedureService.runIntegrationService('Type_SubType', …)and<ns>.DRGlobal.process(…, 'Bundle')/processObjectsJSONwith a literal key (namespaceomnistudio,vlocity_cmt,vlocity_insorvlocity_ps) becomedispatchesOmniActionedges from the Apex class (via: apex), resolved onto the version that runs — so usage, impact and unused-component answers count Apex callers, and an IP called only from Apex no longer looks unused. The call no longer mints a phantomApexClass:IntegrationProcedureService/DRGlobalor aCustomField:<ns>.<Class>read. A key built at runtime is not guessed. New conceptapex-runs-omnistudio. - The managed runtime's IntegrationProcedureService is an IP call, not Apex. A Remote Action on
<ns>.IntegrationProcedureServicedispatches to the Integration Procedure its method names (dispatchesOmniAction,via: runtime-service); the save and prefill traces follow it into that IP, andintegration_procedure_chainlists it as anintegration-procedureendpoint. - Concepts and rules for the new semantics (DataMapper writes, remote Apex calls, Edit Block delete wiring, IP Delete Actions, lookup delete behavior, picklist restriction, dead-end sections, DataMapper
activenot being a runtime switch, managed-package OmniStudio records).
Changed
integration_procedure_chainwalks steps nested in blocks, classifies every spelling through the catalog, names the active version of a multi-version target (others listed), resolves Apex calls to ApexClass nodes, reports Delete Actions, and sums what the chain reads, writes and deletes. Its andomniscript_flow's Apex disclosure now says what is and is not modeled.- The OmniStudio formula evaluator computes
CONTAINSand returns computed values, not only truth. - The Integration Procedure extractor compares element types through the catalog, so an
HTTP Actioncallout is recorded like aRest Action. - An Apex class OmniStudio calls by name that the vault does not hold says what that means and how to tell a missing class from a package service.
- The five OmniStudio component tools share ONE native-vs-managed-package disclosure, which now says where managed-package components come from instead of that they are not extracted.
- Every tool the release added is registered in the high-fan-out inventory with a real-org probe (the 0.3.3 registration backlog is empty).
- Question routing keeps the older tools reachable next to the new OmniStudio vocabulary: "which OmniScripts exist" reaches the inventory tool (
list_components, through a component-family card that dilutes nothing else), and "would the platform block deleting this field" still reachessafe_to_delete_field. - The default tool profile hid the tools the product's own front page promises it can answer. The package documentation invites readers to ask "what breaks if I delete this field?", "who can edit this field?", "why can't this profile see this record?", "what happens when I save this?", and "give me a tour of this org" — and the tools that actually answer those questions were among the 198 of 217 tools the default profile did not advertise. A schema-driven MCP host reads the tool list once per connection and will not invent an internal gateway tool on its own, so the product answered its own advertised questions only for a caller who already knew that gateway existed. The token-cost argument for a small default roster was real but had never been measured: measured now, the six newly-added tools cost about 36.5 KB against a 665 KB full roster and a 67 KB spine — the default grows from 67 KB to 104 KB, about 16% of the full roster, specifically for the tools the front page promises. The default ("core") profile is now derived, not hand-picked, from a map pairing each published question to the tools that answer it, checked by a test that every question in that map appears verbatim in the package README — so a promised question with no tool behind it now fails the build instead of failing a user. Default tool count: 19 → 25.
- Twenty-two tools, including the single most-called tool in the roster, advertised every argument as optional and then refused to be called with none at all. Each takes a selector under multiple accepted spellings (an id under two different names, an object under three), so the real contract is "at least one of these," and nothing in the advertised schema said so — a schema-driven host had no way to know an empty call would be refused rather than accepted. The existing schema-accuracy check measured required arguments by removing one key at a time from a full example input; with two valid spellings present, removing either one alone still parses, so the check had structurally never asked the one question that would have caught this: can this be called with no arguments at all? A new shared helper, applied uniformly across all twenty-two tools rather than as twenty-two hand-written variants, closes the gap.
- Seventeen tool descriptions were rewritten to match the object-scope-refusal fixes above — the description is the contract a host LLM reads before deciding whether to call a tool, so a stale one is not cosmetic. One had gone further than stale: it had documented its own defect as a deliberate feature, describing an unmodeled object's empty result as clean rather than unchecked. A new drift ratchet now tracks hand-rolled "was this actually extracted" checks that should route through the product's shared helper for that instead, so the pattern this release fixed repeatedly cannot silently regrow.
- The roster-count change rippled outward: four tests that had pinned the old count of 19 as a literal now derive it from the same source of truth, and six more places — across the contributor guide, the configuration docs, and both website pages — that still described the default profile as "19 tools" or as merely "the spine" (rather than "the spine plus the tools the package page a...
v0.3.2
Added
titleandwebsiteUrlon the MCP Registry record.packages/cli/server.json— the manifest the release workflow publishes to the official MCP Registry on everyv*tag — carriedname,description,status,repository,versionandpackages, and nothing else. The published record had no human-readable name beyond the reverse-DNS idio.github.PranavNagrecha/sf-intelligence, and the GitHub repo as its only link; the documentation site appeared nowhere in it. The record now sets atitleandwebsiteUrl(https://sfi.auditforce.cloud). 0.3.2 is the first release that PUBLISHES them: the fields were added 70 minutes after thev0.3.1tag was already cut, and the registry rejects a duplicate version outright, so they could only reach the registry with the next tag.- Setup mode: the MCP server now always connects, even with no vault. Previously
sfi mcpwrote its "runsfi init" guidance to stderr and exited 1, which every MCP host renders as "server failed to connect" — the one message that would have unblocked the user went to a channel the user cannot see. The server now boots and exposessfi.setup_status, a read-only tool reporting where it looked for a vault, which orgs are authenticated, and the exact ordered commands to build one, so the assistant can walk the user through setup from inside the chat. docs/guides/mcp-hosts.md— per-host, per-platform connection guide for Claude Code, Claude Desktop, Codex, and VS Code + GitHub Copilot on macOS and Windows: exact config paths, exact blocks, log locations, and an ordered "when it does not connect" checklist.
Changed
sfi initnow requires--target-orgwhen stdin is not a terminal. This is breaking for any script or automation that relied on the default-org fallback; pass the alias explicitly.- Six hand-rolled spellings of "split a path" / "render a path relative to" across five packages are replaced by one module,
@sf-intelligence/core'spath-portable. The rule it encodes: the number of correct spellings is one.toPosixPath(unconditional) andtoRelativePosix(host-gated) are deliberately kept distinct — the vault'ssourceTreeHashdepends on the gated form, and merging them would make every existing vault report stale. A pinned-digest test now locks those bytes. - The Windows CI job runs again, on every push. It had been
if: github.event_name == 'workflow_dispatch'— never running on push or PR — and excluded four tests plus three whole CLI test files. The gate was disarmed at precisely the files carrying the Windows defects this release fixes. It now runs the full unit suite on every push. It is ADVISORY for the moment — its first real runs surfaced runner-performance failures (a DuckDB-backed test at 48s against a 45s ceiling; the 10k-node scale import at ~99s against 90s; graph fixtures thatrmdirwithout closing their DuckDB handle) which are not product bugs and need work in the graph/test layer. Advisory rather than excluded, so the debt stays visible in the log instead of being silently filtered the way the whole job used to be. - The platform-fragile tests are no longer hidden by CI-only
-t/--excludefilters. Genuinely POSIX-only fixtures (a#!/bin/shscript, a0600file mode, aCOMSPEC-stubbed win32 simulation) aredescribe.skipIf(process.platform === 'win32')in their own source, where a reader can see them; the rest now derive their expectations fromnode:pathinstead of hardcoding a POSIX rendering. - New
pnpm check:portabilitygate (wired into CI): fails the build if hand-rolled path-separator logic reappears in a package source tree. The four legitimate exceptions are allowlisted with their reasons.
Fixed
- Nine tools advertised an input shape their own validators do not enforce. Every
sfi.*tool carries TWO input contracts — the Zod schema its handler validates against, and the JSON Schema an MCP host reads out oftools/list— and where they disagree, a host that validates arguments before sending them refuses calls the handler would have served. Three were flagged on therequiredaxis:sfi.get_componentadvertisedidas required while the already-advertisedcomponentIdalias satisfies it through the preprocess,sfi.explain_erroradvertisederrorTextwhileerror/message/errorMessage/texteach satisfy it, andsfi.explain_debug_logadvertisedlogTextwhiledebugLog/log/text/contentdo. Those aliases exist BECAUSE hosts guess those names, so the advertisement refused exactly the call the design anticipates. Six more tools accepted keys no schema-driven host could see:componentId(interchangeable withfieldIdat the validator) onsfi.explain_fieldandsfi.field_access_audit,eventApiNameonsfi.event_subscribers,staticOnlyonsfi.unused_fields_deep, and the object selectors onsfi.layout_assignmentsandsfi.automation_collisions.sfi.automation_collisionswas the worst of them: its advertised schema pairedadditionalProperties: falsewithrequired: ['object'], socomponentId/objectApiName/objectIdwere not merely invisible — the advertisement declared them INVALID. Sevenrequiredlists shrank in all, not three, because advertising an alias is what makes the old key omittable:required: ['fieldId']came offsfi.explain_fieldandsfi.field_access_audit,required: ['componentId']offsfi.layout_assignments,required: ['object']offsfi.automation_collisions. No handler and no validator changed — this is the advertisement catching up to what the code already accepts. Two costs are recorded rather than hidden. JSON Schemarequiredcannot express "either key", so those schemas now UNDER-claim: a call naming neither key is advertised as valid and still fails at the validator with a named error. And the two object selectors added an advertisedobjectId, which the ADR-007 id-naming gate reads as fresh drift — grandfathered inresponse-consistency-baseline.jsonbeside the 21 sibling tools that already carried it. retrieveConfirmedwas set on 0 of 96 coverage rows — the flag that separates a CONFIRMED-empty family from one nobody ever retrieved was inert in the field built for it. Not a missing feature: a regression this project caused.stampFamilyEpochscarriesretrievedAtandepochforward across a--no-pullrebuild and silently droppedretrieveConfirmedalongside them, so a rebuild erased the evidence while leaving every row looking intact. It is carried forward now, and only when this pass gives no reason not to: never over aretrieveConfirmedthis pass set itself, an error it found, or apendinga decorator forced. Delivered in the same squash:pendingmeant two different things. "Not yet attempted" and "attempted, capped, real partial evidence" were one flag, so the usage-ranked report/dashboard pull (SFI_REPORTS_CAP) and the report-node persistence ceiling wrotepending: truebeside a non-zeroretrieved— a family with 388 items actually retrieved read as if nothing had run. A separatecappedstate now carries that case, with its own bucket insfi.coverage_report. Downstream behaviour is deliberately unchanged: a capped family is still excluded fromcovered, still folded intomissingCoverage, and every absence caveat still fires — only the REASON is now legible. A--no-pullor scoped refresh with no cap evidence of its own falls back to the previous manifest'sreportsCapblock rather than regressing the row to the fold-erased default.ConditionalContextwas required coverage it can never satisfy. It is a synthetic node the extractor mints while parsing a firer's condition — never a familysf project retrievepulls — sobuildCoverageEntriesnever writes it a coverage row. Naming it inVALUE_LITERAL_READER_COVERAGEmade that list permanently unsatisfiable, poisoningmissingCoveragefor its two consumers,sfi.value_change_auditandsfi.what_if_remove_picklist_value: every answer hedged against a gap no refresh could ever close. Replaced by the firer families that actually producefiresWhenedges —ApprovalProcess,AutoResponseRule,AssignmentRule,EscalationRulejoin theWorkflowRule/ValidationRule/Flowalready listed. Both consumer fixtures had hand-copied the list and invented aConditionalContextcoverage row no real vault has, which is exactly how it went unnoticed; they now import the shared constant.sfi.find_component_usagescounted a component's own declaration as evidence something uses it. The grep supplement matched a class's ownclass Fooline exactly as a caller's reference would, so a component with zero graph referrers and one grep hit — its own declaration — reportedhasStaticEvidence: true, in the tool people consult before deleting things. Self-matches (the declaring file, or any file in the component's own LWC / Aura / Visualforce bundle directory) are now excluded beforematchCountandhasStaticEvidenceare computed, counted in a newselfMatchesExcluded, and named inboundaries[]so "grep ran and found only its own declaration" cannot be read as "grep ran and found nothing".sfi.who_can_access_objectshipped half a page and called it whole. The handler sized its page tolimitbut not to the response byte budget, so the envelope's blind tail-truncation cutgrantersout from under the already-computedhasMore/truncated: 218 real rows delivered as 109, withhasMoreandtruncatedbothfalse, so nothing told the caller there was more to page for. The completedatapayload is now fit to the budget by binary search before the envelope sees it, sohasMore/truncated/offsetdescribe the rows actually shipped. Sharing-rule rows also gainedsourceRuleId:vianames the rule TYPE, not the rule, so two different rules of the same type sharing with the same principal collided...
v0.3.1 — the tools audit themselves
A correctness release against tools that were already shipped. No new capability tier.
Three adversarial audits went through the permissions, schema-and-search, and automation tool families, hunting only tools that predated the work. They produced 39 verified defects — each reproduced against a real org vault, measured for scale, and checked against a sibling tool answering the same question. Then a review pass and an adversarial QA pass found 18 more that the fixes themselves introduced, all of them invisible to a green suite.
Both numbers are reported here on purpose. A release note claiming a clean sweep, from the same work that shipped the original 39, would not be credible.
What moved
| Measurement | Before | After |
|---|---|---|
| Apex classes reported likely dead, of 186 | 85 | 0 |
find_dead_code likely_dead (both vaults) |
18 | 2 |
definitely_dead at org scale (the control) |
1041 / 979 | unchanged |
| Classes reported as having no test | 58 | 19 |
| Save-order steps returned for a busy object | 5 of 57, marked complete | 57 of 57 |
| Share of a save response spent on the answer | 12% | 78% |
| Rows reachable by paging a code-quality audit | 94 of 647 | 647 |
| Rows reachable by paging an unused-field audit | 281 of 570 | 570 |
| Trigger-deactivation verdicts carrying information | 0 of 22 | 22 of 22 |
| "What runs when I save a contact?" answered end to end | no | yes |
Four worth naming
A verdict justified by something untrue about Salesforce. find_dead_code told you it was safe to delete classes that run on a schedule, reasoning that such a class "must be enqueued/executed/scheduled by user Apex." An admin scheduling through Setup creates a CronTrigger record — data, not metadata — never retrieved, no node, no edge, and no refresh can ever close that gap. 16 of 18 likely_dead verdicts on each vault were exactly those classes. They are now uncertain; definitely_dead is unchanged, so the control held.
A stale retrieval overwriting the current one. A vault can hold two complete retrievals. Node writes replace on conflict and the directory walk is alphabetical, so the older copy won. On a real org, three profiles reported an MFA-bypass permission the current retrieval no longer declared. The canonical layout now wins and every conflicted component carries a sourceConflict field naming both paths and how the answer was chosen — or says precedence: undetermined rather than guessing.
The honesty payload was being truncated. The response trimmer learned to descend a level, which made the disclosure arrays reachable — the lists of what a tool did not check — and began shortening them under a note claiming a total that was never published. Disclosures are now never trim candidates; an oversize response refuses and names real narrowing knobs instead.
Resume pointers that skipped whole windows. A pointer computed from what a handler intended to return, rather than what survived a second trim, made rows unreachable by any call. Pointers are now corrected only on a positive match against a page size the payload itself publishes, and invalidated with a stated reason otherwise.
What was actually wrong, underneath
Almost every defect was a second copy of something — two same-named constants, a block duplicated under a comment promising byte-identity, a hand-copied JSON Schema beside the validator that enforces, two rule counters, two byte limits that had to stay ordered and were set independently in the wrong order. Every one written by someone who knew. The comment was the mechanism, and comments do not hold.
So the durable part of this release is not the fixes:
- an advertised-vs-enforced schema parity gate over all 217 tools on four axes, with 27 pre-existing violations baselined and reasoned rather than hidden
- a tool-local byte budget derived from the global one, so the ordering holds by construction
- drift tests running two implementations of a shared predicate against one fixture
- a documentation pin that could not fire — its pattern could not match across a line break, so a stale count sat beside three correct ones and the file reported clean
Upgrading
No tool's input contract breaks an existing call. Ten tools gained advertised inputs they already accepted, one dropped an input that was a pure synonym for the canonical one, and several now refuse selector combinations that previously resolved silently to something you did not ask for.
If your vault holds more than one retrieval of the same org — a source tree with both a flat and an SFDX layout — rebuild it. Until you do, the tools will tell you which components are affected instead of quietly answering from whichever copy sorted last.
11,412 tests green. Full detail for this release is in CHANGELOG.md — the complete section runs to ~140,000 characters, past GitHub's release-note limit, so this page is a summary.
Release post: https://sfi.auditforce.cloud/blog/sf-intelligence-0-3-1
v0.3.0
Added
- Supply-chain hardening (AUDIT-F10). Consumer guide
docs/guides/supply-chain.md(pinned install, provenance verify, SBOM);
scripts/check-pack-allowlist.mjs/pnpm pack:checkenforces
packages/clifilesas the published tarball allowlist (wired into
prepublishOnly); tag publish attaches a CycloneDX SBOM to the GitHub
Release. - Verdant public truth gate (AUDIT-F7). Hand-authored
examples/demo-vault/truth/manifest.jsonplus
sf-intelligence-qa/scripts/verdant-truth.mjs(inventory + design-goal tool
pins + mutation self-test). Wired asharness:verdant-truthin the commit
gate. Multi-org expansion and full scorecards deferred. - Untrusted org-metadata branding (AUDIT-F8). Contracts expose
UntrustedOrgText/ContentPolicy/ORG_METADATA_CONTENT_POLICY.
sfi.get_componentandsfi.resolveadd additivelabelOrgText/
descriptionOrgTextfields; the MCP dispatcher stampscontentPolicyon
success envelopes so hosts treat org strings as data, never instructions or
consent. Markdown escaping remains a renderer concern
(escapeMarkdownInlineexported from@sf-intelligence/renderers). - The zero-friction demo is now discoverable.
npx -y sf-intelligence demoserves a synthetic org with no Salesforce auth, nosfCLI and nothing to configure, but it appeared on only 2 of 34 site pages and in zero occurrences acrossllms.txt/llms-full.txt— so an assistant reading the site saw only a three-prerequisite wall and had reason to hedge before recommending the tool. It is now on/mcp, every use-case page, all comparison pages and inllms.txt, including a generic stdio config for Claude Desktop, Cursor and Codex that previously existed nowhere (the demo was documented for Claude Code only). - Sitemap entries now carry
lastmod, sourced from each page's real git commit date — the one sitemap signal Google uses for recrawl scheduling, previously absent on all 34 URLs whilechangefreqandpriority(both documented as ignored) were set on every one. Deliberately falls back to filesystem mtime and then to omitting the field rather than stamping build time, which would make the signal a lie Google learns to discount. - Structured data:
WebPage,PersonandImageObjectnodes on every page, so a graph is actually bound to the URL being parsed. Fixes/mcpemitting a typed-but-emptySoftwareApplication—@idscope is per-document, so referencing the home page's entity by id produced the site's only invalid item, on its best-ranking page.og:type,datePublishedanddateModifiedare now threadable from any page throughDocPageandBase. - Supply-chain provenance is now stated on the site: npm publishing is keyless OIDC with Sigstore attestations, verifiable via
npm audit signatures.
Changed
- EvidenceEnvelope v2 (AUDIT-F4). Shared output contract
(EvidenceEnvelopeV2in@sf-intelligence/contracts) for claims, evidence,
coverage, freshness, pagination, and absence verdicts. Opt-in projection
underdata.evidenceEnvelopeonsfi.interpretandsfi.safe_to_delete_field
(legacy keys unchanged). RuntimeassertEvidenceEnvelopeV2guards those
handlers; not applied roster-wide. - Retrieval ledger + family epochs (AUDIT-F5). Coverage rows carry per-family
retrievedAt/epoch(preserved across scoped--typesrefreshes). Refresh
writesmeta/retrieval-ledger.jsonand appendsmeta/tombstones.jsonlfor
confirmed reconcile deletions (never on refuse).TrustSummary.freshness
can discloseoverall: 'mixed'withfamilies/oldestEvidenceAt.
sfi.coverage_reportsurfaces tombstones + mixed-freshness limitations. - Core-by-default + strict invocation (AUDIT-F6). Default
SFI_TOOL_PROFILE
iscore(19-tool spine, incl.sfi.live_consent). Directtools/calloutside the advertised set is
denied under core — usesfi.run_analysis(target must be a registered tool).
sfi.describe_analysisgains progressivedetail(summary|schema|
full; defaultsummaryunder core). SetSFI_TOOL_PROFILE=fullfor the
previous advertise-everything behavior. - Tool catalog hygiene (AUDIT-F9). Scrubbed internal milestone / wave
language (R6-*,AUDIT-*,v2.x R2a…,P5-*, …) from MCP tool
descriptions sotools/listandlist_analysesone-liners read as product
jobs.list_analysesnow omitshiddenretired aliases (same advertise
contract astools/list; still invokable viarun_analysis). Structural
consolidations (−4 hidden aliases) were already shipped; further handler
merges deferred. - ProductManifest /
sfi.capabilitiesreportdefaultProfile: 'core',activeProfile, and anadvertisedcount that matchestools/listunder the active profile (full roster remains underprofiles.full). - SERVER_INSTRUCTIONS and capabilities routing guidance teach the core profile +
run_analysisgateway; they no longer tell hosts to callsfi.interpret/sfi.live_consentdirectly or to useliveEnabled: trueas consent. - Skills / agents / commands under default
SFI_TOOL_PROFILE=corenow
instruct hosts to invoke non-core analyses throughsfi.run_analysis
{ name, args }(Decision 2=C). Shared grounding footer + entry skill teach
the gateway;pnpm skill-gatewayfails CI on direct non-core Call/Fire
instructions.llms.txt/llms-full.txtand.claude-plugin/plugin.json
pinsf-intelligence@0.3.0(gated byverify-doc-sync); the website's own
install snippets stay unpinned sonpx -yresolves latest. Every surface
states core as the default, not full. - SBOM generation uses
@cyclonedx/cdxgenviapnpm sbom(pnpm-aware,
fail-closed). Tag publish attaches a non-empty CycloneDX 1.5 artifact or
fails the job — no more emptynpm sbomskip. - New article: "What does this Salesforce Flow do?" — reading a Flow you did not build. Documents that the concept model does not extract
triggerOrderand therefore cannot confirm whether Flow Trigger Order is configured, rather than implying the order is undefined. - The three thinnest comparison pages (
hubbl,dx0,metazoa— 314/328/347 words, all with zero search impressions) expanded past 1,150 prose words each, and a false claim corrected: the Hubbl page said MCP was "not the core public product story", but Hubbl ships an MCP server and documents it ("Connect AI assistants to Hubbl via the Model Context Protocol"). An unverifiable dx0 ISO certificate number was removed, and Metazoa pricing is now described qualitatively because they publish no list price. /use-cases/what-breaks-if-you-delete-a-fieldand/blog/delete-unused-salesforce-fieldswere competing for the same query with overlappingFAQPagequestions. The use-case page is narrowed to the product surface and itsFAQPageblock removed; the blog keeps informational intent. Sitewide there are now zero duplicate FAQ questions across 15 pages and 58 questions./use-cases/sharing-troubleshootingexpanded from ~230 to ~1,450 prose words, and a wrong tool name (why_cant) corrected towhy_cant_user_see_record.
Fixed
- A no-intent question could be dressed up as an advisory route. "any thoughts on the general vibe of the setup here" upgraded from an honest
unroutedtofunnel-advisory, topped bysfi.live_setup_audit_trail— attraction to the bare token "setup" (the English noun vs the Salesforce menu), not meaning. Measuring 30 no-intent × 20 advisory-tier questions showed the score threshold cannot fix it: the noise ceiling (0.436) sits above the advisory signal floor (0.261), and raising the bar past the noise leaves ~7 of 21 genuine advisories alive. The separable axis is evidence breadth — candidate mass at ranks 3-8, where noise spans [0.249, 0.277] and signal spans [0.376, 1.672]. A newFUNNEL_MIN_EVIDENCE_BREADTH(0.32, the centre of that gap) is now a second condition on the advisory upgrade: one lexical collision lights up a single tool and leaves the tail flat, while a real question spreads support across many index terms.FUNNEL_PRIMARY_MIN_SCOREstays 0.26 — the fix does not belong on that axis. Funnel-recall, the router goldset and the routing gate are unchanged. Partial, and measured as such: on a 30-question no-intent set this cuts spurious advisories from 6 to 2. It closes the single-token-collision class ("setup", "audit"). It does not close multi-token conversational asks — "how does this compare" still reachescompare_components, "can you help me out" still reachesdoc_coverage_report— because those words genuinely spread across the tool index, putting their breadth inside the range real questions occupy. Tightening past them would cut measured signal, so the remaining class needs an intent classifier, not a threshold. - A version tag published without any guarantee CI had run.
publish.ymlfires on tag push;ci.ymlfires on push-to-main/release/**and pull_request — never on tags. The two were fully decoupled, so tagging any commit shipped it to npm and the MCP Registry unverified. (needs:cannot close this — it only orders jobs within one workflow file.) The publish job's first step now asks the API what CI actually did on the tagged SHA and fails closed on everything except a clean success, including the real hole — "CI never ran" is never treated as a pass. It waits out an in-flight run rather than racing a fresh merge. - The published SBOM under-reported what consumers install.
cdxgenwas pointed atpackages/cli, which has nopnpm-lock.yaml— with nothing to resolve it fell back to that package.json alone and emitted 19 direct entries with an empty dependency graph. It now resolves the transitive runtime closure from the workspace lockfile: 137 components, 138 graph nodes. A component floor and a non-empty-graph check fail the publish rather than shipping a hollow ...
v0.2.5 — field-audit surface + delete-safety edge fixes + refresh data-loss fixes
Closes five ways sfi.safe_to_delete_field could report a field safe when the platform refuses to delete it, and fixes four pre-existing refresh bugs found along the way — one of which deleted the vault it was refreshing and reported success.
Delete-safety gaps closed
Every one sat inside a metadata family the refresh had fully retrieved, so no coverageCaveat fired and the verdict presented as clean rather than hedged. A tool can only warn about the gaps it knows it has.
| Gap | Symptom |
|---|---|
| Flow record-trigger entry criteria | Flow ships two XML spellings of one condition triplet; only <leftValueReference> was parsed. On the reference org: 449 filters across 160 of 275 flows, 100% in the unparsed <field> dialect |
| Roll-up coupling | Held as a node property, never an edge — declared on the parent, so a child-field walk never reached it |
| Condition field refs | Held as a property; firesWhen runs firer → context, so nothing reached the field |
Formula __r traversals |
Skipped — a field read only via Parent__r.Field__c showed zero referrers |
FlexiPage relatedListFieldAliases |
Bare names on the related object, invisible to the dotted sweep |
85 fields on the reference org had zero dependency evidence before this release.
Citation honesty
A correct verdict with invented evidence is still a defect. Fixed: a roll-up summary cited on 127 fields where none exists; a traversalPath promised in a note that no renderer emitted; two of three roll-up roles described; 4 of 7 condition firer families named; and one validation rule counted as two blockers in two categories. Every citation now names the component to actually go change.
Refresh bugs (all pre-existing)
reconcileSourceDeletionsdeleted the vault it was refreshing. It compared raw paths across two layouts, so every in-scope file read as "deleted in the org" — 974 nodes,status: success, exit 0. Reproduced by executing the shipped code: 8 of 8 files deleted. Now layout-agnostic, with a wholesale-deletion guard that refuses and explains.- The additive retrieve had never worked. It named an
--output-dirmodernsfrejects. Both explicit targets are refused; the accepted form names none. - A failed report pull was swallowed, leaving a vault byte-identical to a successful one. Now recorded on the manifest and printed.
- Report coverage asserted a confirmed zero it could never contradict —
retrievedwas counted after the usage fold drops those nodes, yetretrieveConfirmed: truewas stamped anyway, about an org with 4,296 reports.
⚠️ Behaviour change
safe is harder to reach on a coverage-degraded vault. Four more condition-firer families are attested, and a pre-0.2.5 vault routes safe → review until re-refreshed. Deliberate: an unretrieved family can hide a condition blocker. Re-run sfi refresh after upgrading.
New surface
salesforce-field-audit (26th skill) · salesforce-field-auditor + salesforce-field-refuter (the plugin's first subagents) · /sfi-field-audit · sfi.field_audit MCP prompt · field_360.rollups
209 tools · 26 skills · 5 slash commands · 2 subagents
Verification
9,895 tests, lint, all gates, CI on Node 20/22 + macOS. Real-org end-to-end through the MCP server: each fixed path returns blocking with a checkable citation, and a field with no dependencies still returns review — not blocking — so the tool still discriminates. All three refresh paths reconcile at identical counts.
Known limits
windows-build-test is skipped by repo config. Report coverage is honestly partial (1,959 of 4,296 retrieved, reported pending). The manifest edge tally is render-derived and under-counts; documented, not fixed.
📖 Can I delete this Salesforce field? — the method behind the release.