Skip to content

feat(coordinator)!: plan parents through an ordered list of sources - #658

Merged
karhu merged 6 commits into
tnet-157/9-sourcesfrom
tnet-157/10-build-sources
Nov 10, 2026
Merged

karhu merged 6 commits into
tnet-157/9-sourcesfrom
tnet-157/10-build-sources

Conversation

@karhu

@karhu karhu commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

The Stack

This stack covers refactoring the internal logic of how releases are resolved during read, download & build, in particular also in the context of parent releases / parent datasets. On top of that it refactors the public TimeNet APIs and CLI interfaces around these actions, to streamline the behavior across them.

This PR

Moves the build planner onto an ordered list of sources.

Details

A build gets each parent that the store lacks from one registry (fetch_from) or from a build (build_parents) (TNET-157). The design replaces both with one ordered list of sources, which #657 adds. This PR moves the build planner onto that list.

  • plan_steps() replaces plan_parents(). It takes the InstalledBuilder of the requested release and parents_from, an ordered list of sources. For each release that the store lacks or holds damaged, resolve_release() picks the first source that provides its exact version. A registry gives a copy step, and InstalledConnectors() gives a build step.
  • The steps are typed: CopyStep(release, registry, depends_on, with_payload_sources) and BuildStep(release, builder, depends_on, pinned_by, force). Step is their union. Each type has a read-only action ("copy" or "build") for the plan tree. run_plan() matches on the step type, with one arm for each type.
  • A BuildStep keeps the InstalledBuilder that the lookup read, and the run builds with it. So a plan and its run read the card of each connector once.
  • resolve_release() takes ask=, a function that asks one source. The planner passes one that keeps each answer.
  • A release needs exactly the payload sources that its manifest lists, never the payload sources of a payload source. The Role docstring states this rule. The planner applies it before any step runs, with the walker of copy_release(), find_payload_sources() (feat(registry)!: copy a release between registries #649). The copy of a dataset brings the payload sources that its registry serves. A payload source that both the registry and the store lack gets its own step, and the copy waits for it. That copy has with_payload_sources=False. So a copy that cannot be completed fails before any download.
  • The planner now makes the build step of the requested release too. A step waits for the nearest steps that it depends on, also through a release without a step.
  • build_release() is now plan_release_build() and then run_plan(). run_plan() copies only through copy_release().
  • BuildStep has force. A forced build step builds its release again, over any copy in the store. Two cases force the build step of the requested release: the caller asks for force, or the store holds a damaged copy of that release. So run_plan() takes no force.
  • The CLI, timenet_connectors.build() and the client keep their keywords for now and map them onto parents_from. The hint for a missing parent names only the options that the build did not use. If a payload source is missing, copy_release() names both registries.
  • Size: This PR has about 1580 changed lines: about 980 in the source and 600 in tests. No split keeps each part green without a later PR rewriting the code of an earlier one. Start the review with packages/timenet/src/timenet/coordinator/plan.py and build.py. They hold the planner core, about 770 of the changed lines. The rest is the callers, ask= in sources.py and the migrated tests.

Design decisions

  • In one plan, the planner asks each source at most once for each release, and it keeps each answer. It takes the manifests from the answers, so the plan reads each manifest once. The run reads the manifests of the releases that it copies again, from the registry as it is at run time. A copy downloads every file of the release anyway, so the extra request makes no real difference.
  • After the walk, one check covers every pin. It checks each copied release against its registry and each held release against the store. For a release to build, all readers must pin each of its files alike. So a pin conflict stops the plan before any step, also for a plan that only builds.
  • The store is never a source for itself. The planner drops it from the list, also as another LocalRegistry over the same directory.
  • If no other registry provides a parent or a payload source that the store holds damaged, the damaged copy stops the plan. Nothing builds over it. Only a damaged copy of the requested release gets a forced build.
  • A registry that lists a release but does not serve its files does not count, so a later source can provide the release.

Breaking changes

  • A parent that no installed connector builds at its exact version raises TimeNetMissingParentError, not TimeFValidationError.
  • If InstalledConnectors() is in parents_from, a parent that a registry lists but does not serve now builds.

Stack

Changes later in the stack:

  • docs: update the docs for the build coordinator #683 holds the doc changes of this PR.
  • The mapping of --fetch-from, --build-parents and timenet_connectors.build(fetch_from=, build_parents=) onto parents_from is thin glue. "Defaults and the producer API" replaces it with timenet.build() and its CLI flags.
  • The client's mapping in _obtain() and _prepare() is thin glue. The next PR replaces both functions with the read coordinator.

Merge message

feat(coordinator)!: plan parents through an ordered list of sources

plan_steps() takes each release that the local registry lacks from the
first source in parents_from that provides its exact version. A registry
gives a copy step, and the installed connectors give a build step.

- CopyStep and BuildStep are typed steps, and run_plan() matches on the
  type. The plan also holds the build of the requested release.
- A release needs exactly the payload sources that its manifest lists.
  The planner applies this rule before any step runs, so a copy that
  cannot complete fails before any download.
- One check covers every pin before any step runs.
- A damaged copy of a parent stops the plan unless another registry
  provides it. Only a damaged copy of the requested release gets a
  forced build.

BREAKING CHANGE: plan_parents() becomes plan_steps(), which takes an
ordered list of sources in parents_from.

BREAKING CHANGE: A parent that no installed connector builds at its
exact version raises TimeNetMissingParentError instead of
TimeFValidationError. A parent that a registry lists but does not serve
builds if a connector provides it.

@karhu
karhu added this pull request to stack #650 November 4, 2026 12:04
@karhu
karhu force-pushed the tnet-157/10-build-sources branch from 3e8998f to 4b047e9 Compare November 4, 2026 12:42
@karhu
karhu force-pushed the tnet-157/10-build-sources branch from 4b047e9 to b430716 Compare November 4, 2026 13:08
@karhu
karhu force-pushed the tnet-157/10-build-sources branch 2 times, most recently from 3c83246 to b64f9a0 Compare November 4, 2026 13:34
@karhu
karhu force-pushed the tnet-157/10-build-sources branch 2 times, most recently from 49f127a to 45e1d83 Compare November 4, 2026 15:27
@karhu
karhu force-pushed the tnet-157/10-build-sources branch from 45e1d83 to a54370c Compare November 4, 2026 15:31
@karhu
karhu force-pushed the tnet-157/10-build-sources branch from a54370c to 5d1ef6e Compare November 4, 2026 20:02

@TimoStoff TimoStoff left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Boy that was a big one! I think I have some helpful comments, looks mostly good though I have to admit this was a bit at the limit of what I have time to review really properly….

@karhu
karhu force-pushed the tnet-157/10-build-sources branch 2 times, most recently from 1112233 to c10efe0 Compare November 7, 2026 15:20
@karhu
karhu force-pushed the tnet-157/10-build-sources branch from c10efe0 to 7dce03e Compare November 7, 2026 16:27

@TimoStoff TimoStoff left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on second thoughs….lgtm

@karhu
karhu force-pushed the tnet-157/10-build-sources branch 2 times, most recently from 89068d6 to bf854e7 Compare November 9, 2026 13:56
@karhu
karhu force-pushed the tnet-157/10-build-sources branch from bf854e7 to 9cab11e Compare November 10, 2026 14:39

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and found no bugs. It is a large breaking redesign of the build planner, and new commits landed after the last approval, so a human should still look at it.

What was reviewed:

  • plan_release_build and run_plan in coordinator/build.py. Every CopyStep now copies only through copy_release with with_payload_sources, and every BuildStep builds with the InstalledBuilder the lookup stored and its own force.
  • The glue in client.py (_prepare, _obtain), api.py and cli.py that turns fetch_from / build_parents / auto_build into parents_from, including the new hints that name only the options the build did not use.
  • The _Graph walk in plan.py. This covers source ordering, damaged local copies, payload-source steps, and the pin check, which runs once before any step.
Extended reasoning...

The change rewrites the build coordinator (plan.py, build.py), adds ask= to sources.py, changes the missing-parent message in registry/_copy.py, and updates the client, the connectors API and the CLI. It is about 1,570 changed lines across 11 files. It touches no auth, crypto or injection surface. I deferred because it is large and breaking, and it makes design decisions a maintainer should weigh: source ordering, the store never being its own source, damaged copies stopping the plan, and the error type change from TimeFValidationError to TimeNetMissingParentError. There were also commits after the reviewer's approval.

@karhu
karhu force-pushed the tnet-157/10-build-sources branch from 9cab11e to 2cf6965 Compare November 10, 2026 15:50
@karhu
karhu removed this pull request from stack #699 November 10, 2026 15:58

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new blocking issues. 1 optional suggestion (a nit or a note on pre-existing code) was found and not posted. Nothing in this review needs a push before merging.

@karhu
karhu force-pushed the tnet-157/10-build-sources branch from 2cf6965 to 7449854 Compare November 10, 2026 16:24
@karhu
karhu added this pull request to stack #772 November 10, 2026 16:24

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped: your organization has reached its API usage limit, so Code Review can't run right now.

An organization admin can review the limit in your organization's usage and billing settings.

Once the limit resets or is raised, someone with write access to this repository can comment @claude review on this pull request to trigger a review.

A build gets each parent that the store lacks from one ordered list of
sources: registries to copy from, and InstalledConnectors() to build
with. Before this change, the planner took one registry (fetch_from)
and one flag (build_parents).

The planner now takes parents_from. For each
release that the store lacks or holds damaged, resolve_release() picks
the first source that provides its exact version. A registry gives a
CopyStep from that registry, and InstalledConnectors() gives a
BuildStep with the InstalledBuilder of the lookup. Step is the union of
the two types. A registry that lists a release but does not serve its
files does not count, so a later source can provide the release.

A release needs exactly the releases that its manifest lists as
payload sources, never the payload sources of a payload source. The
copy of a parent brings each of its payload sources that the registry
serves. The planner finds them before any step runs, with
find_payload_sources(), the walk and access check that copy_release()
uses too. A payload source that both the registry and the store lack
gets its own step, from its own source, and the copy waits for it. So
a copy that cannot be completed fails before any download. A copy of a
payload source alone takes only its own files
(with_payload_sources=False).

After the walk, the planner checks the pins of each pinned release: a
copied release against its registry, a held release against the store,
and for a release to build, that its readers pin each file alike. So
these errors stop the plan before any step runs.

In one plan, the planner asks each source at most once for each
release, and it keeps each answer. It takes the manifests from the
answers, which a registry that is not local fills. It reads a manifest
itself only from the store and from a local registry. So the hosted
registry gets one request for each manifest. A BuildStep holds the
InstalledBuilder that the lookup read, and run_plan() builds with it.
So a plan and its run read the card of each connector once.
plan_steps() takes the builder of the requested release.

The planner now makes the build step of the requested release too. A
step waits for the nearest steps that it depends on, also through a
release without a step. For example, if the store holds a parent but
not its payload source, the build waits for the copy of that payload
source.

The store is never a source for itself. The planner drops it from the
list, also as another LocalRegistry over the same directory, so it
never copies the store onto itself. A damaged copy of a parent stops
the plan if no other registry provides it. A damaged copy of the
requested release gets a forced build, so the plan never ends with
"Nothing to do" for it.

build_release() and plan_parents() go away. plan_release_build() plans
a build, and run_plan() runs the plan with one arm for each step type.
plan_release_build() checks the connector and the version, and then it
plans. Each BuildStep has a force flag. plan_release_build(force=True)
sets it on the build of the requested release, and run_plan() passes it
to the backend. run_plan() copies only through copy_release().

The CLI, timenet_connectors.build() and the client keep their keywords
for now. Each maps them onto parents_from: the registry of --fetch-from
first, then InstalledConnectors() if --build-parents is set. The client
always adds InstalledConnectors(). The hint of a missing parent names
only the options that the build did not use. If a payload source is
missing, copy_release() now names both registries.

BREAKING CHANGE: A parent that no installed connector builds at its
exact version raises TimeNetMissingParentError, not
TimeFValidationError. A parent that a registry lists but does not serve
now builds when the build may build parents: with --build-parents,
build_parents=True, or the auto_build of the client.

Part of TNET-157.
The comments in the connectors API and CLI that map fetch_from and
build_parents onto a list of sources now start with "Glue:".

Part of TNET-157.
The glue comments of this change start with TODO(adrian), so a search
finds each one that is left.

Part of TNET-157.
Say only what a caller of the build plan and of its list of sources needs to know.

Part of TNET-157.
Shorter docstrings for the code that this change adds or changes.

Part of TNET-157.
Drop tests that cannot fail or repeat other tests, and sharpen the rest.

Part of TNET-157.
@karhu
karhu force-pushed the tnet-157/10-build-sources branch from 7449854 to fef6ac7 Compare November 10, 2026 17:41
@karhu
karhu merged commit 14fe77d into main Nov 10, 2026
4 checks passed
@karhu
karhu deleted the tnet-157/10-build-sources branch November 10, 2026 18:24
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