lightcone-cli 0.3.0__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/PKG-INFO +1 -1
  2. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/agents/lc-extractor.md +1 -3
  3. lightcone_cli-0.3.1/claude/lightcone/guides/astra-reference.md +389 -0
  4. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/guides/lightcone-cli-reference.md +22 -8
  5. lightcone_cli-0.3.1/claude/lightcone/scripts/check-lc-run.sh +107 -0
  6. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/scripts/session-start.sh +24 -44
  7. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/skills/lc-migrate/SKILL.md +11 -8
  8. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/skills/lc-new/SKILL.md +9 -14
  9. lightcone_cli-0.3.1/claude/lightcone/templates/CLAUDE.md +25 -0
  10. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/cli/commands.py +81 -0
  11. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/status.py +8 -1
  12. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/eval/harness.py +24 -4
  13. lightcone_cli-0.3.0/claude/lightcone/guides/astra-reference.md +0 -338
  14. lightcone_cli-0.3.0/claude/lightcone/guides/ui-brand.md +0 -86
  15. lightcone_cli-0.3.0/claude/lightcone/scripts/check-lc-run.sh +0 -140
  16. lightcone_cli-0.3.0/claude/lightcone/skills/lc-build/SKILL.md +0 -92
  17. lightcone_cli-0.3.0/claude/lightcone/skills/lc-build/assets/loop-prompt.md +0 -91
  18. lightcone_cli-0.3.0/claude/lightcone/skills/lc-build/scripts/setup-lc-build.sh +0 -240
  19. lightcone_cli-0.3.0/claude/lightcone/skills/lc-verify/SKILL.md +0 -58
  20. lightcone_cli-0.3.0/claude/lightcone/templates/CLAUDE.md +0 -30
  21. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/.gitignore +0 -0
  22. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/LICENSE +0 -0
  23. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/hooks/langfuse_git_commit_hook.py +0 -0
  24. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/hooks/langfuse_hook.py +0 -0
  25. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/hooks/langfuse_prepare_commit_msg.py +0 -0
  26. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/hooks/langfuse_session_init_hook.py +0 -0
  27. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/hooks/langfuse_utils.py +0 -0
  28. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/scripts/activate-venv.sh +0 -0
  29. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/scripts/validate-on-save.sh +0 -0
  30. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/skills/lc-feedback/SKILL.md +0 -0
  31. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/pyproject.toml +0 -0
  32. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/cli/__init__.py +0 -0
  33. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/cli/plugin.py +0 -0
  34. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/__init__.py +0 -0
  35. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/container.py +0 -0
  36. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/dask_cluster.py +0 -0
  37. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/manifest.py +0 -0
  38. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/runner.py +0 -0
  39. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/scratch.py +0 -0
  40. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/site_registry.py +0 -0
  41. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/snakefile.py +0 -0
  42. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/tree.py +0 -0
  43. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/validation.py +0 -0
  44. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/verify.py +0 -0
  45. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/eval/__init__.py +0 -0
  46. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/eval/build.py +0 -0
  47. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/eval/cli.py +0 -0
  48. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/eval/graders.py +0 -0
  49. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/eval/models.py +0 -0
  50. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/eval/report.py +0 -0
  51. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/eval/sandbox.py +0 -0
  52. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/snakemake_executor_plugin_dask/__init__.py +0 -0
  53. {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/snakemake_executor_plugin_dask/executor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lightcone-cli
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Lightcone command-line toolchain
5
5
  Author: Lightcone Research
6
6
  License: BSD-3-Clause
@@ -69,12 +69,10 @@ prior_insights:
69
69
  doi: "[DOI]"
70
70
  version: <version if arXiv, omit otherwise>
71
71
  quote:
72
- type: TextQuoteSelector
73
72
  exact: "<VERIFIED exact quote from paper>"
74
73
  prefix: "<~20-100 chars BEFORE the quote>"
75
74
  suffix: "<~20-100 chars AFTER the quote>"
76
75
  location:
77
- type: FragmentSelector
78
76
  page: <page number hint>
79
77
  scope: "<when this applies -- optional, include only if the claim has limited applicability>"
80
78
 
@@ -111,4 +109,4 @@ verification_summary:
111
109
  | `prefix/suffix mismatch` | Context text does not match surrounding text | Re-read the area around the quote, copy exact surrounding text |
112
110
  | Persistent `not_found` | OCR artifacts, ligatures, or Unicode differences | Try shorter quote avoiding problem characters; increase prefix/suffix |
113
111
 
114
- **Recovery**: Re-read the failing page, copy the exact text, update prefix/suffix, verify with `astra paper verify-quote`, then run `astra validate astra.yaml --verify-evidence`.
112
+ **Recovery**: Re-read the failing page, copy the exact text, update prefix/suffix, verify with `astra paper verify-quotes`, then run `astra validate astra.yaml --verify-evidence`.
@@ -0,0 +1,389 @@
1
+ # ASTRA Reference
2
+
3
+ ## What an ASTRA Analysis Is
4
+
5
+ An ASTRA analysis is a structured layer between the code and the paper. It surfaces the inputs a computation depends on, the outputs it produces, and -- critically -- every methodological decision that could plausibly affect the results. The goal is to make the full decision space explicit and machine-readable, so that alternative defensible choices can be systematically explored rather than silently baked in.
6
+
7
+ An `astra.yaml` spec captures this for a single unit of work. The structure is **self-similar**: a top-level analysis and a nested sub-analysis have exactly the same shape. Everything in this reference applies equally to both.
8
+
9
+ ## astra.yaml Structure
10
+
11
+ Fields: `id`, `version`, `name`, `narrative`, `authors`, `tags`, `inputs`, `outputs`, `decisions`, `prior_insights`, `findings`, `analyses`, `container`. `narrative` is the analysis-level prose field -- see [Narrative](#narrative) (typically filled in later, once the structural pieces have settled).
12
+
13
+ **Reserved IDs.** No analysis entity (input, output, decision, option, finding, prior insight, evidence, sub-analysis) may use any of these names as its `id` -- they collide with the narrative anchor grammar:
14
+
15
+ ```
16
+ inputs outputs decisions findings prior_insights
17
+ analyses options content narrative
18
+ ```
19
+
20
+ **`label` field.** Inputs, Outputs, Decisions, Options, and Insights all accept an optional `label:` -- a short human-readable name for compact rendering (margin glyphs, breadcrumbs, card titles). Tooling falls back to `id` when absent. `label` is required only on Options.
21
+
22
+ ```yaml
23
+ # Simple analysis -- everything at top level
24
+ version: "1.0"
25
+ name: "My Analysis"
26
+ # narrative: { ... } # see Narrative section; typically added later
27
+ inputs:
28
+ - id: training_data
29
+ type: data
30
+ source: "data/train.csv"
31
+ decisions:
32
+ scaling:
33
+ label: "Feature Scaling"
34
+ tags: [preprocessing] # optional freeform tags for grouping
35
+ rationale: "Affects convergence"
36
+ default: standard
37
+ options:
38
+ standard: { label: "StandardScaler" }
39
+ minmax: { label: "MinMaxScaler" }
40
+ use_pca:
41
+ label: "Use PCA"
42
+ default: "no"
43
+ options:
44
+ "yes": { label: "Yes" }
45
+ "no": { label: "No" }
46
+ n_components:
47
+ label: "PCA Components"
48
+ default: "50"
49
+ options:
50
+ "50": { label: "50 components" }
51
+ "100": { label: "100 components" }
52
+ outputs:
53
+ - id: accuracy
54
+ type: metric
55
+ inputs: [training_data] # provenance lives on the Output
56
+ decisions: [scaling, use_pca, n_components] # not on the recipe
57
+ recipe:
58
+ command: >-
59
+ python src/evaluate.py
60
+ --train {inputs.training_data}
61
+ --scaling {decisions.scaling}
62
+ --out {output}
63
+ container: Containerfile
64
+ ```
65
+
66
+ ### Cross-Analysis Inputs
67
+
68
+ To consume the outputs of a separate ASTRA analysis as a whole-cloth dependency, declare an Input of `type: analysis` with `ref:` (and optionally `ref_version:` and `use_outputs:`):
69
+
70
+ ```yaml
71
+ inputs:
72
+ - id: prior_study
73
+ type: analysis
74
+ ref: analyses/preprocessing_comparison
75
+ ref_version: "v1.2"
76
+ use_outputs: [best_method, performance_table]
77
+ ```
78
+
79
+ This is distinct from `from:` -- `ref` points to an external analysis by reference; `from:` aliases an element within the current analysis tree (see [Composition Mechanics](#composition-mechanics)).
80
+
81
+ ## Decisions
82
+
83
+ A decision is a methodological choice where a different defensible option could plausibly produce a different numerical result. Include it if changing the choice could shift a quantitative outcome -- even modestly. Many small decisions can compound. When in doubt, include it.
84
+
85
+ **Not decisions -- skip these:**
86
+
87
+ - **Tooling choices** that produce identical numerical results: programming language, library/framework (PyTorch vs TensorFlow), file format, parallelization strategy, plotting style.
88
+ - **Fixed constraints** with no degrees of freedom: "use the data that exists," "satisfy the grant requirements."
89
+ - **What to produce** -- decisions control *how* something is computed, not *what* outputs exist. Outputs are fixed by the analysis structure.
90
+
91
+ **These ARE decisions -- do not skip:**
92
+
93
+ - Algorithmic choices (MCMC vs optimization, KDE vs histogram, smoothing method)
94
+ - Numerical parameters and thresholds (sigma clipping level, bin width, convergence criterion, iteration count)
95
+ - Statistical method choices (bootstrap vs analytic errors, Bayesian vs frequentist)
96
+ - Data selection criteria (quality cuts, magnitude limits, spatial boundaries)
97
+ - Correction and calibration choices (which reddening law, which zero-point, which prior)
98
+
99
+ ### Tags
100
+
101
+ Decisions may carry an optional `tags:` list for grouping (e.g. `[preprocessing]`, `[physics]`, `[stats]`). Keep the tag vocabulary **small and consolidated** -- reuse existing tags rather than minting new ones, since tags are mostly useful for cross-cutting views over a shared decision space, and that view fragments quickly when every decision invents its own label.
102
+
103
+ ### Parameterization
104
+
105
+ **Every decision must be parameterized in code** -- never hardcode a decision value. The recipe's `command:` template references it via `{decisions.<id>}` (see [Command Template Substitution](#command-template-substitution)).
106
+
107
+ ### Constraints
108
+
109
+ - `when: "decision.option"` -- decision only exists given an upstream choice (e.g., `svm_kernel` only exists `when: model.svm`)
110
+ - `incompatible_with: ["decision.option"]` -- cannot coexist in a universe
111
+ - `requires: ["decision.option"]` -- must be selected together
112
+ - `excluded: true` + `excluded_reason: "..."` -- option considered but rejected (cannot be default or selected)
113
+
114
+ ## Recipe Format
115
+
116
+ ASTRA is asset-centric: the **Output** declares its provenance (`inputs`, `decisions`) and when it's active (`when`); the recipe is pure *how*. Recipe fields: `command` (required), `container`, `resources`.
117
+
118
+ ```yaml
119
+ outputs:
120
+ - id: accuracy
121
+ type: metric
122
+ inputs: [trained_model] # Dependencies live on the Output
123
+ decisions: [scaling, classifier] # Decisions that parameterize this output
124
+ recipe:
125
+ command: >-
126
+ python src/evaluate.py
127
+ --model {inputs.trained_model}
128
+ --scaling {decisions.scaling}
129
+ --out {output}
130
+ container: ghcr.io/proj/ml:latest # Overrides analysis-level default
131
+ resources: { cpus: 4, memory: "32GB", gpus: 1, time_limit: "2h" }
132
+ ```
133
+
134
+ Set `container:` at analysis level (all recipes inherit); per-recipe `container:` overrides. Pass either a container image name (e.g., `python:3.12-slim`, `ghcr.io/org/img:latest`) or a path to a Containerfile (e.g., `Containerfile`, `containers/Dockerfile`). The runtime figures out whether to pull or build.
135
+
136
+ ### Command Template Substitution
137
+
138
+ Runners expand `{...}` placeholders in `command:` before invoking it: `{inputs.<id>}` (input path), `{inputs}` (all input paths, declared order), `{decisions.<id>}` (active option ID), `{output}` (artifact path), `{{`/`}}` (literal braces). Every `{inputs.<id>}` and `{decisions.<id>}` must name something declared in the parent Output's `inputs:`/`decisions:` lists -- always **local IDs** (no `../`; bridging is declared once at the Input/Decision via `from:`).
139
+
140
+ ### Conditional Outputs
141
+
142
+ Outputs can have `when` conditions -- the output only exists when the condition is met for a given universe. Uses the same syntax as decision `when` (negation with `~`, lists AND'd).
143
+
144
+ ```yaml
145
+ outputs:
146
+ - id: faint_metrics
147
+ type: metric
148
+ when: "~training_sample.bright_only" # Only when NOT bright_only
149
+ recipe: { command: python src/evaluate.py }
150
+ - id: combined_report
151
+ type: report
152
+ when: ["~training_sample.bright_only", model.svm] # AND: both must be true
153
+ recipe: { command: python src/combo.py }
154
+ ```
155
+
156
+ ## Universe Management
157
+
158
+ A universe selects one option per decision -- a defensible alternative analysis path. Bug fixes and refactors are normal commits, not universes. Universe IDs use the pattern `^[a-z][a-z0-9_-]*$` (hyphens allowed, unlike other ASTRA IDs).
159
+
160
+ ```bash
161
+ astra universe generate -n experiment1 -d "Testing hypothesis X"
162
+ # Edit universes/experiment1.yaml, then run with the runner of your choice.
163
+ ```
164
+
165
+ **Adding a new decision:** (1) add to `astra.yaml` with options/default/rationale, (2) add parameter to code, (3) add to all existing universe files with default, (4) create new universe, (5) `astra validate astra.yaml`.
166
+
167
+ ## Prior Insights and Findings
168
+
169
+ Two kinds of insight, distinguished by direction:
170
+
171
+ - **Prior insights** (`prior_insights:`) — knowledge from outside the analysis that informs decisions. From literature (by DOI) or artifacts from a prior/parent analysis.
172
+ - **Findings** (`findings:`) — conclusions from the analysis itself, backed by its own output artifacts.
173
+
174
+ Both use the same Insight model: `id`, `label` (optional), `claim`, `created_at`, `evidence`, plus optional `derived` (true if synthesized/inferred from multiple sources), `scope` (applicability conditions), `tags`, `notes`. Placement determines direction.
175
+
176
+ Each evidence item has its own fields: `id`, exactly one of `doi` (literature) or `artifact` (output ID), and either a `quote` (TextQuoteSelector with required `exact`, optional `prefix`/`suffix`) or `location` (FragmentSelector with `value` like `"page=6"` and/or 1-indexed `page`). DOI evidence may add `version` (arXiv version). Artifact evidence may add `snapshot` (path to an immutable artifact copy) and `source_commit` (git commit that produced it).
177
+
178
+ ```yaml
179
+ prior_insights:
180
+ layer_norm_stability:
181
+ id: layer_norm_stability
182
+ label: "LN stability"
183
+ claim: "Layer normalization improves training stability"
184
+ created_at: "2025-01-15T10:30:00"
185
+ derived: false
186
+ scope: "Transformer training with batch sizes < 64"
187
+ tags: [optimization]
188
+ evidence:
189
+ - id: e1
190
+ doi: "10.48550/arXiv.1607.06450"
191
+ version: 1
192
+ quote: { exact: "Exact text", prefix: "~20-100 chars before", suffix: "~20-100 chars after" }
193
+ location: { value: "page=5", page: 5 }
194
+
195
+ findings:
196
+ scaling_result:
197
+ id: scaling_result
198
+ claim: "StandardScaler achieves 97% accuracy vs 91% for MinMaxScaler"
199
+ created_at: "2025-02-01T14:00:00"
200
+ derived: true
201
+ evidence:
202
+ - id: e1
203
+ artifact: accuracy # references a declared output ID
204
+ snapshot: "snapshots/run_2025-02-01.json"
205
+ source_commit: "a3f9c12"
206
+ - id: e2
207
+ artifact: model_comparison
208
+ quote: { exact: "StandardScaler achieved 97% accuracy vs 91% for MinMaxScaler" }
209
+ ```
210
+
211
+ Link prior insights to decisions: `options: { layer_norm: { insights: [layer_norm_stability] } }`
212
+
213
+ Artifact references are validated against declared outputs — `astra validate` flags any `artifact:` that doesn't match an output ID. Literature evidence (DOI) requires a `quote` (a `TextQuoteSelector` with required `exact` plus optional `prefix`/`suffix`); artifact evidence does not. Each evidence item must set exactly one of `doi` or `artifact`.
214
+
215
+ **Sub-analysis findings as prior insights:** When a sub-analysis explores a specific question (calibration study, simulation validation, sensitivity test), its findings can inform decisions elsewhere. The parent or sibling references the sub-analysis output as artifact evidence in its own `prior_insights`, e.g. `artifact: "build_mocks.noise_diagnostics"`. This creates a traceable chain from sub-analysis conclusion to downstream decision.
216
+
217
+ ### Adding a Paper as Prior Insight
218
+
219
+ Found a paper through literature search? Three steps to wire it into the analysis:
220
+
221
+ 1. **Cache the PDF** — `astra paper add <doi>` downloads it to the project's paper cache. Pass `--pdf PATH` if you already have a local copy, or `--version N` for a specific arXiv version.
222
+ 2. **Add a `prior_insights:` entry** that cites the DOI (and optionally `version`) under `evidence:`. The `quote.exact` text must match the PDF verbatim; optional `prefix`/`suffix` (~20–100 chars on either side) disambiguate when the exact string occurs more than once.
223
+ 3. **Verify** — `astra paper verify-quotes <doi>` for one paper, or `astra validate astra.yaml --verify-evidence` to check every quote in the spec. A wrong `exact` string fails validation.
224
+
225
+ `astra paper list` shows what's cached; `astra paper path <doi>` prints the PDF path so you can open it for review.
226
+
227
+ ## Sub-Analyses
228
+
229
+ ### What a Sub-Analysis Is
230
+
231
+ Each `astra.yaml` -- root or nested -- represents a **unit of work**: meaningful inputs, methodological decisions, meaningful outputs. A sub-analysis is one of these units nested inside a larger analysis. It can be understood, executed, and evaluated on its own terms.
232
+
233
+ ### When to Split
234
+
235
+ Default to a **single analysis**. Split into sub-analyses only when:
236
+
237
+ - **Decision ownership** -- the stage has its own decisions that could meaningfully vary, clearly scoped to that stage rather than the broader analysis. Shared decisions live at the parent (`from: ../`); stage-specific decisions live in the sub-analysis. If you can't cleanly assign decisions to levels, the split is probably wrong.
238
+ - **Reusability** -- someone working on a different paper could use this stage's output as-is (a cleaned catalog, a trained emulator, a set of mocks).
239
+ - **Side quests** -- independent investigations (diagnostics, calibrations, simulation studies) that have different inputs/outputs/code from the main analysis are sub-analyses, not universes. Universes are different parameter choices on the same pipeline.
240
+ - **If boundaries are unclear**, start flat and split later when they become explicit: separate stage outputs, explicit `from` links, clear decision ownership per level.
241
+
242
+ ### Worked Examples
243
+
244
+ #### Two-Stage Pipeline (DAG Split)
245
+
246
+ A paper builds mock galaxy catalogs, then trains a neural network on them for photometric redshift estimation. Natural split:
247
+ - **`build_mocks`**: simulation inputs + survey properties, decisions about noise model and selection function. Produces mock catalogs.
248
+ - **`photo_z`**: mocks (from sibling) + real survey data, decisions about network architecture and training. Produces redshift estimates.
249
+
250
+ The mock-building decisions are independent from training decisions. Someone could reuse the mocks for a different estimator.
251
+
252
+ #### When NOT to Split
253
+
254
+ A paper downloads galaxies, applies quality cuts, corrects for extinction, computes luminosity functions, fits a Schechter function. Five steps -- but one objective, shared decisions, one end product.
255
+
256
+ ### Anti-Patterns
257
+
258
+ - **Splitting by script** rather than by analytical unit.
259
+ - **Zero-decision sub-analyses** that just pass data through -- make these output recipes in the parent.
260
+ - **Premature splitting.** Start flat, split when boundaries become explicit. Easier to split a working flat analysis than merge a broken hierarchical one.
261
+ - **Forcing a linear DAG.** Independent stages don't need to be wired in sequence just because the paper presents them that way.
262
+
263
+ ### Composition Mechanics
264
+
265
+ Sub-analyses can be **inline** (their content lives directly under the parent's `analyses:` map) or **external** (`path:` points to a directory with its own `astra.yaml`). `path:` is mutually exclusive with inline content -- a sub-analysis entry sets either `path:` or fields like `inputs`/`outputs`/`decisions`, not both. The parent below uses external sub-analyses:
266
+
267
+ ```yaml
268
+ # Root astra.yaml
269
+ inputs:
270
+ - id: survey_catalog
271
+ type: data
272
+ source: "data/survey.parquet"
273
+ decisions:
274
+ cosmology_model: # Shared across stages
275
+ label: "Cosmological Model"
276
+ tags: [physics]
277
+ default: flat_lcdm
278
+ options:
279
+ flat_lcdm: { label: "Flat LCDM" }
280
+ wcdm: { label: "wCDM" }
281
+ outputs:
282
+ - id: trained_model
283
+ from: train_network.trained_model # Re-export from sub-analysis (pure alias)
284
+ analyses:
285
+ build_mocks:
286
+ path: ./analyses/build_mocks
287
+ train_network:
288
+ path: ./analyses/train_network
289
+ ```
290
+
291
+ Inside each sub-analysis's own `astra.yaml`, `from:` wires inputs and decisions to the parent or siblings:
292
+
293
+ ```yaml
294
+ # analyses/train_network/astra.yaml
295
+ inputs:
296
+ - id: training_data
297
+ from: ../build_mocks.mock_catalog # Sibling output (escape upward, then descend)
298
+ outputs:
299
+ - id: trained_model
300
+ type: data
301
+ inputs: [training_data]
302
+ decisions: [cosmology_model, noise_model]
303
+ recipe: { command: python src/train.py --train {inputs.training_data} --out {output}, resources: { gpus: 1, memory: "32GB" } }
304
+ decisions:
305
+ cosmology_model:
306
+ from: ../cosmology_model # Inherit parent decision
307
+ noise_model:
308
+ label: "Noise Model"
309
+ default: heteroscedastic
310
+ options:
311
+ homoscedastic: { label: "Homoscedastic" }
312
+ heteroscedastic: { label: "Heteroscedastic" }
313
+ ```
314
+
315
+ **Path grammar.** `from:` paths use a uniform tree-path syntax: `../` escapes one scope upward (stack as needed), and `name.subname` descends into a named child scope. Multiple levels work in either direction. Per-slot direction:
316
+
317
+ | Slot | Legal forms | Meaning |
318
+ |---|---|---|
319
+ | `Input.from` | `../id`, `../../id`, `../scope.out_id` | parent/ancestor Input, or a sibling sub's Output (escape up, then descend) |
320
+ | `Decision.from` | `../id`, `../../id` | parent/ancestor Decision (downward-only flow; share via common ancestor) |
321
+ | `Output.from` | `child.out_id`, `child.sub.out_id` | own child sub's Output (re-export; descend through nested children) |
322
+
323
+ `from:` makes the node a pure pointer -- only `id` and `from` (plus `when` on Outputs) are allowed; everything else (`type`, `description`, `source`, `options`, `default`, `recipe`, …) is inherited from the source.
324
+
325
+ The **`universe:` field** in universe files selects which sub-analysis universe to load: `build_mocks: { universe: baseline }` loads `./analyses/build_mocks/universes/baseline.yaml`.
326
+
327
+ ## Narrative
328
+
329
+ `narrative` is the analysis-level prose field on any Analysis (root or sub). It's structured as five Markdown sections: `summary`, `findings`, `methods`, `inputs`, `outputs`. The schema is closed (`additionalProperties: false`) -- no other keys are allowed.
330
+
331
+ **Recommendation:** fill `narrative` in *later*, once the structural pieces of the analysis (decisions, outputs, sub-analyses) have settled. Prose written too early goes stale fast and tends to describe what no longer exists. Per-element prose (what each Input, Output, Decision, Option, or Insight is and why) belongs on the elements themselves via `description`/`rationale`/`notes` -- those can be written from day one.
332
+
333
+ **Conditional coverage.** All five sections are schema-optional, but `astra validate` enforces:
334
+
335
+ | Section | Required when |
336
+ |---|---|
337
+ | `findings` | the analysis has entries under `findings:` |
338
+ | `methods` | the analysis has entries under `decisions:` or `analyses:` |
339
+ | `inputs` | the analysis has entries under `inputs:` |
340
+ | `outputs` | the analysis has entries under `outputs:` |
341
+ | `summary` | always optional |
342
+
343
+ Authors narrate what they declare; stub analyses with only a summary stay clean.
344
+
345
+ **Anchor references.** Inside any section, link to other elements with Markdown anchor links (`[text](#path.to.element)`) using the same tree-path grammar as `from:` -- `#decisions.scaling`, `#decisions.scaling.options.standard`, `#findings.best_model`, `#analyses.preprocessing` (whole sub-analysis), `#analyses.preprocessing.outputs.features` (element inside a sub-analysis), `#../decisions.method` to escape to a parent scope.
346
+
347
+ **Inline images.** Standard Markdown image syntax inside any section -- `![alt](path/to/img.png)` for repo-relative paths or `![alt](https://...)` for URLs. Renderers like lightcone-ui pick them up the same way they pick up text.
348
+
349
+ ```yaml
350
+ narrative:
351
+ summary: |
352
+ A two-stage pipeline for Iris classification that demonstrates
353
+ sub-analyses.
354
+ methods: |
355
+ The [feature_extraction sub-analysis](#analyses.feature_extraction)
356
+ produces encoded features, which feed
357
+ [classification](#analyses.classification). A
358
+ [test_split](#decisions.test_split) decision controls the holdout.
359
+ inputs: |
360
+ [iris_data](#inputs.iris_data) is Fisher's 150-sample, 4-feature,
361
+ 3-class dataset.
362
+ outputs: |
363
+ The top level exposes [accuracy](#outputs.accuracy) and a
364
+ [pipeline_summary](#outputs.pipeline_summary) report.
365
+ ```
366
+
367
+ ## CLI Reference (astra)
368
+
369
+ ```bash
370
+ astra init [DIRECTORY] # Scaffold a new analysis
371
+ astra validate astra.yaml # Validate (run after every change)
372
+ astra validate astra.yaml --verify-evidence # + verify insight quotes against PDFs
373
+ astra info [--decisions|--inputs|--outputs] # Analysis summary / element details
374
+ astra universe generate -n NAME [-d "desc"] # Generate universe from defaults
375
+ astra universe check universes/x.yaml # Check universe constraints
376
+ astra viz [--fmt ascii|mermaid] # Visualize decision space
377
+ astra schema show analysis|universe|insights # Show JSON schema
378
+ astra paper add DOI [--version N] [--pdf PATH] # Cache a paper for evidence checks
379
+ astra paper list # List cached papers
380
+ astra paper show DOI # Show metadata for a cached paper
381
+ astra paper path DOI [--version N] # Print the cached PDF's path
382
+ astra paper verify-quotes DOI # Batch-verify quotes; reads {"quotes":[...]} JSON from stdin
383
+ ```
384
+
385
+ ## Validation
386
+
387
+ Run `astra validate astra.yaml` after **every** spec change. Additional checks:
388
+ - Universe files: `astra universe check universes/<name>.yaml`
389
+ - Evidence quotes: `astra validate astra.yaml --verify-evidence`
@@ -5,14 +5,23 @@ Reference for lightcone-cli execution: CLI commands, development workflow, statu
5
5
  ## CLI Reference
6
6
 
7
7
  ```bash
8
- lc init [DIR] # Scaffold a new ASTRA project
9
- lc run [OUTPUT] [--universe NAME] # Materialize outputs
10
- lc build [--force] [--runtime docker] # Build container images from specs
11
- lc status [--universe NAME] [--json] # Materialization status (text or JSON)
12
- lc verify [--universe NAME] # Recompute hashes and walk the provenance chain
8
+ lc init [DIR] [--permissions yolo|recommended|minimal] [--scratch PATH] # Scaffold a new ASTRA project
9
+ lc run [OUTPUTS...] [--universe NAME] [--force] [--verbose] [--rerun-triggers TRIGGERS] # Materialize outputs
10
+ lc build [--force] [--runtime docker] # Build container images from specs
11
+ lc status [--universe NAME] [--json] # Materialization status (text or JSON)
12
+ lc verify [--universe NAME] # Recompute hashes and walk the provenance chain
13
+ lc eval {run,report,compare} # Run/inspect eval suites (requires the 'eval' extra)
13
14
  ```
14
15
 
15
- The first `lc` invocation auto-creates `~/.lightcone/config.yaml` with defaults; edit it directly to pin a container runtime or set the extraction model.
16
+ `lc run` is quiet by default — pass `--verbose` to see worker output. `--scratch` is only relevant on HPC sites where `$HOME` doesn't honor `flock` (NERSC etc.); it redirects Snakemake state and Dask spill onto the named filesystem.
17
+
18
+ The first `lc` invocation auto-creates `~/.lightcone/config.yaml`:
19
+
20
+ ```yaml
21
+ container:
22
+ runtime: auto # or: docker | podman | podman-hpc | none
23
+ # extraction_model: sonnet # optional; controls the lc-extractor agent
24
+ ```
16
25
 
17
26
  **Always run via `lc`.** Recipes must execute through `lc run` so that container builds, option resolution, resource limits, and result paths are applied. Treat the underlying execution engine as a black box — never invoke schedulers or container runtimes directly, that will bypass reproducibility guarantees.
18
27
 
@@ -20,7 +29,7 @@ The first `lc` invocation auto-creates `~/.lightcone/config.yaml` with defaults;
20
29
 
21
30
  Sub-analyses are scaffolded by hand, since each one is just another `astra.yaml` nested in a directory. To add one:
22
31
 
23
- 1. Create `analyses/<name>/` with its own `astra.yaml` (and optionally `scripts/`, `universes/baseline.yaml`, `results/`).
32
+ 1. Create `analyses/<name>/` with its own `astra.yaml` (and optionally `src/`, `universes/baseline.yaml`, `results/`).
24
33
  2. Add a `path:` entry to the parent `astra.yaml` under `analyses:` (e.g. `analyses: { my_sub: { path: ./analyses/my_sub } }`).
25
34
  3. Add a `<name>: { universe: baseline }` entry to each existing parent universe file.
26
35
 
@@ -30,10 +39,14 @@ Populate the sub-analysis's `astra.yaml` with inputs, outputs, and decisions. Us
30
39
 
31
40
  Three overlapping phases:
32
41
 
33
- 1. **Write & Debug** — Run scripts directly (`python scripts/compute.py`) to iterate. Write them recipe-ready from the start: parameterize decisions, write to convention paths, one script per output.
42
+ 1. **Write & Debug** — Run scripts directly (`python src/compute.py`) to iterate. Write them recipe-ready from the start: parameterize decisions, write to convention paths, one script per output.
34
43
  2. **Integrate** — Add `recipe:` blocks to outputs in `astra.yaml`. Track with `lc status` (`alias` / `missing` / `stale` / `ok`). Set `container:` at analysis level or per-recipe — pass an image name (e.g., `python:3.12-slim`) or a path to a Containerfile (e.g., `Containerfile`).
35
44
  3. **Materialize** — `lc run` executes recipes inside their declared containers and writes a content-addressed manifest next to each output. Done when `lc status` shows all `ok`.
36
45
 
46
+ Bare `lc run` materializes every output across every universe in `universes/*.yaml`; pass `OUTPUT_ID...` to scope to specific outputs and `--universe NAME` to scope to one universe. **Build iteratively** — name one upstream output at a time (`lc run <output_id> --universe <name>`) so you can inspect each intermediate before chaining further downstream, rather than running the whole DAG and debugging from the bottom of a long failure trace. `lc run` auto-builds container images on demand, so `lc build` is only needed for pre-warming or forcing a rebuild with `--force`.
47
+
48
+ Outputs land at `results/<universe>/<output_id>/`, with the per-output manifest at `<output_dir>/.lightcone-manifest.json`. Path-rooted sub-analyses prefix the sub's path: `<sub_path>/results/<universe>/<output_id>/`.
49
+
37
50
  **An output is not done until `lc run` produces it.** Running scripts directly is for debugging only — final results must always come from `lc run` so they are reproducible.
38
51
 
39
52
  ### Spec-Code Invariant
@@ -57,5 +70,6 @@ Three overlapping phases:
57
70
  - **Script arg not recognized** — The recipe's `command` template controls how decisions reach the script. Make sure each `{decisions.<id>}` is paired with a flag the script's argparse defines (e.g. `--<id> {decisions.<id>}` ↔ `parser.add_argument('--<id>')`).
58
71
  - **Recipe input not found** — Materialize upstream outputs first.
59
72
  - **Undeclared placeholder error** — A `{decisions.<id>}` or `{inputs.<id>}` in the recipe references something not listed in `Output.decisions` / `Output.inputs`. Add it to the Output's declaration, or remove the placeholder.
73
+ - **`lc verify` failure** — `missing_manifest` (output dir exists with no `.lightcone-manifest.json`), `tampered_data` (bytes on disk no longer hash to the recorded `data_version`), or `broken_chain` (an upstream's `data_version` drifted from what this output's manifest recorded). Re-run the affected output with `lc run` to repair.
60
74
 
61
75
  After failure: fix, then `lc run <output_id> --universe <name>`.
@@ -0,0 +1,107 @@
1
+ #!/bin/bash
2
+ # PostToolUse hook: warn when the agent runs a Python script that has an
3
+ # integrated recipe — they should use `lc run` so a manifest gets written.
4
+ #
5
+ # Pure bash + jq + the `lc` binary on PATH. No Python imports — the hook
6
+ # previously imported `lightcone.engine.status` which silently failed in
7
+ # the empty venv `lc init` creates, so the hook was a no-op for nearly
8
+ # every project. Status data now comes from `lc status --json`, which
9
+ # carries the recipe template per output as of the same change.
10
+
11
+ input=$(cat)
12
+ command=$(echo "$input" | jq -r '.tool_input.command // empty')
13
+
14
+ [ -z "$command" ] && exit 0
15
+
16
+ # Only inspect commands that contain "python" / "python3"
17
+ echo "$command" | grep -qE 'python[23]?[[:space:]]' || exit 0
18
+
19
+ # Skip lc/astra invocations (they're already going through the right path)
20
+ echo "$command" | grep -qE '(^|[[:space:]/])(lc|astra)[[:space:]]' && exit 0
21
+
22
+ # Must be in an ASTRA project
23
+ [ -f "astra.yaml" ] || exit 0
24
+
25
+ # Must have lc on PATH; otherwise we can't get status
26
+ command -v lc &> /dev/null || exit 0
27
+
28
+ # Parse --universe NAME or --universe=NAME from the agent's command;
29
+ # default to baseline.
30
+ universe=$(echo "$command" \
31
+ | grep -oE -- '--universe[= ][[:space:]]*[^[:space:]]+' \
32
+ | head -1 \
33
+ | sed -E 's/--universe[= ][[:space:]]*//')
34
+ [ -z "$universe" ] && universe="baseline"
35
+
36
+ status_json=$(lc status --json --universe "$universe" 2>/dev/null) || exit 0
37
+
38
+ # Collect script paths the agent invoked. Split on && || ; first so each
39
+ # chained command is considered separately, then look for tokens ending
40
+ # in .py and `python -m module.path`.
41
+ agent_scripts=$(echo "$command" | awk '
42
+ {
43
+ # Split on shell separators
44
+ gsub(/&&|\|\||;/, "\n")
45
+ print
46
+ }' | awk '
47
+ {
48
+ n = split($0, tok, /[[:space:]]+/)
49
+ for (i = 1; i <= n; i++) {
50
+ t = tok[i]
51
+ if (t ~ /\.py$/) {
52
+ sub(/^\.\//, "", t)
53
+ print t
54
+ n2 = split(t, pp, "/")
55
+ print pp[n2]
56
+ }
57
+ if (t == "-m" && i + 1 <= n) {
58
+ mod = tok[i+1]
59
+ gsub(/\./, "/", mod)
60
+ print mod ".py"
61
+ n2 = split(mod, pp, "/")
62
+ print pp[n2] ".py"
63
+ }
64
+ }
65
+ }' | sort -u)
66
+
67
+ [ -z "$agent_scripts" ] && exit 0
68
+
69
+ # For each output that has a recipe, extract the script path from the
70
+ # recipe template and check whether the agent's command invoked it.
71
+ matched=$(echo "$status_json" | jq -r --arg uni "$universe" '
72
+ .universes[]
73
+ | select(.universe_id == $uni)
74
+ | .outputs[]
75
+ | select(.recipe_command != null and .recipe_command != "")
76
+ | "\(.output_id)\t\(.status)\t\(.recipe_command)"
77
+ ' | while IFS=$'\t' read -r out_id out_status recipe_cmd; do
78
+ recipe_scripts=$(echo "$recipe_cmd" | tr ' \t\n' '\n' | grep '\.py$' | sed 's|^\./||')
79
+ for r in $recipe_scripts; do
80
+ rb=$(basename "$r")
81
+ if echo "$agent_scripts" | grep -qFx "$r" || echo "$agent_scripts" | grep -qFx "$rb"; then
82
+ printf '%s\t%s\n' "$out_id" "$out_status"
83
+ break
84
+ fi
85
+ done
86
+ done | head -1)
87
+
88
+ [ -z "$matched" ] && exit 0
89
+
90
+ matched_id=$(echo "$matched" | cut -f1)
91
+ matched_status=$(echo "$matched" | cut -f2)
92
+
93
+ case "$matched_status" in
94
+ missing|stale)
95
+ msg="WARNING: You ran the script for output \`$matched_id\` directly (status: $matched_status in $universe), which has an integrated recipe. Use \`lc run $matched_id --universe $universe\` instead so a manifest is written and the result is reproducible."
96
+ ;;
97
+ ok)
98
+ msg="NOTE: Output \`$matched_id\` already has current results in $universe from \`lc run\`. Running the script directly bypasses the manifest — use \`lc run $matched_id --universe $universe\` to regenerate reproducibly."
99
+ ;;
100
+ *)
101
+ exit 0
102
+ ;;
103
+ esac
104
+
105
+ escaped_msg=$(echo "$msg" | jq -Rs .)
106
+ echo "{\"hookSpecificOutput\": {\"hookEventName\": \"PostToolUse\", \"additionalContext\": $escaped_msg}}"
107
+ exit 0