Skip to content

Add @Publish with owner-scoped component resolution and monotonic publishing (v0.1.15) - #58

Merged
Volv-G merged 4 commits into
masterfrom
piforge/tangle-pipeline-crud/tangle-cli-publish-marker-monoto-daee24b
Sep 17, 2026
Merged

Volv-G merged 4 commits into
masterfrom
piforge/tangle-pipeline-crud/tangle-cli-publish-marker-monoto-daee24b

Conversation

@Volv-G

@Volv-G Volv-G commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

(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-downgrade
escape hatch exists for the intentional case.

2. @Publish and owner-scoped resolution

@Publish records a component's registry name and version as real CallableRef
fields, and the compiler emits that declaration into the resolver sidecar:

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 uses local_from_python
only when no such component exists yet. A registry error is not a fallback —
resolution fails closed instead of quietly building from local source.

publish is a separate literal true. Publication is never inferred from
name + version + local_from_python, so a hand-written or fallback entry of
that shape is not silently published.

Why publisher: me

A 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 compiler
always writes together. So me is not a reserved account id: a hand-authored
publisher: me is still used verbatim, and still resolves without requiring
authentication.

Exact owner filtering

The API owner parameter is a substring match, so for an exact published_by
request the server result is only a prefilter. The client now post-filters the
returned rows: a superset id (alice matching alice2) and a row with no owner
are both refused, and the client does not assume the server enforced the filter
it was given. Explicit published_by_substring callers 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

  • Version bumped to 0.1.15 across the four sites prior release PRs touch.
    uv lock was not run; the lockfile self-entry is edited in place.
  • Compiling performs no network, registry, or publication side effects. The
    decorator is an inert metadata stub.

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
@Volv-G
Volv-G requested a review from Ark-kun as a code owner September 17, 2026 21:41
@Volv-G
Volv-G merged commit e7bef0f into master Sep 17, 2026
6 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.

1 participant