Skip to content

CoreEdge Console: the seven-phase perfect-solution remediation - #226

Merged
ib823 merged 12 commits into
mainfrom
claude/capability-catalogue-accuracy-183ha7
Aug 6, 2026
Merged

CoreEdge Console: the seven-phase perfect-solution remediation#226
ib823 merged 12 commits into
mainfrom
claude/capability-catalogue-accuracy-183ha7

Conversation

@ib823

@ib823 ib823 commented Aug 5, 2026

Copy link
Copy Markdown
Owner

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 build green.

The phases

  1. Governance surfaces stop lying — one effective-grant resolver (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.
  2. Product/edition/client awareness end to end — the original defect: every catalogue read filtered appliesToPublic: true regardless of product. Now hubCatalogueScope (edition | product-tag | none) decides per product; import preserves product identity (productTags column); sap-client propagates; SuccessFactors SAML fixed (authType + sealed secrets); throttle and probe-guard coverage widened.
  3. The governance loop closes — write-credential issuance ships (6-gate SoD), interface-scoped idempotency, DRAFT/DEPRECATED interface semantics agreed between discovery and enforcement, Test Console runs the real broker pipeline with dryRun audit, credential lifecycle verbs audited, saved test cases reachable.
  4. Real observability — nightly connection probe sweep (read-only $metadata) + SapConnectionProbeEvent history + healthy→failing drift alerts (Sentry + email, transition-deduped); Ops "Probe now"; connection-drift incident 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".
  5. Catalogue Health — built per FRESHNESS-RESPEC (deployment-scoped, platform_admin-only): per-type freshness against the printed CATALOGUE_STALE_AFTER_DAYS constant, import provenance per row, probe coverage, guided file-based refresh driving the existing seed/harvest-import routes. The "deliberately absent" rail note is retired.
  6. Enforcement equals declaration — system audit writes actually persist (nullable assessmentId; the "system" sentinel violated its FK at 15 sites); logout POST-only; trusted client IPs at all session-minting sites; org maxConcurrentSessions honored; 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_MATRIX reconciled to ROLE_CAPABILITIES with 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.
  7. Commercial identity (engineering only) — per-product-line Subscription (WORKBENCH | COREEDGE | APTUS, no inheritance between lines), nightly CoreEdgeUsageRollup from the audit feed (bounded, idempotent, floor-not-census), Control Tower Usage screen reading rollups only, and docs/coreedge/COMMERCIAL-DECISIONS.md recording 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-drift incident 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 in lib/ops/incidents.ts).
  • Three audit-report items (magic-link fail-closed, G23, G24) need their original findings re-verified before changing behavior.
  • Browser-upload catalogue refresh: refresh stays repo-committed by design so the running catalogue is explainable from commit history (recorded in FRESHNESS-RESPEC).

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEq89i8SZpWXPbDXoaRvX8

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
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
aptus Ready Ready Preview Aug 6, 2026 3:24am

…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
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

Deployment failed for project aptus with the following error:

Hobby accounts are limited to daily cron jobs. This cron expression (0 */6 * * *) would run more than once per day. Upgrade to the Pro plan to unlock all Cron Jobs features on Vercel.

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
claude added 2 commits August 6, 2026 01:54
…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 ib823 changed the title CoreEdge Console remediation: governance truth, product-aware catalogue, and the full 7-phase plan CoreEdge Console: the seven-phase perfect-solution remediation Aug 6, 2026
@ib823
ib823 marked this pull request as ready for review August 6, 2026 03:24
@ib823
ib823 merged commit 23a16c4 into main Aug 6, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants