Skip to content

feat(entity-client): publish the attribute-type allowlists - #469

Merged
josecarneiro merged 3 commits into
mainfrom
claude/conditional-pricing-attributes-w18k7u
Sep 1, 2026
Merged

feat(entity-client): publish the attribute-type allowlists#469
josecarneiro merged 3 commits into
mainfrom
claude/conditional-pricing-attributes-w18k7u

Conversation

@josecarneiro

Copy link
Copy Markdown
Contributor

Why

RELATION_ATTRIBUTE_TYPES and the overridable_attribute type allowlist are currently hand-copied across four repos, kept in sync by a comment:

Repo File Copy of
entity-api .../schema/schema-attribute-service.ts:11, :98 both
epilot360-entity-builder .../custom-configs/editModeUtils.ts:13 relation
epilot360-entity-builder src/entity-builder/conditions/conditions-config.ts:321 overridable
epilot360-entity-builder-v2 src/lib/edit-mode.ts:33 relation
epilot360-integration-hub .../config-schema-validation.ts:41 relation

conditions-config.ts even carries a comment saying it was "verified member-for-member" against entity-api — a manual sync contract, which is the thing worth deleting.

This is the follow-up to #464, which built the mechanism (a client's schema-model.ts reaching consumers as real runtime values) but didn't yet use it for the constants that motivated it.

What changed

clients/entity-client/src/schema-model.ts gains:

export type AttributeType = NonNullable<Attribute['type']>

export const RELATION_ATTRIBUTE_TYPE_LIST = [...] as const satisfies readonly AttributeType[]
export const OVERRIDABLE_ATTRIBUTE_TYPE_LIST = [...] as const satisfies readonly AttributeType[]

export const RELATION_ATTRIBUTE_TYPES: ReadonlySet<string> = new Set(RELATION_ATTRIBUTE_TYPE_LIST)
export const OVERRIDABLE_ATTRIBUTE_TYPES: ReadonlySet<string> = new Set(OVERRIDABLE_ATTRIBUTE_TYPE_LIST)

These reach consumers as @epilot/entity-client exports and, through the copyModels generator step from #464, as @epilot/sdk/entity exports too.

Three decisions worth a look

ReadonlySet<string>, not ReadonlySet<AttributeType>. Every consumer calls .has(attribute.type) on an unnarrowed string. A Set<AttributeType> would reject that and force a cast at all four call sites; typing the set as string keeps the migration import-only. The type safety lives on the arrays instead, where it costs nothing.

The anchor is Attribute['type'], not a hand-written union. Attribute is an anyOf of 35 variants, so that resolves to every type literal the spec declares (39 in total). Verified the guard bites — adding 'not_a_real_type' fails with TS1360.

Renamed VARIANT_OVERRIDABLE_ATTRIBUTE_TYPESOVERRIDABLE_ATTRIBUTE_TYPES, matching the overridable_attribute flag it gates and what epilot360-entity-builder already calls its own copy. Easy to revert if you'd rather keep the old name.

Test plan

  • pnpm build (tsup + dts) clean across all 51 APIs
  • 11 tests pass in models.test.ts (2 new)
  • New test parses the entity spec, resolves all 39 declared attribute types, and asserts every allowlist member is one of them — a runtime guard complementing the compile-time satisfies. It also pins ordered_list as declared-but-not-overridable, the case that made an allowlist necessary rather than a denylist
  • Verified the values in the built bundle, not just source: dist/apis/entity.cjs exports both sets with the right members, and RelationAffinityMode still resolves
  • Drift guard confirmed both ways: a member absent from the spec fails TS1360; the spec-parsing test is non-vacuous (39 types found, rejects a fake one)
  • pnpm lint clean (524 files)

Note on releasing

This touches no clients/*/openapi.json, so the spec-gated auto-release job will not fire — the same thing that happened to #464, which only shipped when an unrelated spec PR swept it up ~90 minutes later. It needs the changesets flow (pnpm version-packages && publish-packages) or a manual @epilot/sdk@x.y.z tag. A changeset is included.

Worth considering separately whether the gate should also trip on clients/*/src/schema-model.ts and packages/epilot-sdk-v2/src/**.

Follow-up (not in this PR)

Once released, delete the four copies. entity-api is the awkward one — it has no @epilot/sdk dependency and is pinned to @epilot/entity-client@^4.25.1 while 7.4.0 is published, so it needs either a new dep or a three-major bump. Those are GitLab repos, so they'll be MRs rather than PRs.


Generated by Claude Code

RELATION_ATTRIBUTE_TYPES and OVERRIDABLE_ATTRIBUTE_TYPES are hand-copied
in entity-api, epilot360-entity-builder, epilot360-entity-builder-v2 and
epilot360-integration-hub, kept in sync by a comment. Publishing them from
entity-client — and so through @epilot/sdk/entity, via the schema-model.ts
mechanism — lets those four copies become an import.

Both are ReadonlySet<string> so a caller can test an unvalidated
attribute.type without narrowing first, which is how every consumer uses
them today. The literal-typed arrays are exported alongside, and both are
`satisfies readonly AttributeType[]` against the union of every type the
Attribute variants declare, so a member the spec does not have is a
compile error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXhwfYV419Bodt42E3aw2C
Conditional pricing is the only consumer of the `overridable_attribute`
type allowlist, so it ships from pricing-client rather than entity-client.

RELATION_ATTRIBUTE_TYPES stays in entity-client: three of its four call
sites are unrelated to pricing (edit_mode_config in both entity builders,
relation mapping validation in integration-hub), and moving it would make
those repos depend on pricing-client.

The moved list loses its compile-time anchor — it holds entity types, and
pricing-client has no entity schema to `satisfies` against and must not
import one, since @epilot/sdk depends on no client package. models.test.ts
checks both lists against the entity spec instead, which covers it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXhwfYV419Bodt42E3aw2C
Cut the block comments to one line each. The rationale for where the lists
live and why the pricing one has no compile-time anchor belongs in the PR,
not repeated above every export.

Comments only; no behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXhwfYV419Bodt42E3aw2C
@josecarneiro
josecarneiro merged commit a34c82d into main Sep 1, 2026
7 checks passed
josecarneiro pushed a commit that referenced this pull request Sep 1, 2026
The auto-release gate only fired on `clients/*/openapi.json`, so a change
to the SDK's own runtime (proxy.ts, retry.ts, client-factory.ts), to a
hand-written client module (schema-model.ts, additional-types.ts), or to
the generator itself never published. #464 and #469 both landed on main
unpublished and only shipped when an unrelated spec PR swept them up.

The gate now matches those paths too, and excludes the generated output
under src/{apis,types,models,definitions,docs} — the auto-release commit
rewrites those itself, so matching them would make it re-trigger this job
and bump versions in a loop. The release commit also now carries
`[skip release]`, which the job's own `if` already honours, as a second
layer in case the exclusion is later widened carelessly.

Verified the new expression against real commit file lists: a spec change,
#464, #469, an SDK runtime fix and a generator change all trigger; the
auto-release commit 5e39d9c, docs-only, changeset-only and
generated-output-only changes all do not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXhwfYV419Bodt42E3aw2C
josecarneiro pushed a commit that referenced this pull request Sep 1, 2026
Merging main brought in the environments spec reshape (#463), the entity/pricing
attribute-type allowlists (#469) and the @epilot/cli@0.1.142 release. That
release again staged only packages/cli/package.json, so main had
definitions/environments.json stale against the spec it is copied from and
src/index.ts reporting 0.1.51 against a package.json at 0.1.142.

Regenerating brings both in line: all 51 definitions match their client specs
and src/index.ts reports 0.1.142.

Third occurrence of this drift since the PR was opened — once per release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MhYbcfHQfe7bk2R37P75zV
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