Skip to content

Make lc a runner on ASTRA's resolved analysis, and answer for the runner's rules - #273

Draft
EiffL wants to merge 6 commits into
mainfrom
astra-0.1-runner
Draft

EiffL wants to merge 6 commits into
mainfrom
astra-0.1-runner

Conversation

@EiffL

@EiffL EiffL commented Nov 14, 2026 •

Copy link
Copy Markdown
Member

Why

ASTRA 0.1 changes what a spec is and who answers what. Universes live in astra.yaml, and the specification fixes where a result and its provenance record go and what makes a result stale. It names three tools: a validator, a checker and a runner, and gives the runner a layer of rules of its own.

This PR makes lc the runner and nothing else, on the astra-tools API of astra-project/astra-tools#138, whose data model is proposed in astra-project/astra-spec#117 (RFC 0009). It supersedes #268.

The split

Question Who answers How lc asks
Is the analysis valid? What are its results, where does each go, what does each read, what command makes it? Does each record still hold? astra-tools One call, astra.api.check, per verb
Which results to make now, in what order, where? Which environment, sandbox, storage, commit? What does a record say of its run? lc n/a

api.check returns the resolved analysis and one status per result, as plain data. lc imports astra through astra.api only, plus astra.scaffold (the empty spec) and astra.papers.cache (the viewer).

What the engine no longer does

ASTRA's model gives every path from the project root and each command already rendered, so the code that converted paths between frames and filled placeholders is gone.

  • plan.build is lookups. A Task is ASTRA's own Result plus what a run needs resolved once from the model's references: the folder the recipe runs in, the file each entry reads, the options it is made with, what its recipe asks for.
  • A task key is ASTRA's result id: fit#baseline, stage.fit#u1, and stage:fit#v1 for a result that belongs to another analysis (it was stage/fit#v1).
  • One status per result, where there was one per place.
  • Records are written by api.write_records, one at each place of a result, from the facts of the run. lc hashes its own files (assets.digest_file).
  • The viewer's run route reads a record as the JSON it is, with no astra import.

The one rule lc adds to the checker's

The checker says what a result breaks in itself (Status.why). Whether a result that only reads a rebuilt one must be rebuilt too is lc's rule, in assets.classify: compare the bytes each result it reads has now with the bytes its record says were read. A task passes what its upstreams actually produced; the driver passes what their records say, or None while one may still be remade. A byte-identical rebuild still stops a cascade, with no second check and nothing asked of ASTRA from a worker.

The specification's runner rules

Each rule of the runner layer is kept in one place, and tests/test_runner_rules.py has a test named after each, on a real project. The suite reads the rules from the installed astra-spec, so a rule a later release adds fails there until it is answered. AGENTS.md has the table.

Rule Where
UNRECORDED_RESULT, FALSE_RECORD, WRONG_WORKING_DIRECTORY worker.execute
UNRECREATABLE_ENVIRONMENT new check: a run is refused while a file the environment's identity reads is not tracked by git. A clean tree does not rule that out, since an ignored file is clean.
UNCOMMITTED_CHANGES The dirty-tree refusal, and a new check as each made result arrives (below).
REBUILT_FROM_COMMIT A result that belongs to an analysis declared with a commit is no task.
NOT_AT_COMMIT, STALE_INPUT Kept by refusal: lc builds nothing from an analysis at a commit.
UNDECLARED_READ The exec boundary, for files outside the project. Inside it the tree is readable whole, so there the rule is the author's to keep.
CODE_OUTSIDE_ANALYSIS ASTRA finds it; lc reports it with every run and --check.

A behavior change to decide on

An edit or a commit while a run is in flight now fails the results that arrive after it. Before, the run finished, committed them, and warned that "the records name the starting commit, which no longer describes this code". That warning described a record that is false, and the specification makes both rules a MUST. Now such a result is restored and reported failed, with what was edited; results committed before the edit stand, and the edit itself is left untouched.

The cost is real: touching a README during a long run discards the results that finish afterwards. If that is too strict in practice, the check is one call in materialize._consume and can go back to a warning.

Dependencies

astra-tools is pinned by git to the head of astra-project/astra-tools#138 until that is released. astra-spec>=0.1.0rc1 is a direct dependency only so that a resolver accepts the release candidate.

Known gaps

  • lc view's page needs @astra-spec/sdk and @astra-spec/ui for 0.1; the browser test fails in CI until then.
  • .github/workflows/eval.yml and evals/ are still 0.0.14.
  • What the record cannot hold is not kept: what the sandbox enforced, the image id, the uv version. A slot for runner attestation is a change to the specification.
  • An analysis declared with a commit is refused wherever a result would be built from it. ASTRA's model has no command for such a result (the RFC's open question).
  • lc run grants a probe no declared input while the spec is invalid, as only a valid analysis resolves. Files inside the project are readable anyway.
  • No migration for 0.0.14 projects, by the no-back-compat rule.

Tests

Locally, with LC_SANDBOX_TESTS_REQUIRED=1 LC_CRATE_TESTS_REQUIRED=1: 1261 passed, 1 failed. ruff check and mypy src/ are clean.

The failure is tests/test_view_browser.py::test_open_dialog_follows_artifacts_and_late_provenance, the first known gap above: the page refuses a 0.1 analysis. It is left failing rather than marked expected, so the PR cannot merge with a viewer that does not work.

New or re-expressed tests pin:

  • each runner rule, by its code (tests/test_runner_rules.py, 16 tests), including a result with two names being made once and recorded at each place, and the two mutation-checked boundary tests;
  • an edit, a stage and a foreign commit while a run is in flight;
  • classify on the level of each diagnostic in why, with read=None, with the recorded digest and with another;
  • a task asking ASTRA nothing about the project (check, resolve and validate all raise inside it), and a run asking api.check once however far a remake cascades;
  • what a task looks up: its folder, the file each entry reads, its decisions by ref, and keys for a result that belongs to another analysis;
  • the crate's ids for decisions by ref and for a result of another analysis.

🤖 Generated with Claude Code

EiffL and others added 5 commits November 11, 2026 14:23
The old file was a ~1,500-line per-layer record written while the
codebase was being re-added layer by layer. That rebuild is done, so the
file now describes the code as it stands: commands, the review rules,
and the architecture that spans modules.

Also drop the remaining rebuild wording from the engine docstring and
two test comments.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
ASTRA 0.1 moves universes into astra.yaml, defines where a result and
its provenance record go, and says when a record still describes the
analysis. astra-tools now answers all of that through `astra.api`, so
lc stops deriving it and becomes the runner only.

- plan.py asks `astra.api.plan` and carries each planned result across
  as a picklable Task keyed by its address (`fit#baseline`, `fit` for an
  output built once, `stage.fit#u1` driven, `stage/fit#v1` owned by a
  declared analysis). Paths are measured from the project root. Results
  of an analysis declared with a `commit` are refused.
- assets.classify asks `astra.api.check` instead of hashing recipes and
  inputs; lc adds only `behind` (the record names an earlier
  environment) and the git-history foreign-write test.
- The worker writes ASTRA records under `.astra/records/`, naming the
  environment `urn:lightcone:env:<env_version>`, with an
  `astra.environments` entry point for `astra check`. A recipe runs in
  its own analysis's folder. The manifest keeps only what the closed
  record cannot hold.
- A task downstream of a possible remake re-asks the checker once its
  upstreams have landed, so a byte-identical rebuild still stops the
  cascade.
- Scaffold, .gitattributes (nested `results/` are annexed), the RO-Crate
  renderer, `lc run` input discovery, the `lc view` server routes and
  the docs follow.

ASTRA 0.1 is not on PyPI yet: astra-tools and astra-spec are pinned to
git commits. The `lc view` page still needs the 0.1 SDK, and the eval
workflow is not migrated.

Tests are re-expressed as 0.1 specs; test_composition.py runs the
specification's own shapes (pins, imports, derived_from, when, driving)
end to end.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: Francois Lanusse <fr.eiffel@gmail.com>
… result

The manifest held what ASTRA's closed provenance record cannot: the
rendered recipe, the decisions, the git remote, the uv version, the
sandbox's hermeticity attestation and the image. None of it is kept
any more, so `.<name>.manifest.json`, `assets.Manifest` and the helpers
that existed only to fill it (`project.uv_version`,
`Runtime.manifest_image`, `Runtime.arch`) are gone, with the
`.gitattributes` lines that kept manifests out of the annex.

- A run commits two files per place, the bytes and the record; the
  foreign-write check looks at those two.
- The RO-Crate takes the repository's remote as an argument, describes
  each run with the task's command and decisions as the spec now stands,
  and no longer has a ContainerImage entity.
- The warning about an output the spec dropped is now raised from its
  orphaned record.
- `/lightcone/api/run` no longer answers `recipe`.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: Francois Lanusse <fr.eiffel@gmail.com>
The engine and the test suite re-expressed as 0.1 specs. What follows
rewrites every place that talks to astra-tools against its rescoped API.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
ASTRA 0.1 names three tools: a validator, a checker and a runner.
astra-tools is the first two; this makes lc the third, on the rescoped
`astra.api` (astra-project/astra-tools#138), in place of the earlier
port's workarounds.

- One question per verb. `plan.build` calls `astra.api.check` once and
  gets the planned results and how each record stands together, kept
  as `Graph.found`. The earlier port asked `api.plan` and `api.check`
  separately.
- A task asks ASTRA nothing but a pure rule. The driver hands each task
  what the checker found; once its upstreams have landed, the task
  applies `Status.unchanged_given` to the bytes they produced. No
  check and no git inside a worker, and a byte-identical rebuild still
  stops the cascade.
- One classification rule with one varying input: `classify(found,
  environment, read, foreign)`. A task gives what its upstreams made;
  the driver gives what their records say, or None while one may still
  be remade.
- lc fills its own commands with `api.fill`, relative to the folder the
  recipe runs in. ASTRA owns the grammar, lc the paths.
- No environment plug-in. lc minted the IRI, so `lc status --json`
  gives each result's recorded environment beside the project's.
- The crate takes its records from the checker and the label and
  description from the resolved analysis; the probe binds offline; the
  scaffold is astra's public one again.
- lc imports astra through `astra.api`, `astra.scaffold` and
  `astra.papers.cache` only.

AGENTS.md records the split, the changed invariants and what was
decided against.

astra-tools is pinned by git to the head of #138 until it is released.
The viewer's browser test fails: its page reads the spec with the
released SDK, which does not read 0.1 yet.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
astra-tools now answers in one plain data model: api.check returns the
resolved analysis, with every path from the project root and each command
as it runs, and one status per result. The engine no longer converts paths
between frames or fills a command. A task is ASTRA's own Result with what a
run needs looked up once, and its key is ASTRA's id for the result, so a
result that belongs to another analysis is stage:fit#v1.

Whether a result that only reads a rebuilt one must be rebuilt too is lc's
rule now, in classify: the bytes each result it reads has, against the
bytes its record says were read. Records are written by api.write_records,
one at each place, and lc hashes its own files.

Each rule of the specification's runner layer is kept in one place, with a
test named after it in tests/test_runner_rules.py. Two checks are new. A
run is refused while a file the environment's identity reads is not tracked
by git, as an ignored file leaves a tree clean (UNRECREATABLE_ENVIRONMENT).
And a result that arrives after the checkout was edited or committed to is
restored and reported failed, where the run used to commit it and warn
that its record named a commit that no longer described the code
(UNCOMMITTED_CHANGES).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@EiffL EiffL changed the title Make lc a runner on astra-tools' resolve API (ASTRA 0.1) Make lc a runner on ASTRA's resolved analysis, and answer for the runner's rules Nov 15, 2026

This branch has not been deployed

No deployments
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