Add @Publish with owner-scoped component resolution and monotonic publishing (v0.1.15) - #58
Merged
Volv-G merged 4 commits intoSep 17, 2026
Conversation
Regular component publishing is now monotonic against the highest non-deprecated, owner-scoped published version: a local version older than the latest published version is a no-op SKIP instead of publishing the older spec and deprecating the newer one. Equal is still a SKIP and newer still PROCEEDs. Ordering comes from the existing compare_versions. Skips now surface the exact digest they were compared against so callers (for example ship-on-publish) can pin it: - ProcessingResult.latest_digest: digest of the selected latest non-deprecated owner-scoped published version, set on PROCEED/SKIP and carried through the SUCCESS/ERROR results that follow a version check. - ProcessingResult.resolved_digest: digest or latest_digest, but None unless the outcome is SUCCESS or SKIP, so a publish request that raised and may still have landed never hands back the stale digest. - ProcessingResult.digest keeps its existing SUCCESS-only meaning. If several non-deprecated owner-scoped components tie at the selected latest version, no exact digest can be chosen, so the check fails closed with an error naming the tied digests rather than guessing from API ordering. Deprecated components are never selected as latest. Deliberate downgrades remain possible via an explicit opt-in: --allow-downgrade on published-components publish, or allow_downgrade=True on the publisher and its wrappers. Default is off. Assisted-By: devx/6aa1a44a-8f9f-4be7-b20c-752af4396eb9 (cherry picked from commit 58593ea3c453ca2429efbe668ad7a8ec48dfd0e8)
…blish Follow-up to the monotonic publishing change, addressing review findings that monotonicity could still be violated. Preflight previously warned and skipped any candidate whose spec/version could not be read, and the publish path then re-listed and deprecated every returned row. A listed-but-unreadable row (possibly newer) could therefore be deprecated sight-unseen, and a version appearing between the two lookups could be deprecated by a decision made before it existed. - Reading the published state is now a single observation (collect_published_state) returning a verified digest -> version snapshot plus the identifiers of any unreadable non-deprecated candidate. Any unreadable candidate, including one with no digest, is an ERROR: nothing is published and nothing is deprecated. - Policy lives in one place (_evaluate_published_state) and is re-applied to a fresh observation taken immediately before create, so a concurrently published equal/newer/ambiguous row still turns the publish into a SKIP or ERROR with no create or deprecate calls. - Only digests proven strictly older in that final observation are deprecated, in sorted order. A row first seen after the publish decision is never deprecated. allow_downgrade likewise never deprecates a strictly newer row. A race after the final read needs a server-side conditional/CAS operation and is out of scope here. - Latest/tied digest lists are sorted and the latest version is chosen deterministically, so results and diagnostics no longer depend on API response ordering. resolved_digest outcome safety and allow_downgrade semantics are unchanged, and the added regressions cover unreadable newer rows, concurrent newer/equal/ambiguous rows between lookups, refusal without create/deprecate, and order-independent diagnostics. Assisted-By: devx/6aa1a44a-8f9f-4be7-b20c-752af4396eb9 (cherry picked from commit 585a998228c7433c11d8a01a184c66d984cccc53)
…docs The dry-run override built _PublishedState(latest_version=...), but latest_version is a derived property, so any dry-run with TEST_LATEST_VERSION set raised TypeError before reaching the policy. The synthetic override now has its own field. It has no backing published component, so it yields no digest: latest_version reports it, while latest_digest and resolved_digest stay None, and the normal older/equal/newer rules apply. The README outcome table also still claimed that an unreadable remote version proceeds, which contradicted the fail-closed behavior described directly below it. Unreadable and ambiguous states are now their own error row. Assisted-By: devx/6aa1a44a-8f9f-4be7-b20c-752af4396eb9 (cherry picked from commit 11e45bc3bea322150b20f4af7640e944b33f87ae)
Introduce a `@Publish` decorator that records a component's registry name and
version as real `CallableRef` fields, and emit that declaration into the
compiled resolver sidecar so separate tooling can act on it:
orders-loader:
name: orders-loader
version: "1.0"
publisher: me
local_from_python: {...}
publish: true
The declaration uses the entry's OWN generic name/version fields, so ordinary
hydration looks the published component up and falls back to the local block
only when no such component exists. A registry error is not a fallback:
resolution fails closed rather than quietly building from local source.
`publish` is a separate literal `true` and is never inferred from the other
fields, so a hand-written entry of that shape is not silently published.
`publisher` is the symbolic `me`, resolved at hydration time to whoever is
authenticated. Compilation stays offline and records no account id, so an
artifact is not bound to one author's account. Without this scoping the lookup
is global, and a component published by someone else under the same name and
version is a valid candidate -- a name is not an identity control. The symbol
is interpreted only alongside `publish: true`, which the compiler always writes
together, so `me` is not a reserved account id and a hand-authored publisher is
still used verbatim and still resolves without authentication.
Owner filtering is made exact. The API owner parameter is a substring match, so
an exact `published_by` request is post-filtered on the returned rows: a
superset id (`alice` vs `alice2`) and a row with no owner are both refused.
Explicit `published_by_substring` callers keep partial matching. This also
tightens the publisher's owner-scoped version check, which must not treat
another account's components as its own.
Identity parsing is shared by the publisher and the hydrator so the two cannot
disagree about who the current user is.
Publication is deliberately NOT part of component dedup identity, so two
declarations that agree are still one component.
Assisted-By: devx/6257e672-aaa7-443c-a4e1-a0150a485d9d
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.
(AI-assisted)
Two related changes to component publishing, landed together because the second
depends on the first being owner-scoped and exact.
1. Monotonic component publishing
Publishing a component now compares against the latest published version for the
owner and refuses to move backwards, surfacing the compared digest so a skip or
refusal explains itself. Unreadable rows fail closed rather than being treated as
absent, published state is re-checked immediately before create to narrow the
race window, and only components proven older are deprecated. A
--allow-downgradeescape hatch exists for the intentional case.
2.
@Publishand owner-scoped resolution@Publishrecords a component's registry name and version as realCallableReffields, and the compiler emits that declaration into the resolver sidecar:
The declaration uses the entry's own generic
name/versionfields, soordinary hydration looks the published component up and uses
local_from_pythononly when no such component exists yet. A registry error is not a fallback —
resolution fails closed instead of quietly building from local source.
publishis a separate literaltrue. Publication is never inferred fromname+version+local_from_python, so a hand-written or fallback entry ofthat shape is not silently published.
Why
publisher: meA name is not an identity control. Without an owner the lookup is global, and a
component published by someone else under the same name and version is a valid
candidate — so a collision or a squat could be resolved and deployed in place of
the author's own component. The window is real: a marked entry resolves by name
until its first publication.
The compiler cannot write an account id, because compilation is offline and must
stay so, and baking one author's account into an artifact would be wrong anyway.
It writes the symbolic
me, which hydration resolves to whoever is authenticated.If the account cannot be determined, resolution fails closed rather than widening
to a global search.
The symbol is interpreted only alongside
publish: true, which the compileralways writes together. So
meis not a reserved account id: a hand-authoredpublisher: meis still used verbatim, and still resolves without requiringauthentication.
Exact owner filtering
The API owner parameter is a substring match, so for an exact
published_byrequest the server result is only a prefilter. The client now post-filters the
returned rows: a superset id (
alicematchingalice2) and a row with no ownerare both refused, and the client does not assume the server enforced the filter
it was given. Explicit
published_by_substringcallers keep partial matching.This also tightens the publisher's owner-scoped version check, which should not
treat another account's components as its own.
Identity parsing is shared by the publisher and the hydrator, so the two cannot
disagree about who the current user is.
Not part of dedup identity
Publication metadata is deliberately excluded from component identity, so two
agreeing declarations remain one component.
Notes
uv lockwas not run; the lockfile self-entry is edited in place.decorator is an inert metadata stub.