Skip to content

Releases

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 19 Oct 20:54

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 refresh

Refresh 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-parser 4.5 → 5.11, clearing the moderate advisory (GHSA-gh4j-gqv2-49f6) that npm install reported 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_impact rows are untouched Nodes (their properties.status / active is the raw value), and a CustomField root on a vault with no recorded builder version now reads complete: true because every field referrer kind is an edge at the current tier. object_360, safe_to_delete_field's referrer run state and automation_collisions read activation through the same helper; an ApexTrigger whose status is Deleted no 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 by insert, update or Database.update, alone or inside a list literal) now mints a parsed writesTo edge like record.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_analyses finds a tool from any of the words in a question. The query filter 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 found sfi.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 its matchedTerms. 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.ts promised a subscribesToChange modeling path for CDC subscribers and never emitted it, so sfi.cdc_subscribers saw a CDC trigger only through its object-lifecycle triggersOn edge and never saw an EventBus.subscribe to a Change Event at all, while sfi.event_subscribers described the same registration as recognized. New EdgeType subscribesToChange (23 → 24): a trigger X on {X}ChangeEvent header mints it declared, and EventBus.subscribe('<X>ChangeEvent' | '/data/<X>ChangeEvent', ...) mints it heuristic, both into the same synthetic CustomObject:{X}ChangeEvent id every CDC reader already scans. A runtime-built channel mints no edge and is counted on the class or trigger as properties.unresolvedSubscriptions. sfi.cdc_subscribers and sfi.event_topology read the new edge first and list a subscriber once even when it also carries triggersOn; both tools' caveats and sfi.event_subscribers' now say only what is still unseen. The isChangeEventApiName rule moved to @sf-intelligence/contracts so the extractors, the graph and the tools share one copy.
  • what_happens_on_save and order_of_execution no 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.notModeled now 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_report reads the org security toggles that change what a god-mode grant means (SecuritySettings:default login-as-any-user, login-access and password policy; SessionSettings:default timeout, 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 in securityContext. A caveat about org settings is printed only when neither settings node is in the vault.
  • sfi.unused_components judges an ApexClass with zero inbound edges through the same liveness predicate as sfi.find_dead_code (entryKindsFor plus the static type-name grep, now one exported staticallyReferencedApex in apex-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 new uncertain array with its reasons instead of as unused, and byType.ApexClass (the count tech_debt_score reads) counts only the classes the predicate clears. On a reference org 14 of 23 "unused" classes were uncertain in the sibling tool; now 0 diverge.
  • Resolvable dynamic Apex is an edge, not a caveat. Type.forName('Demo_Handler') named its class as plainly as new Demo_Handler() and still minted nothing, so a class reached only that way read uncertain (or dead) in sfi.find_dead_code and sfi.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 heuristic references edge with mechanism: Type.forName from 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 as properties.unresolvedDynamicDispatch. find_dead_code reads the resolved edge before the unproven-registration shape, so a framework subclass or Callable dispatched 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.md and the reachability skill now say what is resolved and what is not.
  • downstream_effects now reports a callout effect for every callout:{NamedCredential} reference the Apex extractor already minted, with the credential as the target and a summary.callout count. 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_catalog now lists URL-shaped values stored in Custom Metadata and Custom Setting rows under storedEndpoints (record id, field, URL), read with the same reader lookup_record uses, and reports how many records it read in storedEndpointsScan. 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_field reads 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 a CustomField:Record.* id that names no node, and every custom LWC or Aura bundle a page places by componentName gets a declared references edge (componentPlacement) onto the bundle node that e...
Read more

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 17 Oct 17:51

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 ObjectProvided fields, a Lightning web component with lightning-record-form field bindings, and a Project layout related list with Installation columns, each with a demo question whose answer cites it.

Changed

  • verify-doc-sync now fails when a relative Markdown link in README, CONTRIBUTING, RELEASING, SECURITY, llms-install.md, or docs/** points at a path that does not exist.
  • /sfi-field-audit now 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 a c: prefix there cannot mint a dangling link either.
  • OmniScript save-for-later URL patterns (saveURLPatterns on 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 a writesTo link 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

Choose a tag to compare

@github-actions github-actions released this 16 Oct 20:03

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-*-form field lists, Aura force:recordData, and lightning:inputField / lightning:outputField now produce graph edges when the object can be derived from object-api-name / sobjectType (or when the attribute names Object.Field directly). 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 as unresolvedFieldBindings and do not invent a CustomField edge.
  • coverage_report and health_check now derive the same not modeled type list from summarizeCoverage().notModeledTypes, fixing cross-tool honesty drift on real vaults (scorecard d/e).
  • sfi.what_happens_on_save and sfi.order_of_execution share one disclosure and one trigger-bypass annotation, so both report the same "this trigger may be switched off" warning. sfi.tests_for_change and sfi.test_coverage_for_method share one test-selection walk; direct coverers and trigger-mediated candidates stay in their separate tiers.
  • Flow extractor: Screen <fields> with objectFieldReference now emit readsFrom / writesTo edges to the bound CustomField; extensionName on a screen component emits a references edge to the LWC or Aura bundle. Non-Apex <actionCalls> (emailAlert, quickAction, externalService, flow) emit references edges to WorkflowAlert, QuickAction, ExternalService, or Flow targets 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 usedInLayout edge 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 objectApiName is not renamed onto componentId for Apex-scoped tools such as sfi.crud_fls_audit (regression a security-audit question in the regression bank).
  • Move the org-metadata untrusted-data disclosure from per-response contentPolicy objects to MCP SERVER_INSTRUCTIONS; success envelopes now stamp contentPolicy: "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.cff is 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

Choose a tag to compare

@github-actions github-actions released this 15 Oct 18:36

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's unit-test and build-test jobs locally, read from .github/workflows/ci.yml so 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_object and sfi.field_access_audit remain available through sfi.run_analysis with byte-identical output, or directly under SFI_TOOL_PROFILE=full.
  • The skill-gateway check (scripts/check-skill-gateway.mjs) now derives the core roster from eval/product-manifest.json instead of a hand-mirrored copy that had drifted silently.
  • Shorter contracts for the default tools: sfi.effective_permissions, sfi.why_cant_user_see_record and sfi.safe_to_delete_field descriptions trimmed by a fifth to a quarter; the long form stays available through sfi.describe_analysis { detail: 'full' }. sfi.org_overview now points at sfi.org_card through sfi.run_analysis. sfi.what_happens_on_save trimmed by a third and points at sfi.order_of_execution through sfi.run_analysis.
  • vaultState.freshness.warning is 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 fields builtBy, runningVersion, ageDays, and stale stay on the stamp. Edge-family detail moves to sfi.health_check (freshness.nudge + missingEdgeFamilies) — measured on packages/cli/demo-source via sfi.get_component on CustomObject:Project__c: warning 1044 → 94 bytes.
  • conceptReasoning is now opt-in (includeConceptReasoning: true) on sfi.get_component, sfi.field_360, sfi.what_happens_on_save, and sfi.explain_apex_method. Default responses carry conceptReasoningAvailable: true plus a one-line hint naming the flag instead of ~5 KB of nested completeness/coverage/trust around a few short claims.
  • Root pnpm test builds all packages before running unit tests; pnpm test:all adds 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 unless SFI_ALLOW_NO_BLOCKLIST=1.

Fixed

  • find_dead_code now greps production Apex source before reporting a class likely_dead (not only definitely_dead). A class reached only from tests but still named in production code as a type (List<Other>, a static field, or JSON.deserialize(..., List<Other>.class)) is downgraded to uncertain, 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, or ApexClass__c now mint a heuristic references edge 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 references edges now target VisualforcePage:{name}, matching the VisualforcePage extractor node id (previously ApexPage:{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 modeled callsApex edges (AST when present), not a regex-only / no-AST scanner.
  • cdc_subscribers and event_subscribers: both now state the same EventBus truth — Platform Event subscribe is listensTo; Change Event subscribe is parsed but not yet an edge.
  • endpoint_catalog: notCovered no 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

Choose a tag to compare

@github-actions github-actions released this 10 Oct 20:41

Added

  • safe_to_delete_field lists 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 as alias:Field__c. These never become edges, so the verdict could read safe while 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 a safe verdict to review. The object is not verified, and the platform does not block the delete on these matches. The scan has its own name-match row in checkedCategories. A match from a component that already references a same-named field on another object names it in alsoModeledOn, since the mention is probably that field.
  • why_field_changed lists 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 marked name-only. safe_to_delete_field lists them too: an exact match blocks the delete (it must be removed first), a name-only match is review.
  • safe_to_delete_field returns checkedCategories: every category it checked, including the empty ones, with found / 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 as not-checked and is named in coverageCaveat. The name scan row is not-checked for a family the vault holds but whose source it cannot read (e.g. a DataPack export).
  • why_field_changed lists DLRS rollups as writers, with a rollup block 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_execution and 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__LookupRollupSummary2 custom metadata record becomes a writesTo edge into its target field and readsFrom edges onto the child fields it aggregates, filters, relates through and orders by, each stamped with active and calculationMode.
  • omitted-sharing-on-entry-point code-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, replacing no-sharing-declared-on-entry-point (which called a Lightning-only controller high), and the catalog's regex copy is not shown beside it. Guest-exposure findings name it as a security rule.
  • endpoint_catalog.apexCallouts. Lists Apex setEndpoint(...) 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, and summary.unauthorizedCalloutHosts lists hosts that no active remote site covers. Mock endpoints in @isTest classes are skipped.
  • "Is this deployment risky?" can now be answered from a conversation. sfi.review_change takes the change set the way you have it: packageXml and destructiveChangesXml (the manifest bodies; destructive members are deletions), sourcePaths (changed force-app/... files, or git diff --name-status lines such as D\tforce-app/...), or components given as ids, bare api names, or rows with an optional changeKind. 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 of export_manifest's type table (ApexPage, ApexComponent, CustomMetadata, Settings, and the Layout member 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 new inputResolution.unresolved block 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 a changeKind is reviewed as modified and counted.
  • deployDecision on every review. It is no-go, review-first or go, with the reasons behind it, and it is derived from the review's own tallies. It is never go while 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 with includeCollisions: false.
  • automationCollisions on the review. It reuses sfi.automation_collisions on 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 with includeCollisions: false.
  • who_can_access_object now says what its filters left out. With accessLevel or principalType set, the response carries excludedByFilter (count, byReason, up to 5 examples per reason) for principals that still reach the object: read-only (Read / View All only), record-sharing-only, other-capabilities (for example Create without Edit) and other-principal-type. The boundaryNote names the counts. Before this, "which permission sets can edit X" dropped a View-All-only permission set without a word.
  • endpoint_catalog Apex callouts carry asyncContext and credentialCandidates. A call site inside a @future method is marked future at method granularity, with allowsCallouts read from callout=true. A site in a Queueable or Database.Batchable class is marked at class granularity, with allowsCallouts read from Database.AllowsCallouts. A dynamic site in a class that builds 'callout:' + name now 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 always heuristic: the literal is there, but the call that passes it is not traced.
  • what_happens_on_save with event: 'upsert' labels each step with firesOn (insert, update or 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_360 format: "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 from what_happens_on_save and 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 / maxRowsPerSection with this format is a named invalid-query, because they shape the default sections format only.
  • what_happens_on_save and order_of_execution return a reentry block, the "why did my flow run twice" an...
Read more

v0.3.3

Choose a tag to compare

@github-actions github-actions released this 03 Oct 20:58

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 a remoteClass calls 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's isActive is the runtime switch; a DataMapper's active flag 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 with DEAD_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) and omni_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) and how_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 the org-kb folder or the folder that holds it. gaps report --vault counts only that vault's gaps.
  • App scope for counting tools. picklist_integrity_scan and doc_coverage_report accept scope {namePrefixes, namespaces} (default: org-kb/config/app-scope.json, else the OmniStudio appScope) and answer with the scoped number, the org-wide number beside it as a labelled contrast. picklist_integrity_scan now reports how every picklist is restricted (global value set / inline restricted / inline unrestricted / unknown).
  • The field extractor records a picklist's restricted flag and a lookup's deleteConstraint; 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 real vlocity_cmt / vlocity_ins / vlocity_ps / omnistudio prefix) 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 carry sourceFormat: vlocity-datapack and cite the DataPack file; a snapshot fingerprints the whole DataPack folder, so omni_changed_since sees an edit to a sibling file; other DataPack types are disclosed as skipped, and the refresh never deletes an export. OmniStudio tools say in trust.limitations when an installed Vlocity package has no export (its components are missing, not absent) and how many components came from one. Export recipe in docs/configuration.md.
  • Apex that runs OmniStudio is a caller in the graph. <ns>.IntegrationProcedureService.runIntegrationService('Type_SubType', …) and <ns>.DRGlobal.process(…, 'Bundle') / processObjectsJSON with a literal key (namespace omnistudio, vlocity_cmt, vlocity_ins or vlocity_ps) become dispatchesOmniAction edges 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 phantom ApexClass:IntegrationProcedureService / DRGlobal or a CustomField:<ns>.<Class> read. A key built at runtime is not guessed. New concept apex-runs-omnistudio.
  • The managed runtime's IntegrationProcedureService is an IP call, not Apex. A Remote Action on <ns>.IntegrationProcedureService dispatches to the Integration Procedure its method names (dispatchesOmniAction, via: runtime-service); the save and prefill traces follow it into that IP, and integration_procedure_chain lists it as an integration-procedure endpoint.
  • 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 active not being a runtime switch, managed-package OmniStudio records).

Changed

  • integration_procedure_chain walks 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 and omniscript_flow's Apex disclosure now says what is and is not modeled.
  • The OmniStudio formula evaluator computes CONTAINS and returns computed values, not only truth.
  • The Integration Procedure extractor compares element types through the catalog, so an HTTP Action callout is recorded like a Rest 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 reaches safe_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...
Read more

v0.3.2

Choose a tag to compare

@github-actions github-actions released this 24 Aug 19:57

Added

  • title and websiteUrl on the MCP Registry record. packages/cli/server.json — the manifest the release workflow publishes to the official MCP Registry on every v* tag — carried name, description, status, repository, version and packages, and nothing else. The published record had no human-readable name beyond the reverse-DNS id io.github.PranavNagrecha/sf-intelligence, and the GitHub repo as its only link; the documentation site appeared nowhere in it. The record now sets a title and websiteUrl (https://sfi.auditforce.cloud). 0.3.2 is the first release that PUBLISHES them: the fields were added 70 minutes after the v0.3.1 tag 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 mcp wrote its "run sfi 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 exposes sfi.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 init now requires --target-org when 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's path-portable. The rule it encodes: the number of correct spellings is one. toPosixPath (unconditional) and toRelativePosix (host-gated) are deliberately kept distinct — the vault's sourceTreeHash depends 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 that rmdir without 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 / --exclude filters. Genuinely POSIX-only fixtures (a #!/bin/sh script, a 0600 file mode, a COMSPEC-stubbed win32 simulation) are describe.skipIf(process.platform === 'win32') in their own source, where a reader can see them; the rest now derive their expectations from node:path instead of hardcoding a POSIX rendering.
  • New pnpm check:portability gate (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 of tools/list — and where they disagree, a host that validates arguments before sending them refuses calls the handler would have served. Three were flagged on the required axis: sfi.get_component advertised id as required while the already-advertised componentId alias satisfies it through the preprocess, sfi.explain_error advertised errorText while error / message / errorMessage / text each satisfy it, and sfi.explain_debug_log advertised logText while debugLog / log / text / content do. 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 with fieldId at the validator) on sfi.explain_field and sfi.field_access_audit, eventApiName on sfi.event_subscribers, staticOnly on sfi.unused_fields_deep, and the object selectors on sfi.layout_assignments and sfi.automation_collisions. sfi.automation_collisions was the worst of them: its advertised schema paired additionalProperties: false with required: ['object'], so componentId / objectApiName / objectId were not merely invisible — the advertisement declared them INVALID. Seven required lists shrank in all, not three, because advertising an alias is what makes the old key omittable: required: ['fieldId'] came off sfi.explain_field and sfi.field_access_audit, required: ['componentId'] off sfi.layout_assignments, required: ['object'] off sfi.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 Schema required cannot 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 advertised objectId, which the ADR-007 id-naming gate reads as fresh drift — grandfathered in response-consistency-baseline.json beside the 21 sibling tools that already carried it.
  • retrieveConfirmed was 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. stampFamilyEpochs carries retrievedAt and epoch forward across a --no-pull rebuild and silently dropped retrieveConfirmed alongside 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 a retrieveConfirmed this pass set itself, an error it found, or a pending a decorator forced. Delivered in the same squash:
    • pending meant 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 wrote pending: true beside a non-zero retrieved — a family with 388 items actually retrieved read as if nothing had run. A separate capped state now carries that case, with its own bucket in sfi.coverage_report. Downstream behaviour is deliberately unchanged: a capped family is still excluded from covered, still folded into missingCoverage, and every absence caveat still fires — only the REASON is now legible. A --no-pull or scoped refresh with no cap evidence of its own falls back to the previous manifest's reportsCap block rather than regressing the row to the fold-erased default.
    • ConditionalContext was required coverage it can never satisfy. It is a synthetic node the extractor mints while parsing a firer's condition — never a family sf project retrieve pulls — so buildCoverageEntries never writes it a coverage row. Naming it in VALUE_LITERAL_READER_COVERAGE made that list permanently unsatisfiable, poisoning missingCoverage for its two consumers, sfi.value_change_audit and sfi.what_if_remove_picklist_value: every answer hedged against a gap no refresh could ever close. Replaced by the firer families that actually produce firesWhen edges — ApprovalProcess, AutoResponseRule, AssignmentRule, EscalationRule join the WorkflowRule / ValidationRule / Flow already listed. Both consumer fixtures had hand-copied the list and invented a ConditionalContext coverage row no real vault has, which is exactly how it went unnoticed; they now import the shared constant.
    • sfi.find_component_usages counted a component's own declaration as evidence something uses it. The grep supplement matched a class's own class Foo line exactly as a caller's reference would, so a component with zero graph referrers and one grep hit — its own declaration — reported hasStaticEvidence: 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 before matchCount and hasStaticEvidence are computed, counted in a new selfMatchesExcluded, and named in boundaries[] so "grep ran and found only its own declaration" cannot be read as "grep ran and found nothing".
    • sfi.who_can_access_object shipped half a page and called it whole. The handler sized its page to limit but not to the response byte budget, so the envelope's blind tail-truncation cut granters out from under the already-computed hasMore / truncated: 218 real rows delivered as 109, with hasMore and truncated both false, so nothing told the caller there was more to page for. The complete data payload is now fit to the budget by binary search before the envelope sees it, so hasMore / truncated / offset describe the rows actually shipped. Sharing-rule rows also gained sourceRuleId: via names the rule TYPE, not the rule, so two different rules of the same type sharing with the same principal collided...
Read more

v0.3.1 — the tools audit themselves

Choose a tag to compare

@PranavNagrecha PranavNagrecha released this 22 Aug 14:53

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

Choose a tag to compare

@github-actions github-actions released this 08 Aug 14:26
8a6b359

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:check enforces
    packages/cli files as 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.json plus
    sf-intelligence-qa/scripts/verdant-truth.mjs (inventory + design-goal tool
    pins + mutation self-test). Wired as harness:verdant-truth in 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_component and sfi.resolve add additive labelOrgText /
    descriptionOrgText fields; the MCP dispatcher stamps contentPolicy on
    success envelopes so hosts treat org strings as data, never instructions or
    consent. Markdown escaping remains a renderer concern
    (escapeMarkdownInline exported from @sf-intelligence/renderers).
  • The zero-friction demo is now discoverable. npx -y sf-intelligence demo serves a synthetic org with no Salesforce auth, no sf CLI and nothing to configure, but it appeared on only 2 of 34 site pages and in zero occurrences across llms.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 in llms.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 while changefreq and priority (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, Person and ImageObject nodes on every page, so a graph is actually bound to the URL being parsed. Fixes /mcp emitting a typed-but-empty SoftwareApplication — @id scope 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, datePublished and dateModified are now threadable from any page through DocPage and Base.
  • 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
    (EvidenceEnvelopeV2 in @sf-intelligence/contracts) for claims, evidence,
    coverage, freshness, pagination, and absence verdicts. Opt-in projection
    under data.evidenceEnvelope on sfi.interpret and sfi.safe_to_delete_field
    (legacy keys unchanged). Runtime assertEvidenceEnvelopeV2 guards those
    handlers; not applied roster-wide.
  • Retrieval ledger + family epochs (AUDIT-F5). Coverage rows carry per-family
    retrievedAt / epoch (preserved across scoped --types refreshes). Refresh
    writes meta/retrieval-ledger.json and appends meta/tombstones.jsonl for
    confirmed reconcile deletions (never on refuse). TrustSummary.freshness
    can disclose overall: 'mixed' with families / oldestEvidenceAt.
    sfi.coverage_report surfaces tombstones + mixed-freshness limitations.
  • Core-by-default + strict invocation (AUDIT-F6). Default SFI_TOOL_PROFILE
    is core (19-tool spine, incl. sfi.live_consent). Direct tools/call outside the advertised set is
    denied under core — use sfi.run_analysis (target must be a registered tool).
    sfi.describe_analysis gains progressive detail (summary | schema |
    full; default summary under core). Set SFI_TOOL_PROFILE=full for 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 so tools/list and list_analyses one-liners read as product
    jobs. list_analyses now omits hidden retired aliases (same advertise
    contract as tools/list; still invokable via run_analysis). Structural
    consolidations (−4 hidden aliases) were already shipped; further handler
    merges deferred.
  • ProductManifest / sfi.capabilities report defaultProfile: 'core', activeProfile, and an advertised count that matches tools/list under the active profile (full roster remains under profiles.full).
  • SERVER_INSTRUCTIONS and capabilities routing guidance teach the core profile + run_analysis gateway; they no longer tell hosts to call sfi.interpret / sfi.live_consent directly or to use liveEnabled: true as consent.
  • Skills / agents / commands under default SFI_TOOL_PROFILE=core now
    instruct hosts to invoke non-core analyses through sfi.run_analysis
    { name, args } (Decision 2=C). Shared grounding footer + entry skill teach
    the gateway; pnpm skill-gateway fails CI on direct non-core Call/Fire
    instructions. llms.txt / llms-full.txt and .claude-plugin/plugin.json
    pin sf-intelligence@0.3.0 (gated by verify-doc-sync); the website's own
    install snippets stay unpinned so npx -y resolves latest. Every surface
    states core as the default, not full.
  • SBOM generation uses @cyclonedx/cdxgen via pnpm sbom (pnpm-aware,
    fail-closed). Tag publish attaches a non-empty CycloneDX 1.5 artifact or
    fails the job — no more empty npm sbom skip.
  • New article: "What does this Salesforce Flow do?" — reading a Flow you did not build. Documents that the concept model does not extract triggerOrder and 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-field and /blog/delete-unused-salesforce-fields were competing for the same query with overlapping FAQPage questions. The use-case page is narrowed to the product surface and its FAQPage block removed; the blog keeps informational intent. Sitewide there are now zero duplicate FAQ questions across 15 pages and 58 questions.
  • /use-cases/sharing-troubleshooting expanded from ~230 to ~1,450 prose words, and a wrong tool name (why_cant) corrected to why_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 unrouted to funnel-advisory, topped by sfi.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 new FUNNEL_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_SCORE stays 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 reaches compare_components, "can you help me out" still reaches doc_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.yml fires on tag push; ci.yml fires 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. cdxgen was pointed at packages/cli, which has no pnpm-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 ...
Read more

v0.2.5 — field-audit surface + delete-safety edge fixes + refresh data-loss fixes

Choose a tag to compare

@PranavNagrecha PranavNagrecha released this 29 Jul 19:10
b46af62

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)

  • reconcileSourceDeletions deleted 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-dir modern sf rejects. 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 — retrieved was counted after the usage fold drops those nodes, yet retrieveConfirmed: true was 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.