CoreEdge Console: the seven-phase perfect-solution remediation - #226
Merged
Conversation
Phase 1 of the console remediation plan: no screen may assert a control
state the broker contradicts.
One resolver, one vocabulary. effectiveDecision now resolves REVOKED from
revokedAt (a granting decision only; the deliberate act wins over expiry),
and a sweep test asserts it agrees with the runtime predicates across the
whole (decision x revoked x expired) space. The Studio ledger selects and
renders revocation as the second fact it is; the grants GET returns it;
northbound discovery filters on it with the same nullishness as access.ts
- closing the drift where a grant an admin had withdrawn was advertised
as callable and still read "Approved" in the builder's own ledger. The
agreement test's fixture gap (revokedAt never set) is closed with revoked
and revoked+expired cases.
Ten stale claims purged: five "there is no revocation" (Control Tower
lede, landing page, ExpiryRunway, the server's refusal text, the request
dialog) and five "v1 enforces nothing at runtime" (grants.ts header, the
access page/client, the access-grants route, the ApiAccessGrant schema
doc, the Interfaces audit row). Expiry stays mandatory - revocation is an
emergency action that needs someone watching; the copy now says that
instead of denying the control exists.
Honest-status corrections riding along: the ReadinessScorecard renders
all SEVEN buckets (NOT_FOUND was missing - the same omission class the
component's own comment recorded for `available`, reintroduced); the
StudioStatusChip accepts a `meaning` override so lifecycle chips stop
announcing probe facts ("Restricted - 401 or 403...") to screen readers;
the dead /docs/coreedge-developer-guide.md link is served for real at
/help/developer-guide (session-gated, reads the repo doc at request time,
traced into the function); Ops empty-state copy interpolates the actual
window instead of hardcoding "24 hours"; Control Tower grant lifecycle
counts are database aggregates instead of page arithmetic (revoked and
unbounded now in the payload, the unbounded panel keys off the DB count);
the incident rules stop firing on revoked grants (and lapsed ones, on the
unaccountable-PROD rule) so the feed agrees with the topology; the
governance audit screen gets the window picker its 30-day default has
been waiting for, plus ISSUE/ROTATE in its tone map.
Two false Workbench manual claims fixed at the source: the presales list's
unreachable "awaiting signoff" state is now real (sent + decisions
recorded, via a _count projection - no blobs), and the Affirm list entry
stops claiming an organization scope the table does not have.
Guard rails so the class cannot recur: manual-truthfulness gains runtime-
enforcement denials and a write-credential entry that arms itself the day
issuance ships; a new governance-claims test pins the corrected screen
prose by presence, the direction that cannot false-positive on the
comments that narrate the old claims.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEq89i8SZpWXPbDXoaRvX8
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…or honestly none Phase 2 of the console remediation plan, and the fix this branch is named for: every connection type rendered the S/4 Public catalogue with identical counts, because ?product= selected which tenant to probe and never which catalogue to show. One scope resolver (hubCatalogueScope): S/4 editions filter on the edition column their importer has always populated; SuccessFactors and Ariba - not editions of anything - select by SAP's verbatim product tag; ECC returns a deliberate "none" with the Gateway-discovery explanation instead of a confident wrong list. All eight query sites in hub-content, plus probe-all, resolve-hub-service and the /capabilities dynamic top-up (which hardcoded edition PUBLIC while the field built for it went unread) now go through it. Dynamic probe sampling is edition-only, so an SF tenant is never again fired at with manufactured /sap/opu/odata/sap/* URLs that 404 by construction - its curated services are its probeable surface. The catalogue header names the real product; Discover renders an honest no-tenant state instead of falling back to an s4hana label. Product identity survives import now. normalizeHubRowForType derived nothing and stamped every harvested artifact appliesToPublic=true - 500 Private-edition BAdIs and every cross-product integration labelled S/4 Public. The edition classifier moved out of the import script into src/lib/sap-public/edition-tags.ts (script re-imports it; its tests keep working), the importers classify each row's product tag and persist the verbatim string on the new SapHubContent.productTags column, and the admin Rebuild projects every addressable SapApiReference row with its real flags instead of Public-only. Operator re-import step documented in the ingest runbook. The sap-client mechanism is live end to end: toSapTenant no longer drops client/environment (the lossy projection every stored-connection read went through), deployment tenants can declare a client in TENANTS_JSON / TENANT_CLIENT, and a propagation test pins every hop to the URL builder that was already waiting for the value. SuccessFactors SAML, doubly broken against SAP's 20 Nov 2026 Basic-auth removal, works: coerceAuthType accepts oauth-saml-bearer (it threw on the type the rest of the product accepted, breaking the connections list and every northbound read for any org that stored one), and openSecrets returns samlAssertion/companyId instead of silently dropping them. A resolver that still cannot read a row refuses with the new CONNECTION_UNREADABLE binding reason - audited and counted by the incident coverage map - instead of escaping as an unaudited 500. Interfaces stop being three-product-only: sapProduct validates against getSapProduct, so RISE/on-prem/ECC connections can finally carry interfaces, grants and credentials; the connections API accepts the Ariba tile its own picker offers (the reconciliation test now checks both directions); InterfacesClient renders the product name through ProductLabel and the raw-key sweep learned to catch sapProduct. Smaller honesty fixes riding along: credentials table shows the SAP client it always collected; the connections form warns when a free-text environment can never match the closed grant vocabulary, and gains the apiPath/timeoutMs fields the API always accepted (apiPath being the only way an ECC connection ever becomes probeable); the catalogue detail gets the tenant KEY not the label, forwards it, and falls back to the stored probe instead of claiming "not probed" under an ACTIVATED list badge; hub-content/detail/capabilities/ariba take the sapLive throttle their live branches always deserved (admin import loops excluded); the Ariba live call takes the probe guard and writes an audit row; the published- count drift reference agrees with the committed artifacts and its own tiles' help text; the unreachable AI.json drop file is removed with a note. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEq89i8SZpWXPbDXoaRvX8
Phase 3 of the console remediation plan: Discover -> Interface -> Grant -> Credential -> Broker -> Ledger is one walkable thread, for reads AND writes, and the console's green means what green should mean. Write-credential issuance ships (owner decision reversing the deliberate hold): POST /api/studio/clients/write-credential mints the second secret behind six named gates - builder role, tenant scope, all three owners, issuer is not an owner (the same SoD as the bearer token), an active runtime credential to seal onto, and a LIVE write grant covering the credential's environment evaluated with the broker's own predicates. The key is shown once, audited as ClientCredential/ISSUE_WRITE. The write ledger and manual stop saying "no write credential can be issued yet"; the truthfulness guard's write-credential capability arms itself against exactly that prose the moment the route exists. The write path's latent defects are fixed now that it is reachable: an idempotency key is scoped to its INTERFACE (an identical payload posted with one key to interface A then B replayed A's response and never wrote B - the reset also re-stamps interfaceId); complete/release carry the tenant in their where and NorthboundIdempotencyKey joins the anchored- model roster, with the reaper deleting per-organization so the one legitimate cross-tenant sweep still re-asserts whose rows it touches. The Test Console runs THROUGH the broker: /api/studio/test/broker-run exercises the real pipeline server-side - the grant gate, the environment+sapClient binding, the same readEntitySet against the BOUND connection - and records a NorthboundAuditEvent marked dryRun (new column). A refusal is returned verbatim as the refusal the deployed app would receive; the result card names WHICH connection answered, flags an unverified binding, and a governance refusal renders no status chip because there is no tenant fact to report. The old /entities+/preview path (no grant check, no binding, no audit, wrong tenant possible) is gone from the run handler and the lazy-read test now asserts its absence. Interface status reaches the read path: DEPRECATED refuses (INTERFACE_DEPRECATED), DRAFT serves with an x-coreedge-interface-status header, discovery agrees on both, and the developer guide states the asymmetry. Promotion to ACTIVE - the write path's load-bearing precondition, previously applied unconditionally - refuses without an entity set and audits as PROMOTE. Credential lifecycle is fully enumerable: revoke and PATCH-rotate audit as ClientCredential REVOKE/ROTATE instead of "somebody edited a solution"; Control Tower's tone map and known-actions learn the verbs. Northbound discovery and schema reads take the per-credential throttle and write audit rows - a stolen token enumerating the surface no longer leaves zero trace. An interface naming an unknown product refuses as UNKNOWN_PRODUCT (counted by the incident rule) instead of masquerading as the excluded NO_CONNECTION; the resolver's bindingUnverified flag and the audit's inference are pinned equivalent by test. Saved test cases become readable at last: listed, replayable through the broker, and deletable (audited) on the Test Console. Studio mutations refresh via the router instead of full page reloads - except the two secret-bearing forms, which keep the reload that clears them, and say why. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEq89i8SZpWXPbDXoaRvX8
… undated Health on the Operations screens previously reflected the last time a consultant happened to click Test in Studio. This makes the facts current, historied, and causable by the persona who owns the screen: - Scheduled probe sweep (GET /api/cron/connection-probes, every 6 hours): read-only $metadata against every active connection with a probe path, grouped per (org, product) through the same resolver every read path uses, bounded concurrency. Writes lastValidationStatus, appends a SapConnectionProbeEvent history row, and alerts on the healthy-to-failing transition (Sentry + email to the org's platform admins). The transition is the dedupe: a connection down for a week alerts once, when it went down; recovery is recorded but deliberately not alerted. lastValidatedAt moves only on a real 200, same rule as the Studio test route. - On-demand "Probe now" (POST /api/ops/connections-health/probe): the Operations role can run the same probe the cron runs, recorded identically with source "manual" - the delegated-action pattern from the CT registers. - connection-drift incident rule (major): derived from the probe event history, window plus the last event before it, one count per connection however often it flapped. Distinct from connection-unhealthy, which scores the standing state; this scores the change. capability-drift is documented as not yet derivable - stored capability probes are latest-only per tenant, and fabricating the "previously" would break the module's one law. - CronRunLog + shared cron machinery (lib/ops/cron.ts): one constant-time authorizeCron replacing three private copies, and recordCronRun appending each run's outcome. All four cron routes now record their runs, and the Operations home renders them as a jobs strip (/api/ops/jobs) - the one deliberate deployment-wide feed, with a guard-coverage allowance proving the two files touch only cronRunLog. - Nothing undated: useOpsFeed now background-refreshes every minute without flashing the screen to Loading, and every Ops and Control Tower feed renders "as at HH:MM:SS - refreshes every minute" (FeedAsAt). - Screen completeness: fleet-health tiles on Connections (the counts the endpoint always computed and no screen rendered); Tokens gains the never-observed-in-use stat and the includeRevoked toggle the endpoint always honored; broker traffic gains solution/environment filter controls, a connectionId filter (new on the route) with a cross-link from each connection row's "Traffic ->", and the mismatch chip now says "historical rows only" - the binding guarantee made fresh mismatches impossible, so an uncaveated chip would imply a live defect. Migration 20260806002000 adds SapConnectionProbeEvent and CronRunLog. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEq89i8SZpWXPbDXoaRvX8
|
Deployment failed for project aptus with the following error: Learn More: https://vercel.link/3Fpeeb1 |
The 0 */6 * * * schedule failed the deployment outright ("Hobby accounts
are limited to daily cron jobs"). The sweep now runs daily at 04:00 UTC,
after the other three jobs; the jobs strip and the probe route's comment
say so. The "Probe now" action is unchanged and covers the intraday gap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEq89i8SZpWXPbDXoaRvX8
…tion FRESHNESS-RESPEC.md refused the org-scoped freshness endpoint for the right reason - SapHubContent has no organization column, so it would have returned empty for every organization while appearing to work - and specified the deployment-scoped panel instead. This builds exactly that: - GET /api/ops/catalogue-health (platform_admin via requireAdmin, never the tenant-scoped guard): per content type, loaded rows, grouped item sums, illustrative counts, oldest/newest row writes, the newest row's own import provenance stamp, and the published-reference figure - every number a real column. The committed hub-artifact-counts drop contributes its _provenance block as the drift reference's provenance. - Staleness is one named constant, printed beside every verdict: CATALOGUE_STALE_AFTER_DAYS = 183 in lib/sap-public/catalogue-health.ts - one SAP half-yearly release cycle, so STALE means "at least one release behind by construction". No rows renders as NEVER_IMPORTED, its own state: an empty catalogue is not a stale one. - /operations/catalogue: the freshness table, probe coverage (env tenants + the last connection probe sweep from CronRunLog), and a guided refresh that drives the EXISTING seed and chunked harvest-import routes with the typed confirmation phrase. What renders after a refresh is the server's own audited inserted/updated/skipped summary. Refresh stays file-based end to end (Hub ToU): export from a logged-in session, commit, deploy, rebuild - no browser upload, because a catalogue unexplainable from commit history would break the provenance the screen exists to show. - The rail entry replaces the "deliberately absent" note in sections.ts, with adminOnly: the operations layout removes it for every other persona, so an org-scoped user never sees an entry that would always refuse them. The Ops home's "will not show you" copy and the manual gain matching entries, and FRESHNESS-RESPEC.md records what shipped and the one delta (no upload). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEq89i8SZpWXPbDXoaRvX8
…nforce Phase 6 batch one - every item here is a declared control the runtime contradicted: - System audit entries actually persist. Fifteen call sites wrote DecisionLogEntry with assessmentId "system"/"SYSTEM"; no Assessment with that id has ever existed, so every such write violated the FK and either threw (failing org-governance requests after their mutation succeeded) or was silently swallowed by a caller's try/catch - months of org-governance, catalogue-import and onboarding audits simply lost. assessmentId is now nullable (migration 20260806010000), null is the recorded absence, and all fifteen sites pass null. - Logout mutates only on POST. The GET handler is a compat redirect that touches nothing; as a GET, any page on the internet could revoke a user's session with an img tag. All four UI call sites now POST (menu links became no-JS form posts); the POST redirect is 303 so the browser GETs login. - Session-minting IPs come from the trusted extraction. bridge, webauthn verify, verify-izzat and test-login all read the client-supplied leftmost x-forwarded-for hop - the spoofable value - into the session audit trail and login notifications. All four now use getClientIp. - The organization's maxConcurrentSessions is honored. The org form has offered it (1-10, default 3) since it shipped; createSession ignored it and revoked every other session on every login. Now the newest (limit - 1) survive, displacement is recorded as before, and an org-less user keeps strict single-session. - MFA required means required on every door. The three console layouts (Studio, Operations, Control Tower) now apply the same step-up gate as the portal layout - they reach live customer SAP systems and were exempt. - The portal-host workbench redirect is derived from the one allow-list (isWorkbenchOnlyPage): the middleware's inline copy had drifted and served the portal's 404 for Studio/Operations/Control Tower URLs. - A stored connection baseUrl cannot aim at the deployment's own network: IP literals, localhost, .local/.internal and private ranges are refused at validation - the broker calls this URL server-side with credentials. - refuseUnlessMayProbeTenant drops its accepted-and-ignored _envPrefix parameter - an argument that implies scoping it does not do. Deferred from the phase plan with reasons: share-link token hashing, the permission-source reconciliation, and the tenant-scope guard attach land in the next batch; magic-link fail-closed and two audit-report items (G23/G24) need their original findings re-verified before changing behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEq89i8SZpWXPbDXoaRvX8
…lation The parity check builds the database from migration history and diffs it against the schema. Making assessmentId optional changes Prisma's canonical referential action from RESTRICT to SET NULL, so DROP NOT NULL alone left the history-built FK a step behind the schema. The migration now swaps the constraint in place; verified with the same prisma migrate diff CI runs - "No difference detected". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEq89i8SZpWXPbDXoaRvX8
…ptions tenantScopeGuard() existed as an exported factory that nothing applied - a declared control the runtime did not enforce, this phase's exact failure signature. It now guards every query through lib/db/prisma: - The guard machinery moves to lib/db/tenant-guard (lib/studio/tenant-scope re-exports it) so the client can attach it without a circular import through the permissions module. - Three env-controlled modes (TENANT_SCOPE_GUARD): throw outside production, log in production as the burn-in - flip to throw once production logs show zero violations. A legitimate path the burn-down missed surfaces as a log line, never an outage. - Deliberate cross-tenant reads are DECLARED, not inferred, via an AsyncLocalStorage context (permitCrossTenantReads): the ops guard's global-admin branch, the connection probe sweep, and the idempotency-key reaper - the three places where "no organizationId" is the design. The token-hash lookup keeps its narrow structural exemption. - Tests pin the attachment itself (a dropped $extends fails CI), the modes, the exemptions, and the declared-context escape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEq89i8SZpWXPbDXoaRvX8
PERMISSION_MATRIX and ROLE_CAPABILITIES grew independently and contradicted each other on most roles: partner_lead held every register and sign-off action in the matrix while the capabilities file - the one most guards enforce - denied all of them; executive_sponsor could transition an assessment on one path and not the other; consultant could sign off on one and not the other. Two live sources that disagree means the answer depends on which door a request knocks on. - The matrix is reconciled to the capabilities file on every axis both speak to (the capabilities file is the enforced one; the matrix's live exposure is the two flow-regenerate routes, the SSO route and the admin roles page). Actions with no capability axis - scope, notes, workshops, reports, profile, admin panel - keep their original grants. - tests/unit/auth/permission-source-reconciliation.test.ts maps the shared axes (ten one-to-one, plus the two coarse gap/register axes with explicit semantics) and fails the build on the next divergence, for every role. - The 1,289-line tests/unit/permissions/permission-matrix.test.ts is deleted: its own header declared it a self-contained specification whose wiring was blocked - 956 green tests exercising an inline copy of a matrix that matched neither real source. A spec test that cannot fail when the product drifts is occupying the place where a real guard goes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEq89i8SZpWXPbDXoaRvX8
…rver-only Two changes riding together: - AssessmentShareLink stores SHA-256 hashes. The raw token IS the credential - anyone holding it reads the assessment - so a readable token column was a database that could leak usable links. Issuance writes only the hash and returns the raw value once; verification looks up by hash first, falls back to the legacy plaintext column, and upgrades a matched legacy row in place (hash written, plaintext nulled), so the dual-read window closes by itself. Migration 20260806012000 makes the plaintext column nullable and adds the unique hash column; parity-checked against the schema. - The tenant-scope guard attaches on the server only. Client bundles reach lib/db/prisma through long-standing value-import chains (ProductMapTable -> product-map -> prisma) where @prisma/client resolves to a browser stub with no $extends - calling it crashed module init for every page whose client chunk pulled the module in, which is what broke the discovery-session E2E suite on CI. No query can run in a browser bundle, so there is nothing for the guard to guard there. Verified: the failing spec now passes locally against a production build (3 passed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEq89i8SZpWXPbDXoaRvX8
…essor Phase 7, engineering only. CoreEdge can now be priced from real data and sold separately, without choosing a payment processor: - Subscription model: one row per (organization, productLine) with WORKBENCH | COREEDGE | APTUS, plan, status, dates, and a provider-agnostic externalRef. Organization.plan stays the WORKBENCH compatibility source (getSubscription derives it); COREEDGE/APTUS resolve from their rows, and NO row means NO subscription - reported as such, never inherited from the Workbench plan, because inheritance is a pricing decision nobody took. checkFeatureAccess gains a productLine argument on the same rule. - CoreEdgeUsageRollup: one bounded row per organization-day, rolled up nightly from NorthboundAuditEvent (calls, rows read on successful reads, accepted writes, 401/403 refusals counted separately as a product signal, distinct interfaces/credentials). Idempotent upserts keyed on UTC midnight; bounded backfill (30 days/run, remainder reported); yesterday always re-runs so late audit writes converge. Rides the nightly analytics cron slot (the deployment plan allows daily-only jobs) with its own CronRunLog entry, visible on the Ops jobs strip. - Control Tower "CoreEdge usage" screen: reads rollups ONLY - never live aggregation over the audit table - and says plainly that the numbers are a floor inherited from the audit feed, under-counting in the customer's favour, and that nothing on the screen bills anyone. - docs/coreedge/COMMERCIAL-DECISIONS.md records the six decisions that stay the owner's (pricing basis, refusal billability, packaging, API licensing, CoreEdge trial semantics, repo visibility) with what the engineering now makes decidable. Migration 20260806020000; parity-checked. 316 files / 4,733 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEq89i8SZpWXPbDXoaRvX8
ib823
marked this pull request as ready for review
August 6, 2026 03:24
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the full approved seven-phase plan from the exhaustive codebase assessment. Each phase is one commit (plus CI fixes), and each commit left typecheck:strict, lint:strict, the full vitest suite and
next buildgreen.The phases
APPROVED…REVOKED), revocation visible in Studio and enforced in northbound discovery, ten stale prose claims purged, honest-status corrections (NOT_FOUND pill, lifecycle chip meanings, DB-aggregate counts), truthfulness guard tests.appliesToPublic: trueregardless of product. NowhubCatalogueScope(edition | product-tag | none) decides per product; import preserves product identity (productTagscolumn);sap-clientpropagates; SuccessFactors SAML fixed (authType + sealed secrets); throttle and probe-guard coverage widened.SapConnectionProbeEventhistory + healthy→failing drift alerts (Sentry + email, transition-deduped); Ops "Probe now";connection-driftincident rule;CronRunLog+ jobs strip; every Ops/CT feed auto-refreshes and shows "as at"; fleet tiles, tokens/traffic screen completions, mismatch chip caveated "historical rows only".CATALOGUE_STALE_AFTER_DAYSconstant, import provenance per row, probe coverage, guided file-based refresh driving the existing seed/harvest-import routes. The "deliberately absent" rail note is retired.assessmentId; the "system" sentinel violated its FK at 15 sites); logout POST-only; trusted client IPs at all session-minting sites; orgmaxConcurrentSessionshonored; MFA step-up on all three console layouts; SSRF guard on connection baseUrl; the tenant-scope guard is attached to the live client (throw in dev/test, log-burn-in in prod, declared cross-tenant contexts);PERMISSION_MATRIXreconciled toROLE_CAPABILITIESwith a build-failing reconciliation test (the 1,289-line self-contained spec test deleted); share-link tokens stored as SHA-256 with dual-read upgrade.Subscription(WORKBENCH | COREEDGE | APTUS, no inheritance between lines), nightlyCoreEdgeUsageRollupfrom the audit feed (bounded, idempotent, floor-not-census), Control Tower Usage screen reading rollups only, anddocs/coreedge/COMMERCIAL-DECISIONS.mdrecording the six owner decisions.Migrations
Five, all parity-checked against the schema with
prisma migrate diff(the CI check's own command): hub product identity, audit dryRun, probe events + cron log, nullable audit assessment + share-link hash, subscriptions + usage rollup.Deferred, with reasons
capability-driftincident rule: stored capability probes are latest-only per tenant, so "previously-200" is not recorded — deriving it would fabricate a signal. Becomes buildable with capability probe history (noted inlib/ops/incidents.ts).🤖 Generated with Claude Code
https://claude.ai/code/session_01FEq89i8SZpWXPbDXoaRvX8