Skip to content

Releases: PranavNagrecha/Salesforce-Intelligence

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 safereview 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.

v0.2.4 — 209 tools · 9 new surfaces + 6 fixes

Choose a tag to compare

@PranavNagrecha PranavNagrecha released this 24 Jul 04:50

Nine new analysis surfaces + six correctness/honesty fixes. The read-only tool roster grows 203 → 209. Deterministic and offline throughout; every new surface ships its honesty boundaries and confidence tiers. No new dependency.

✨ Added

  • flow_bulkification_audit — flag DML / Get-Records inside Flow loops, plus filterless Get-Records (the Flow-side sibling of governor_limit_risks).
  • picklist_integrity_scan — org-wide picklist value-set integrity: declarative literals (VR / formula / Flow / WFR) referencing orphaned or inactive-only values.
  • limit_headroom_report — offline metadata-vs-ceiling headroom, ranked worst-first (the Optimizer limit-report replacement).
  • doc_coverage_report — documentation-gap meter over description + inlineHelpText, edge-degree weighted, lowest-coverage-first.
  • permission_set_consolidation — redundant / duplicate / strict-subset permission-set merge candidates from declared grants.
  • nonselective_soql — the first index-aware Apex static analysis: SOQL WHERE shapes that risk a full table scan at scale.
  • automation_risk_reportmode: 'sprawl' — org-wide, per-object automation-density ranking ("where is sprawl worst first").
  • review_changecheckAccessParity — flags added fields/objects that resolve to zero grants and would "ship for nobody".
  • Cited remediation on interpret / synthesize_answer — dependency-ordered fix steps filled from the same grounded ids, stamped with the claim's own confidence (11 of 193 rules). Refuses counterfactual closure.

🔧 Fixed

  • safe_to_delete_field — polymorphic Activity / Task / Event shared-field write attribution (no more false safe from a lost writesTo).
  • PII classifier — protected-class + securityClassification recognition.
  • get_impact — soundness no longer over-claims complete: true.
  • ApprovaluserHierarchyField related-user approver resolution.
  • call_graph — phantom-node filtering (a local Map var is no longer minted as a phantom ApexClass).
  • Live staleness — millisecond-precision threshold fix, removing a ~1s false-positive drift window.

🎨 Also

  • Full website SEO overhaul and refreshed brand assets (README hero + GitHub social-preview card), aligned to the canonical "Salesforce Org Intelligence for AI agents" positioning.

📦 npm: sf-intelligence@0.2.4 — published with provenance (keyless OIDC Trusted Publishing)
📄 Full detail: CHANGELOG.md
🔒 Read-only · offline · source-available · MIT + Commons Clause

v0.2.2 — reasoning concept model 94→142 concepts / 143→193 rules

Choose a tag to compare

@PranavNagrecha PranavNagrecha released this 22 Jul 16:23

[0.2.2] — 2026-07-22

Concept-model expansion + grow-forever funnel routing. The org-independent
Concept Model behind sfi.interpret grows from 94 concepts / 143 rules to
142 concepts / 193 rules (+48 concepts / +50 rules) — every addition grounded
against an already-extracted metadata property or edge, each with a firing
interpret() seed proof, all curated general-Salesforce truth joined at query time
against the grounded vault slice (no org data in the model). The semantic funnel is
reworked so sfi.interpret is scored as the maximum over a base card plus one
independent per-concept card
, removing the single-document saturation ceiling:
adding a reasoning concept no longer dilutes existing ones, so the model can grow
without bound while every prior query keeps its exact rank. Deterministic and offline
throughout — no LLM, no live org read, no new dependency, no package-weight change.

Added

  • Reasoning model — eight new offline concepts (96 → 104 concepts, 145 → 154 rules).
    A parallel discovery pass surfaced, and this change ships, eight additional
    org-independent structural-implication concepts behind sfi.interpret, each grounded
    on an already-extracted node/edge property with no new engine primitive and no
    extraction change:
    • concept:duplicate-rule-bypass-sharing-match — a duplicate rule whose
      securityOption is BypassSharingRules runs its matching in system context,
      comparing an incoming record against records the running user cannot see.
    • concept:duplicate-rule-references-inactive-matching-rule — an active duplicate
      rule that references a matching rule whose ruleStatus is not Active performs no
      detection on that matcher: dead duplicate protection that silently lets duplicates save.
    • concept:approval-process-final-lock-record-readonly — an approval process with
      finalApprovalRecordLock / finalRejectionRecordLock leaves the record locked
      read-only after it completes, so later user edits and automation updates fail until
      it is unlocked.
    • concept:record-type-inactive — a record type with active=false is not
      assignable to new records; excluded from layout / business-process routing reasoning.
    • concept:remote-site-setting-protocol-security-disabled — a remote site setting
      with disableProtocolSecurity=true permits non-HTTPS outbound callouts to its host
      (isActive gates whether it applies at all).
    • concept:apex-intentional-system-mode-dml — Apex DML issued with an explicit
      AccessLevel.SYSTEM_MODE argument deliberately opts out of the running user's object
      CRUD and field-level security for that write. Surfaced as a review surface, honestly
      NOT a proven defect (a system-context write is often correct); heuristic, from
      tokenized source.
    • concept:field-longtext-richtext-not-filterable — Long Text Area and Rich Text
      (Html) fields cannot appear in a SOQL WHERE / ORDER BY / GROUP BY, a list-view
      or report filter, and cannot be an external id or unique field.
    • concept:dataraptor-field-security-unenforced — a DataRaptor
      (OmniDataTransform) with fieldLevelSecurityEnabled=false reads/writes SObject
      fields without enforcing the running user's FLS.
      Deterministic and offline throughout — cited groundedIn, confidence-tiered claims, no
      LLM, no live org read. Each ships with a firing interpret() seed proof.
  • Reasoning model — two new offline concepts (94 → 96 concepts, 143 → 145 rules).
    sfi.interpret now recognizes two additional org-independent structural-implication
    concepts, each grounded against already-extracted metadata with no new engine primitive:
    • concept:validation-rule-inactive — a validation rule whose active flag is false
      never evaluates its error-condition formula, so it can neither block a save nor surface
      its message; it is excluded from save-order / save-failure reasoning and required-field
      gate counts. Names the non-enforcing structural fact only (the formula is not evaluated
      offline).
    • concept:workflow-rule-inactive-dead — an inactive workflow rule is dead legacy
      automation whose field updates, alerts, outbound messages, and time-dependent actions
      never run; it is excluded from save-order and automation-impact counts. Does not claim a
      Flow has replaced it (migration lineage is org-specific).
      Deterministic and offline throughout — cited groundedIn, confidence-tiered claims (all
      declared), no LLM, no live org read. Each ships with a firing interpret() proof and
      natural-language funnel hooks so the concepts are reachable from sfi.interpret's top-5.
  • Reasoning model — five more offline concepts (104 → 109 concepts, 154 → 159 rules),
    all NL-reachable via the grow-forever funnel.
    Each grounds on an already-extracted
    property with no new engine primitive, ships a firing interpret() seed proof, and gets
    its own funnel card so it ranks sfi.interpret top-5 for its natural questions without
    diluting any existing concept:
    • concept:field-classic-encrypted-text — a classic Encrypted Text field is masked
      for users without "View Encrypted Data" and is not filterable/sortable/groupable, nor an
      external id / unique / formula input (distinct from Shield Platform Encryption).
    • concept:field-autonumber-system-assigned-readonly — an Auto Number field is
      system-assigned at insert, read-only on every write path, null in before-save context,
      and stored as a formatted string.
    • concept:field-multiselect-picklist-storage-semantics — a multi-select picklist
      stores selections as one semicolon-delimited string, so SOQL/reports must use
      INCLUDES/EXCLUDES (not =/IN) and it cannot be a dependency controlling field.
    • concept:permission-set-license-scoped — a permission set bound to a specific user
      license is only assignable to users who hold that license.
    • concept:session-based-permission-set-dormant — a session-based permission set
      grants none of its permissions until it is session-activated; its grants are dormant
      otherwise.
      The last two were shipped-then-dropped earlier this cycle as funnel-losers (their query
      space is shared with permission-set specialist tools); the grow-forever per-concept-card
      funnel makes them independently reachable, so they are revived. Deterministic and offline
      throughout — cited groundedIn, confidence-tiered claims, no LLM, no live org read.
  • Reasoning model — fourteen more offline concepts (109 → 123 concepts, 159 → 174
    rules), all NL-reachable via the grow-forever funnel.
    A parallel design pass (one
    grounding-verifier agent per candidate) produced these; each grounds on an
    already-extracted property/edge with no new engine primitive, ships a firing
    interpret() seed proof, and gets its own funnel card:
    • field-restricted-global-value-set — a picklist bound to a global value set marked
      restricted is a closed vocabulary (out-of-set writes rejected; edits ripple to every
      consumer).
    • field-picklist-has-retired-values — a picklist retaining deactivated
      (isActive=false) values keeps them on existing records though they are no longer
      selectable.
    • approval-process-inactive-dead — an inactive approval process cannot be submitted to.
    • escalation-rule-time-deferred — an active escalation rule's actions fire from a
      background time-based process, not synchronously on save.
    • auto-response-rule-first-match-starvation — a catch-all auto-response entry ordered
      before specific entries starves them (first-match).
    • record-type-business-process-binding — a record type naming a business process
      constrains the stage/status picklist for its records.
    • apex-dynamic-reflective-surface — dynamic/reflective Apex (dynamic SOQL, describe
      reflection) is an analysis blind spot and injection surface (heuristic).
    • named-credential-merge-fields-injectable — a named credential allowing merge fields
      in header/body can interpolate record/user data into outbound requests.
    • connected-app-saml-sso-federation — a SAML-protocol connected app is an inbound SSO
      federation trust surface.
    • apex-fake-assertion-test — a test with tautological assertions inflates coverage
      without verifying behavior (heuristic).
    • entitlement-process-inactive — an inactive entitlement process applies no SLA
      milestones to new entitlements.
    • omnistudio-inactive-component-version — only the active version of a versioned
      OmniStudio component is invoked at runtime.
    • required-field-absent-from-all-layouts — a required field on no page layout cannot
      be supplied through the UI, so UI inserts fail (absence-shaped).
    • dataraptor-errors-ignored — a DataRaptor with Ignore Errors continues past
      row-level failures rather than aborting.
      Grounding for every concept was independently verified against the extractor source before
      integration. Deterministic and offline — cited groundedIn, confidence-tiered claims, no
      LLM, no live org read.
  • Reasoning model — nineteen more offline concepts (123 → 142 concepts, 174 → 193
    rules), completing the concept-model build-out.
    A parallel mining pass (one agent per
    extractor family) surfaced these by finding EMITTED node/edge properties that no concept
    bound yet; each grounds on a verified-emitted property, ships a firing interpret() seed
    proof, and is NL-reachable via its grow-forever funnel card. By family:
    • Fields / objectsobject-deployment-status-in-development (In Development objects
      hidden without Customize Application), object-autonumber-name-field (auto-numbered
      record Name), global-value-set-has-inactive-value / standard-value-set-has-inactive-value
      (deactivated values retained...
Read more

v0.2.0 — capability expansion + correctness hardening

Choose a tag to compare

@PranavNagrecha PranavNagrecha released this 13 Jul 18:15

sf-intelligence 0.2.0 — the biggest release since 0.1, focused on new capability breadth, correctness, and protocol modernization. Offline, read-only, MCP-first knowledge base for one Salesforce org.

Added — 24 new tools (176 → 196 advertised)

  • Agentforce / AI exposure audit (GenAI + Bot extraction), guest + Experience Cloud exposure modeling
  • review_change PR gate — cross-vault diff with SARIF output + a reusable GitHub Action
  • query_graph power-user graph query, permission-set what-ifs (what_if_assign/revoke_permset)
  • 8 new live_* runtime tools (SetupAuditTrail history, security exposure, scheduled jobs, …)
  • explain_error / explain_debug_log — paste an error/log, get the component that produced it
  • Field-level Flow dataflow lineage, generate-fleet-report, ~24 new component types

Fixed / hardened

  • Profile-grant retrieve regression fixed; Profile now co-batched through retrieve splits with an explicit bare-profile disclosure
  • canonicalJson crash class, case-variant dangling edges, N+1 query batching, muting permission-set subtraction

Router & protocol

  • Trigger-gated de-crowd of the two what_if permission-set tools (+0.5pp top-5 advisory recall, zero losses, live-confirmed)
  • MCP protocol: readOnlyHint/openWorldHint annotations, structured output, prompts
  • Windows + macOS CI; npm Trusted Publishing (keyless OIDC, no token)

Full release gate green (60/60). Advertised roster = 196 distinct tools (200 registered incl. 4 back-compat aliases). See CHANGELOG.md for the complete [0.2.0] entry.

npm i sf-intelligence

v0.1.25 — description capture

Choose a tag to compare

@PranavNagrecha PranavNagrecha released this 06 Jul 19:13

Headline: the org's <description> text is now captured and queryable. Four
metadata types silently dropped their descriptions; now every type that carries
one keeps it, and a new missingDescription filter answers "which reports /
objects / permission sets are undocumented?" — previously an honest gap.

Upgrade note: the description is captured at extraction time, so run
sfi refresh once on 0.1.25 to backfill descriptions into an existing vault.

Added

  • Description capture across every metadata type that carries one. The
    org's top-level <description> is now extracted into node.properties.description
    for the four generic metadata types that previously dropped it — Report,
    Dashboard, ReportType, and PermissionSetGroup (extractReport,
    extractDashboard, extractReportType, extractPermissionSetGroup in
    enterprise-metadata.ts now pass extraProperties: ['description']). The
    seven custom extractors (CustomObject, CustomField, PermissionSet,
    Profile, Flow, ValidationRule, RecordType) plus CustomTab and
    CustomApplication already captured it. The description renders as a paragraph
    in the component markdown (unchanged renderer). Verified offline against a real
    ~1,300-object source tree: captured counts match the source's
    files-with-<description> ground truth exactly for every generic type (Report
    221, Dashboard 17, PermissionSetGroup 16, plus CustomField 1315, ValidationRule
    306, PermissionSet 159, RecordType 86, CustomObject 75). Only the genuine
    top-level <description> is captured — nested element-level descriptions (e.g.
    a Flow decision's own <description>) are intentionally excluded, and Profiles
    capture their single real top-level description with no fabrication.
  • list_components documentation-coverage filter. New missingDescription
    / hasDescription boolean flags answer "which reports / objects / permission
    sets / validation rules have no description?" — previously an honest gap. Backed
    by a descriptionPresence: 'present' | 'absent' narrow in the graph layer
    (queries.ts) that folds key-absent, JSON-null, and empty-string all into one
    honest "absent" bucket via coalesce(json_extract_string(...,'$.description'),'').
    The narrow is applied to both the page query and the authoritative
    countNodesByType total; the two flags are mutually exclusive (invalid-query
    guard). Honesty caveat, disclosed in the tool description: for a type whose
    source carries no <description> element at all (ListView, CustomPermission,
    MutingPermissionSet, CustomMetadata), missingDescription matches every
    node — the answer means "no description in this metadata type", not "left blank".

v0.1.24 — assignment-data engine + honesty R4

Choose a tag to compare

@PranavNagrecha PranavNagrecha released this 02 Jul 19:05

Headline: assignment-data engine + router honesty R4 + experimental embeddings.
Three simultaneous workstreams: four new live tools close the "who holds X / who's in
Y" honest-gap family; the router's fourth honesty round adds write-evasion hardening,
forecast/authorship gaps, narrowed clarification, and show-me candidate coverage; and
an opt-in RRF hybrid embeddings layer sits behind a feature gate for early adopters.
Tool count 172 → 176.

Measured on the maintainer's two real-org suites (2,000 + 2,995 primary
questions, 12,352 turns, 0 route errors), 0.1.23 → 0.1.24:
honesty rose on
both — declined-correctly 82.9 → 83.8% (2K) and 57.2 → 66.9% (3K, +9.7)
while over-routing fell on both (2K 89 → 88; 3K 229 → 185, −44), reversing
the eagerness regression 0.1.23 introduced. The injection/write-evasion family
dropped from 8 leaks to 1 (a read-only report with execution skipped, unchanged
since 0.1.23); the curated write-execution set is 24/24 refused. Answer-recall
held (2K 82.9%, 3K 80.2%), recall@3 61.1, needs-live 76.9 → 78.1%. The recall
gain came from precision this round, not more eagerness — the opposite of the
0.1.23 trade.

Added

  • sfi.live_permset_holders — who HOLDS a permission set, permission set
    group, or profile (kind: permissionSet | permissionSetGroup | profile | auto), answered from the live org. PSG-trap-aware: direct holders and
    via-group holders (PermissionSetGroupComponent) are reported separately
    with a deduped effectiveTotal, so the count is audit-grade instead of
    confidently understated. True count first, expired assignments excluded and
    disclosed, 500-row cap with byte-fit that never understates totals, keyset
    paging (afterId/nextAfterId), optional per-profile buckets. This also
    answers the name-by-name profile roster family.
  • sfi.live_user_permsets — the REVERSE direction: what a named USER
    holds. Direct permission sets vs via-PSG assignments (with expirations),
    profile named; PermissionSet.IsOwnedByProfile = false is pinned into every
    assignment query so the system profile-owned row never masquerades as a
    direct assignment. Pairs with vault sfi.effective_permissions for a
    dual-provenance answer (live = which grantors; vault = what they grant).
  • sfi.live_group_members — who is IN a queue / public group right now:
    users, nested groups (expanded at most ONE level, fail-closed and stamped
    expansion: 'partial-one-level'), role-based members surfaced as ROLE
    entries (never silently expanded), queue supportedObjects ("can this queue
    own Case"), and a measured vaultDeclaredMemberCount vs
    liveDirectMemberCount drift check.
  • sfi.live_zombie_accounts — active users with login access but ZERO
    permission-set/PSG assignments (single anti-join on
    PermissionSet.IsOwnedByProfile = false; disclosed bounded client-diff
    fallback when an org rejects the anti-join). Output states verbatim that a
    "zombie" still holds everything its PROFILE grants. Optional
    minDaysInactive / includeAllUserTypes. Dormancy-only questions stay on
    sfi.live_inactive_users.
  • All four follow the live-plane contract: consent-gated (sfi.live_consent /
    SFI_LIVE_PLANE_ENABLED / liveEnabled), budgeted
    (SFI_LIVE_QUERY_BUDGET, budget exhaustion is an honest error, never a
    silent fallback), read-only SOQL, provenance: live_org point-in-time
    stamps. No user identifiers land in the vault — the counts-only facts pin
    is untouched.
  • sfi.coverage_report assignmentData section — runtime assignment
    data (User / PermissionSetAssignment / GroupMember) is reported as
    "not in vault by design" (a runtime data object, not a retrieve gap),
    naming the four live tools, current live-consent state, and the counts-only
    facts snapshot presence/timestamp. sfi.health_check carries the same
    block informationally — it never degrades status; a >30-day-old counts
    snapshot earns an advisory only.

Changed

  • Router retargets (same change as the tools — no contradictory gates):
    permset-user-roster ("which users have permission set X") flips from
    honest-gap refusal to sfi.live_permset_holders; profile-user-roster
    drops its partial-answer gap (the name-by-name roster is now built);
    unassigned-permset-groups and permset-group-grants flip partially to
    sfi.live_permset_holders (per-PSG zero-holder check and PSG containment —
    the enumerate-all-PSGs sweep and the 2-hop "which PSG grants custom
    permission X" chain remain disclosed gaps); NEW queue-group-member-roster
    arm ("who's in the Support queue") routes sfi.live_group_members; NEW
    user-permset-holdings arm ("what permission sets does Jane have") routes
    sfi.live_user_permsets + sfi.effective_permissions as an ordered
    dual-provenance pair; empty-queues-groups keeps the vault scan primary and
    appends sfi.live_group_members for runtime verification.
  • The vault-side assignment disclosures (object_access_audit,
    who_can_access_object, and friends) now name the concrete live tools
    ("answerable via the live plane: …") instead of a generic "run the live org
    plane" pointer.
  • Router R4 — honesty + candidate coverage:
    Injection/write-evasion hardening (indirect re-delegation attempts and
    tool-self-capability asks refused with a read-side alternative);
    forecast/authorship honest-gaps (predictive "how will X change by…" and
    authorship/attribution asks return honest gaps naming the nearest real reads);
    narrow clarification re-introduced for genuine same-name collisions that
    the R2b rebalance over-suppressed; show-me candidate coverage (visual/UI
    render requests clarify to the relevant read tool rather than silently
    mis-routing).

Experimental

  • Embeddings hybrid (SFI_EMBEDDINGS=1, off by default) — an opt-in
    RRF hybrid layer that fuses the existing lexical TF-IDF candidates with a
    locally cached neural sentence-embedding model (Xenova/all-MiniLM-L6-v2,
    ~23 MB, downloaded once on first use from HuggingFace Hub into
    .sfi-embed-cache/). Affects candidate ranking only — the honesty/refusal
    decision and the route.tools deterministic plan are untouched.
    Graceful lexical fallback when the model is absent or the embed fails;
    allowRemoteModels is disabled, so the funnel can never phone home at
    query time. Requires npm i @huggingface/transformers in your project (not
    bundled). See docs/configuration.md §Embeddings for full opt-in details.

v0.1.23 — 80% crossed

Choose a tag to compare

@PranavNagrecha PranavNagrecha released this 02 Jul 15:10

[0.1.23] — 2026-07-02

Headline: 80% crossed — candidate generation and honesty seams in the same
release.
Two eval-driven rounds on 0.1.22's architecture. Measured on the
same 2,000-question real-org bank: answer-clean 77.6% → 80.5% raw
(81.0% relabeled), honesty over-routes down 97 → 89 with zero false
refusals
, funnel-blind recall@8 57.3% → 69.8%, follow-ups with host
context 67.0%. Three-release trend on the same bank: 62.9 → 72.0 → 77.6 →
80.5%.

Added

  • Runtime-analytics honest-gap arms — the refusal gate now recognizes the
    unmodeled telemetry families and discloses the gap (naming the nearest real
    reads) instead of routing: per-user login events/sessions/last-login
    rosters, automation execution traces and aggregate run counts, run/failure
    forensics ("the error message from the last time X failed"), CPU/heap
    profiling, debug-log retrieval, SOQL execution plans, message delivery
    counts and sent-message content, site/community click analytics,
    record-level before/after field history, and record-access audit events
    ("who accessed…"). Precision-guarded: dormancy questions still reach
    live_inactive_users, login IP ranges still reach profile_security,
    System.debug code searches still reach search_apex_source, static
    reference counts and save-order questions route unchanged.
  • Run-imperative refusal — "run the X flow against test data for me" /
    "execute the batch job" is refused as refused-write (executing automation
    mutates the org) with a read-side alternative describing what the
    executable WOULD do (explain_flow, scheduled_job_catalog,
    what_happens_on_save by target). Permission/hypothetical frames ("who can
    run…", "what happens if I run…", "how do I run…") route normally.
  • Privilege-escalation injection arm — "sudo …" and grant-to-self asks
    ("give me full/admin access") land refused-injection; READ delivery asks
    ("give me the FLS grant list…") are unaffected.
  • permset-group-grants capability gap — "which PSG grants the X custom
    permission / the Y role" discloses that PermissionSetGroup composition is
    not modeled instead of advisory-routing to permission tools that cannot
    answer it; PSG→permission-set REFERENCE reads stay routed.
  • Gap detection before context continuation — a follow-up that is itself
    gap-shaped (judgment, delivery/export, tool-self-capability,
    deployment-status) never inherits previous.tool; it returns a
    non-executable context-gap-followup route with an honest disclosure.
    Legitimate continuations ("is it safe to delete?", "what about on
    Contact?") are unchanged, and a gap-shaped question WITHOUT context routes
    exactly as before.
  • Business-user register corpus — the funnel utterance corpus grew a
    non-technical phrasing band ("what business process does this flow
    support", "the 10,000-foot view", "which automation is Salesforce going to
    sunset", "a reference sheet for the business team", "who's been making the
    most changes lately") plus matching synonym/idiom bridges (sunset→retire,
    flips→transition, grade→health, "kicks in"→fires, "big picture"→overview).
    Business-user recall on the additions-tuning set rose 60.9% → 76.1% with
    zero regressions.
  • component-type intent — "is a flow or a trigger?", "…has
    'Trigger' in the name but is it actually a test class?", "what type is
    that?" now routes resolve-first with both family explainers; a same-name
    cross-type collision is treated as the ANSWER (resolve enumerates it), not
    an ambiguity block. Type-confusion trap family: 52.2% → 91.3%.
  • Type-confusion premise disclosure — when a type-scoped resolve finds
    nothing ("the X permission set") the premise check now retries UNSCOPED
    before declaring nonexistence: a strong match under a different family
    discloses "TYPE CHECK: X exists as , not as a " and
    keeps routing on the component that actually exists. Pure ghosts still get
    the existence PREMISE CHECK and still never advisory-route.
  • Genuine-tie clarifications restored (post-P4 rebalance) — quoted bare
    labels ("the 'Status' field") now reach entity resolution; a question that
    itself asserts a same-name family ("three different things called Status")
    always clarifies; a bare label the resolver silently picked one of several
    identical same-name parents for ("the 'Concentration' field" ×3 parents)
    clarifies; and a compound vault+live ask ("what breaks AND is it actually
    running in production") returns a two-plane tool-choice clarification.
    Additions-tuning clarify 3/13 → 9/13; the P4 junk-tie suppression is
    unchanged (no previously-clean answer question started blocking).
  • CI self-recall gate — new test (funnel-self-recall.test.ts): every
    candidate-eligible tool must retrieve itself in the pure-funnel top-8 for
    ≥70% of its own utterances, so a tool invisible to the funnel is a CI
    failure forever (measured floor at landing: 77.8%).

Fixed

  • Write-gate impact carve-out — "can you deactivate X safely? I need to
    know what depends on it" is a what-if impact ask, not a mutation
    instruction: safely? and explicit dependency/breakage questions now
    excuse the write-imperative refusal. Bare imperatives still refuse.
  • Shield event-monitoring log retrieval — "show me the event monitoring
    log from last Tuesday" now lands the same runtime-telemetry honest-gap as
    debug-log retrieval (additions-tuning honest-gap 16/16).
  • website/recalibrate.mjs stale-count bug — the capabilities.html
    rewrite rules were spacing/markup-sensitive and silently no-opped when the
    page copy reflowed (the shipped stale "171 tools"). Rules are now
    whitespace- and markup-tolerant, and a post-rewrite tripwire warns on any
    surviving tool-count string that disagrees with the registry.