Skip to content

chore(scripts): audit + reconcile brand-domain www/no-www mismatch (#4448)#4457

Merged
bokelley merged 6 commits into
mainfrom
bokelley/audit-brand-domain-www-mismatch
May 12, 2026
Merged

chore(scripts): audit + reconcile brand-domain www/no-www mismatch (#4448)#4457
bokelley merged 6 commits into
mainfrom
bokelley/audit-brand-domain-www-mismatch

Conversation

@bokelley

@bokelley bokelley commented May 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #4448. Two scripts and one applied prod reconciliation:

  1. server/src/scripts/audit-brand-domain-www-mismatch.ts — read-only audit identifying orgs whose past brand_revisions live on a different brand domain than their current organization_domains.is_primary=true row. Tags each row with manifest_orphaned status so future runs distinguish "still drifting" from "historical residue".
  2. server/src/scripts/reconcile-brand-domain-www-mismatch.ts — one-shot reconciliation, hardcoded to the three orgs the audit surfaced. Per affected org: copies brand_manifest.agents from www.<domain><domain> (deduped on URL), marks the www brand row manifest_orphaned=true, inserts a brand_domain_aliases row routing www → root. Idempotent; dry-run by default; --apply to persist.

Applied on prod 2026-05-12 11:33 UTC

Run via fly ssh console -a adcp-docs against machine 6835e5ef7e3548:

[Affinity Answers] agent_copy: 0→1 (www had 1)
[Affinity Answers] orphan_www: www.affinityanswers.com
[Affinity Answers] alias_insert: www.affinityanswers.com→affinityanswers.com
[Affinity Answers] COMMITTED

[BidMachine] agent_copy: 0→1 (www had 1)
[BidMachine] orphan_www: www.bidmachine.io
[BidMachine] alias_insert: www.bidmachine.io→bidmachine.io
[BidMachine] COMMITTED

[Scope3] orphan_www: www.scope3.com
[Scope3] COMMITTED

Post-apply verification:

  • affinityanswers.com brand row: 1 agent in manifest, orphaned=false
  • www.affinityanswers.com brand row: 1 agent (preserved), orphaned=true
  • bidmachine.io brand row: 1 agent in manifest, orphaned=false
  • www.bidmachine.io brand row: 1 agent (preserved), orphaned=true
  • scope3.com brand row: 0 agents, orphaned=false (unchanged)
  • www.scope3.com brand row: 0 agents, orphaned=true
  • All three aliases present.

GET /api/registry/agents?limit=500 now surfaces https://adcp.bidmachine.io/adcp/mcp (BidMachine's JSONB said visibility: public all along, but their root brand.json was empty — they were "publicly listed" in the dashboard yet invisible to the catalog. They're now actually publicly listed.)

Affinity Answers (escalation #334)

This unblocks Santhosh's publish flow. Their member_profiles JSONB is still at visibility: members_only — they need to retry the publish action from the dashboard. The path now succeeds end-to-end because getBrandPrimaryDomain returns affinityanswers.com and the brand row at that domain is ready to receive the agent.

Not in this PR

  • The Scope3 email_domain = "www.scope3.com" drift (separate column inconsistency from the "four overlapping domain columns" pattern) — out of scope; would need a careful update path that doesn't trip the FK/trigger machinery.
  • Forward fix for future Stage 2 drift — not needed if we ensure organization_domains.is_primary is set to the same canonical brand-domain variant the registry crawler uses. The three affected orgs were a one-time backlog from the column drop, not an ongoing source.

Test plan

  • Dry-run on prod
  • Apply on prod
  • Re-audit confirms [RECONCILED — www row orphaned] tag on all 3 entries
  • Public registry catalog (/api/registry/agents) shows BidMachine's previously-stranded public agent
  • Reply to Santhosh confirming retry is now safe; close escalation Fix allOf + additionalProperties validation and add oneOf titles #334 if he publishes successfully

🤖 Generated with Claude Code

bokelley and others added 6 commits May 11, 2026 22:26
The publish-agent gate accepts four API-access tiers (Professional,
Builder, Member, Leader) per server/src/services/membership-tiers.ts.
User-facing copy across 11 surfaces said "Professional tier or higher",
which Addie and customers read as "tiers priced above Professional" —
the reverse of the actual ordering, since Builder ($3K), Member ($15K),
and Leader ($50K) all cost more than Professional ($250) but qualify.

Affinity Answers (escalation #334, Builder plan) was told by Addie to
upgrade to Professional after failing to publish. Builder always
qualified; the copy misled both the user and the agent.

Replace the phrase with explicit lists ("Professional, Builder, Member,
or Leader" / "paying AAO members") across error messages, dashboard
tooltips, Addie's behavior rules, OpenAPI schema descriptions, and the
registry-of-an-agent doc. No behavior change; tests assert on the
`tier_required` error code, not the message text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`docs/aao/addie-tools.mdx` is auto-generated from MCP tool descriptions.
The `save_agent` fix in the prior commit only updated the source file
(`server/src/addie/mcp/member-tools.ts`); the published doc still
carried the misleading "Professional-tier (or higher)" phrasing. This
runs `npm run build:addie-tools` to reflect the updated description.

The regen also picks up unrelated pre-existing drift (the
`list_github_issues` description and the new `agent_conformance` tool
set) that had not yet been regenerated. CI doesn't gate on this, so the
drift was silently accumulating; folding the catch-up in here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two surgical edits in the published `static/openapi/registry.yaml`
mirroring the source descriptions in
`server/src/schemas/member-agents-openapi.ts`. The full regen output
includes a lot of unrelated pre-existing drift (new fields like
`discovery_method`, `resolved_url`, `self_redirected` hosting mode);
keeping those out of this copy-fix PR — they should land in a separate
regen-only PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a read-only audit identifying orgs whose `brand_revisions` history
points at a different brand domain than their `organization_domains.is_primary=true`
row — most commonly `www.<domain>` vs `<domain>`. Stage 2 of #4159
(`5163d21425`, drop of `member_profiles.primary_brand_domain`) moved
the publish path's brand-row authority to the resolver, so any org
whose previous curation lived on the www variant now has its next
publish write land on the root variant — a brand row they haven't
edited.

Affinity Answers (escalation #334) is the known case; this script
quantifies the rest before deciding on a reconciliation strategy
(flip is_primary, merge brand rows, or add a lookup-time canonicalization
fallback at the publish site).

Output groups results into:
  - `www_in_revisions` — revisions on www.<primary>; root primary
  - `www_in_primary` — revisions on root; www.<primary> primary
  - `unrelated` — different brand domain entirely (parent-portfolio /
    multi-brand-curation case; not a regression)

Dry-run only. Run via:
  fly ssh console -a adcp-docs -C \\
    'node /app/dist/scripts/audit-brand-domain-www-mismatch.js'

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One-shot reconciliation for the three orgs identified by the audit
(Affinity Answers, BidMachine, Scope3). Each affected org has brand
content on `www.<domain>` while their `organization_domains.is_primary`
points at `<domain>` — Stage 2 of #4159 (5163d21) moved publish-path
brand-domain authority to the resolver, and the prior `member_profiles.primary_brand_domain`
column had been holding the www variant for these orgs.

Per affected org, the script:

  1. Copies `brand_manifest.agents` from www brand row into root brand
     row, deduped on agent URL.
  2. Marks the www brand row `manifest_orphaned=true` (the lookup
     helpers already skip orphaned rows; preserves history without
     serving from the wrong variant).
  3. Inserts a `brand_domain_aliases` row (`www.<domain>` → `<domain>`).
     For Scope3 the alias already exists, so this step is a no-op there.

Hardcoded list — re-deriving at runtime risked silently expanding scope
if a new mismatch appeared between audit and apply. Audit was run via
fly ssh on 2026-05-12 against prod; results captured in the script
header.

Dry-run by default:
  npx tsx server/src/scripts/reconcile-brand-domain-www-mismatch.ts
  npx tsx server/src/scripts/reconcile-brand-domain-www-mismatch.ts --apply

  fly ssh console -a adcp-docs -C \\
    'node /app/dist/scripts/reconcile-brand-domain-www-mismatch.js --apply'

Idempotent: re-running is a no-op (no agents to copy, www already
orphaned, alias already present).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n row

The reconciliation marks the www brand row `manifest_orphaned=true` and
moves agents to the root row. `brand_revisions` is a permanent log, so
its rows stay on `www.<domain>` — re-running the audit otherwise looks
identical to the pre-fix state.

This adds a `revision_row_orphaned` flag derived from `brands.manifest_orphaned`
at the row the revisions are recorded against, and tags reconciled rows
in the output ("[RECONCILED — www row orphaned]"). Lets a future
operator distinguish "still drifting" from "historical residue".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley changed the title chore(scripts): audit-brand-domain-www-mismatch (#4448 scoping) chore(scripts): audit + reconcile brand-domain www/no-www mismatch (#4448) May 12, 2026
@bokelley

Copy link
Copy Markdown
Contributor Author

Code review of server/src/scripts/reconcile-brand-domain-www-mismatch.ts (already applied to prod 2026-05-12 11:33 UTC — review is for the artifact's correctness and re-run safety):

Ship.

Verified concerns:

  • Trigger brands_orphan_on_owner_cleared_trigger (migration 474) is BEFORE UPDATE OF workos_organization_id — the reconcile only touches manifest_orphaned, brand_manifest, updated_at. Trigger does not fire. Confirmed.
  • Read paths: getBrandPrimaryDomain resolves to the root domain for all three affected orgs, so publish-path (resolveBrand), agent-visibility-enforcement, member-profile assembly, and the registry-api routes all target the root brand row. None of them read the orphaned www row by accident. Routes that consult manifest_orphaned (member-profiles.ts:210, registry-api.ts:2951/3195, referrals.ts:63, http.ts:8487) correctly skip the www row. listDiscoveredBrands doesn't filter on it, but has no callers.
  • Per-org transaction is the right granularity — each org is independent, failures shouldn't roll back the others.
  • Idempotent re-run: orphan flip and alias insert are no-ops on the second run; agent-copy short-circuits because the www manifest agents have been merged into root (root key set now contains them).

Nits (not blockers, no change needed for this artifact):

  • Dedup key asymmetry: root-side key prefers url.toLowerCase(), falls back to id. Same for www-side. If root has an agent keyed by id only and www has the same agent keyed by url only (or vice versa), they'd be treated as distinct. Vanishingly unlikely given the publish path always writes url, but worth noting if this script is ever reused.
  • Agents missing both url and id are silently dropped (key=''). Acceptable — AdCP agents require a url — but a console.warn would be defensible.
  • The reconciled state (workos_organization_id set + manifest_orphaned=true + is_public=true + domain_verified=true) is one no other code path produces. If the same org ever re-claims www.<domain> via applyBrandIdentity, it hits the orphan-decision 409 at brand-identity.ts:190 — odd UX but defensible, and that flow shouldn't happen post-alias anyway.
  • Hardcoded list with stale-audit risk is handled correctly: if a new org joins the same drift class, the script touches only the three listed orgs. Re-running the audit is required to update the list. The script header documents this.

Ready to push.

@bokelley bokelley merged commit a9e292c into main May 12, 2026
18 checks passed
@bokelley bokelley deleted the bokelley/audit-brand-domain-www-mismatch branch May 12, 2026 11:39
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.

Brand-domain resolver returns root domain but prior brand.json revisions live at www.* (Stage 2 #4159 drift)

1 participant