seqforge 2026.7.1__py3-none-any.whl
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.
- seqforge/__init__.py +16 -0
- seqforge/cli/__init__.py +38 -0
- seqforge/cli/__main__.py +8 -0
- seqforge/cli/_common.py +105 -0
- seqforge/cli/compose.py +119 -0
- seqforge/cli/eval.py +103 -0
- seqforge/cli/harvest.py +417 -0
- seqforge/cli/hook.py +247 -0
- seqforge/cli/io.py +502 -0
- seqforge/cli/kb.py +348 -0
- seqforge/cli/manifest.py +536 -0
- seqforge/cli/probe.py +43 -0
- seqforge/cli/processing.py +192 -0
- seqforge/cli/project.py +52 -0
- seqforge/cli/resolve.py +55 -0
- seqforge/cli/root.py +66 -0
- seqforge/cli/run.py +463 -0
- seqforge/cli/schema.py +41 -0
- seqforge/compose/__init__.py +28 -0
- seqforge/compose/core.py +515 -0
- seqforge/compose/gates.py +113 -0
- seqforge/compose/params.py +447 -0
- seqforge/e2e.py +1926 -0
- seqforge/evals/__init__.py +78 -0
- seqforge/evals/case.py +382 -0
- seqforge/evals/grade.py +300 -0
- seqforge/evals/run.py +420 -0
- seqforge/harvest/__init__.py +121 -0
- seqforge/harvest/extract.py +319 -0
- seqforge/harvest/fields.py +212 -0
- seqforge/harvest/normalize.py +537 -0
- seqforge/harvest/prep.py +41 -0
- seqforge/harvest/providers.py +321 -0
- seqforge/harvest/verify.py +251 -0
- seqforge/hooks/__init__.py +33 -0
- seqforge/hooks/guards.py +214 -0
- seqforge/io/__init__.py +61 -0
- seqforge/io/archive.py +450 -0
- seqforge/io/attributes.py +190 -0
- seqforge/io/biosample/attributes.json +6341 -0
- seqforge/io/efo/labels.json +55 -0
- seqforge/io/efo.py +138 -0
- seqforge/io/onlist.py +661 -0
- seqforge/io/onlists/3M-february-2018.codes.gz +0 -0
- seqforge/io/onlists/737K-arc-v1.codes.gz +0 -0
- seqforge/io/onlists/737K-august-2016.codes.gz +0 -0
- seqforge/io/onlists/bd-rhapsody-cls1-384.codes.gz +0 -0
- seqforge/io/onlists/bd-rhapsody-cls1.codes.gz +0 -0
- seqforge/io/onlists/bd-rhapsody-cls2-384.codes.gz +0 -0
- seqforge/io/onlists/bd-rhapsody-cls2.codes.gz +0 -0
- seqforge/io/onlists/bd-rhapsody-cls3-384.codes.gz +0 -0
- seqforge/io/onlists/bd-rhapsody-cls3.codes.gz +0 -0
- seqforge/io/onlists/index.json +74 -0
- seqforge/io/remote.py +659 -0
- seqforge/io/taxonomy.py +194 -0
- seqforge/kb/__init__.py +62 -0
- seqforge/kb/anchor.py +169 -0
- seqforge/kb/generate.py +147 -0
- seqforge/kb/loader.py +152 -0
- seqforge/kb/roundtrip.py +112 -0
- seqforge/kb/schema.py +422 -0
- seqforge/kb/specs/10x-3p-gex/spec.yaml +62 -0
- seqforge/kb/specs/10x-3p-gex-v2/README.md +41 -0
- seqforge/kb/specs/10x-3p-gex-v2/spec.yaml +83 -0
- seqforge/kb/specs/10x-3p-gex-v3/README.md +56 -0
- seqforge/kb/specs/10x-3p-gex-v3/spec.yaml +118 -0
- seqforge/kb/specs/10x-3p-gex-v3.1/README.md +56 -0
- seqforge/kb/specs/10x-3p-gex-v3.1/spec.yaml +124 -0
- seqforge/kb/specs/bd-rhapsody-wta/README.md +103 -0
- seqforge/kb/specs/bd-rhapsody-wta/spec.yaml +130 -0
- seqforge/kb/specs/bd-rhapsody-wta-enhanced/spec.yaml +99 -0
- seqforge/kb/specs/bd-rhapsody-wta-enhanced-v1/spec.yaml +93 -0
- seqforge/kb/specs/bd-rhapsody-wta-enhanced-v2/spec.yaml +81 -0
- seqforge/kb/specs/bulk-rnaseq-pe/README.md +35 -0
- seqforge/kb/specs/bulk-rnaseq-pe/spec.yaml +97 -0
- seqforge/kb/specs/splitseq/README.md +51 -0
- seqforge/kb/specs/splitseq/spec.yaml +157 -0
- seqforge/manifest/__init__.py +61 -0
- seqforge/manifest/fill.py +531 -0
- seqforge/manifest/hash.py +77 -0
- seqforge/manifest/instruct.py +114 -0
- seqforge/manifest/policy.py +409 -0
- seqforge/manifest/validate.py +274 -0
- seqforge/models/__init__.py +268 -0
- seqforge/models/assertion.py +68 -0
- seqforge/models/base.py +100 -0
- seqforge/models/blocker.py +71 -0
- seqforge/models/conflict.py +47 -0
- seqforge/models/dataset.py +320 -0
- seqforge/models/evidenced.py +54 -0
- seqforge/models/observation.py +157 -0
- seqforge/models/processing.py +231 -0
- seqforge/models/records.py +145 -0
- seqforge/models/resolve.py +216 -0
- seqforge/probe/__init__.py +46 -0
- seqforge/probe/core.py +232 -0
- seqforge/probe/signals.py +250 -0
- seqforge/probe/streaming.py +118 -0
- seqforge/project.py +177 -0
- seqforge/py.typed +0 -0
- seqforge/resolve/__init__.py +98 -0
- seqforge/resolve/assign.py +204 -0
- seqforge/resolve/cache.py +119 -0
- seqforge/resolve/confuse.py +215 -0
- seqforge/resolve/engine.py +646 -0
- seqforge/resolve/escalate.py +668 -0
- seqforge/resolve/evaluators.py +306 -0
- seqforge/resolve/geometry.py +89 -0
- seqforge/resolve/group.py +85 -0
- seqforge/resolve/records.py +550 -0
- seqforge/resolve/scoring.py +373 -0
- seqforge/resolve/window.py +206 -0
- seqforge/workflows/__init__.py +234 -0
- seqforge/workflows/cram.py +117 -0
- seqforge/workflows/h5ad.py +368 -0
- seqforge/workflows/map/star.smk +101 -0
- seqforge/workflows/map/starsolo.smk +360 -0
- seqforge/workflows/qc.py +157 -0
- seqforge/workspace.py +125 -0
- seqforge-2026.7.1.dist-info/METADATA +125 -0
- seqforge-2026.7.1.dist-info/RECORD +124 -0
- seqforge-2026.7.1.dist-info/WHEEL +4 -0
- seqforge-2026.7.1.dist-info/entry_points.txt +2 -0
- seqforge-2026.7.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""`seqforge processing` -- the PROCESSING manifest: what to DO with a dataset. Many per dataset."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
import yaml
|
|
10
|
+
from pydantic import ValidationError
|
|
11
|
+
|
|
12
|
+
from .. import __version__
|
|
13
|
+
from ..kb import load_spec
|
|
14
|
+
from ..manifest import (
|
|
15
|
+
Instruction,
|
|
16
|
+
PolicyError,
|
|
17
|
+
ProcessingInputs,
|
|
18
|
+
exit_code_for_report,
|
|
19
|
+
fill_processing,
|
|
20
|
+
instructions_from_assertions,
|
|
21
|
+
prep_type_from_assertions,
|
|
22
|
+
processing_content_hash,
|
|
23
|
+
validate_processing,
|
|
24
|
+
)
|
|
25
|
+
from ..models.assertion import Assertion
|
|
26
|
+
from ._common import _load_manifest, _load_processing, _parse_quantify
|
|
27
|
+
from .root import processing_app
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _instructions_from(path: Path | None) -> list[Instruction]:
|
|
31
|
+
"""Rebuild the instructable surface from `harvest extract`'s artifact.
|
|
32
|
+
|
|
33
|
+
The precedence ladder (§7) is flag > instruction > policy, and `resolve_processing` has always
|
|
34
|
+
implemented it — its `PolicyError` even tells you to "name an assembly in an --instruction
|
|
35
|
+
document". That branch was unreachable: `--assembly` was a REQUIRED option, and nothing passed
|
|
36
|
+
`instructions=` from any production caller. This is the last mile of a join that already existed.
|
|
37
|
+
|
|
38
|
+
Note what is NOT happening: the model does not decide anything here. It found a claim in prose and
|
|
39
|
+
code verified the quote greps back and entails the value; this reads that record and applies
|
|
40
|
+
precedence. "We can accept instructions because we never trust the model to act on them, only to
|
|
41
|
+
find them."
|
|
42
|
+
"""
|
|
43
|
+
if path is None:
|
|
44
|
+
return []
|
|
45
|
+
payload = json.loads(path.read_text())
|
|
46
|
+
if isinstance(payload, list):
|
|
47
|
+
raise ValueError(
|
|
48
|
+
"this looks like a pre-2026.7 assertions.json (a bare list). It cannot say which "
|
|
49
|
+
"documents were --instruction, and only those may set processing.*. Re-run "
|
|
50
|
+
"`seqforge harvest extract`."
|
|
51
|
+
)
|
|
52
|
+
docs = frozenset(payload.get("instruction_docs", ()))
|
|
53
|
+
parsed = [Assertion.model_validate(a) for a in payload.get("assertions", ())]
|
|
54
|
+
instructions, conflicts = instructions_from_assertions(parsed, instruction_docs=docs)
|
|
55
|
+
if conflicts:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"{len(conflicts)} instruction(s) disagree with each other; only their author can "
|
|
58
|
+
f"settle that: " + "; ".join(c.field for c in conflicts)
|
|
59
|
+
)
|
|
60
|
+
return instructions
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _prep_type_from(path: Path | None) -> str | None:
|
|
64
|
+
"""The cells-vs-nuclei prep from `harvest extract`'s artifact, normalized. `None` if absent.
|
|
65
|
+
|
|
66
|
+
A span-verified biology fact (``library.prep_type``) that code — not the model — maps to the
|
|
67
|
+
primary soloFeature: a single-nucleus prep promotes GeneFull. The model named the biology and
|
|
68
|
+
verified its quote; this reads that record. See :func:`resolve_features`.
|
|
69
|
+
"""
|
|
70
|
+
if path is None:
|
|
71
|
+
return None
|
|
72
|
+
payload = json.loads(path.read_text())
|
|
73
|
+
if isinstance(payload, list): # a pre-2026.7 bare list; `_instructions_from` already refuses it
|
|
74
|
+
return None
|
|
75
|
+
parsed = [Assertion.model_validate(a) for a in payload.get("assertions", ())]
|
|
76
|
+
return prep_type_from_assertions(parsed)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@processing_app.command("new")
|
|
80
|
+
def processing_new(
|
|
81
|
+
dataset_path: Path = typer.Argument(..., help="Path to the dataset manifest.yaml."),
|
|
82
|
+
assembly: str | None = typer.Option(
|
|
83
|
+
None, "--assembly", help="liulab-genome UCSC assembly id (e.g. ce11)."
|
|
84
|
+
),
|
|
85
|
+
annotation: str | None = typer.Option(
|
|
86
|
+
None, "--annotation", help="Registered GTF name (e.g. WS298)."
|
|
87
|
+
),
|
|
88
|
+
assertions: Path | None = typer.Option(
|
|
89
|
+
None,
|
|
90
|
+
"--assertions",
|
|
91
|
+
help="Span-verified assertions from `harvest extract` (seqforge/assertions.json). "
|
|
92
|
+
"Instructions in them fill what no flag supplied.",
|
|
93
|
+
),
|
|
94
|
+
quantify: str | None = typer.Option(
|
|
95
|
+
None,
|
|
96
|
+
"--quantify",
|
|
97
|
+
help="Comma-separated soloFeatures. EXACT replacement of the default (which counts all five).",
|
|
98
|
+
),
|
|
99
|
+
threads: int | None = typer.Option(None, "--threads", help="Threads per mapping job."),
|
|
100
|
+
processing_id: str = typer.Option("default", "--id", help="Human slug for this recipe."),
|
|
101
|
+
pin: bool = typer.Option(
|
|
102
|
+
True,
|
|
103
|
+
"--pin/--template",
|
|
104
|
+
help="Bind to this dataset's hash, or leave it portable across datasets.",
|
|
105
|
+
),
|
|
106
|
+
out: Path | None = typer.Option(None, "-o", "--out", help="Write here (default: stdout)."),
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Author a PROCESSING manifest: what to DO with a dataset. Many per dataset.
|
|
109
|
+
|
|
110
|
+
With no flags you get the policy default, which counts every soloFeature — so the common
|
|
111
|
+
case needs no decision from you. --quantify replaces that list exactly; narrowing it warns,
|
|
112
|
+
because dropping a feature is the only irreversible act here.
|
|
113
|
+
"""
|
|
114
|
+
dataset = _load_manifest(dataset_path)
|
|
115
|
+
spec = load_spec(dataset.library.chemistry.value[0])
|
|
116
|
+
try:
|
|
117
|
+
instructions = _instructions_from(assertions)
|
|
118
|
+
prep_type = _prep_type_from(assertions)
|
|
119
|
+
except (OSError, ValueError, ValidationError) as exc:
|
|
120
|
+
typer.echo(f"{assertions}: {exc}", err=True)
|
|
121
|
+
raise typer.Exit(2) from exc
|
|
122
|
+
try:
|
|
123
|
+
processing, warnings = fill_processing(
|
|
124
|
+
spec=spec,
|
|
125
|
+
dataset=dataset,
|
|
126
|
+
processing=ProcessingInputs(
|
|
127
|
+
assembly=assembly,
|
|
128
|
+
annotation_name=annotation,
|
|
129
|
+
features=_parse_quantify(quantify),
|
|
130
|
+
threads=threads,
|
|
131
|
+
),
|
|
132
|
+
instructions=instructions,
|
|
133
|
+
prep_type=prep_type,
|
|
134
|
+
processing_id=processing_id,
|
|
135
|
+
pin=pin,
|
|
136
|
+
seqforge_version=__version__,
|
|
137
|
+
)
|
|
138
|
+
except (PolicyError, ValidationError) as exc:
|
|
139
|
+
typer.echo(str(exc), err=True)
|
|
140
|
+
raise typer.Exit(2) from exc
|
|
141
|
+
report = validate_processing(processing, dataset=dataset)
|
|
142
|
+
payload = yaml.safe_dump(processing.model_dump(mode="json"), sort_keys=True)
|
|
143
|
+
if out is not None:
|
|
144
|
+
out.write_text(payload)
|
|
145
|
+
typer.echo(
|
|
146
|
+
json.dumps(
|
|
147
|
+
{
|
|
148
|
+
"processing": str(out),
|
|
149
|
+
"report": report.model_dump(mode="json"),
|
|
150
|
+
"warnings": [w.model_dump(mode="json") for w in warnings],
|
|
151
|
+
},
|
|
152
|
+
indent=2,
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
typer.echo(payload)
|
|
157
|
+
raise typer.Exit(exit_code_for_report(report))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@processing_app.command("validate")
|
|
161
|
+
def processing_validate(
|
|
162
|
+
processing_path: Path = typer.Argument(..., help="Path to a processing.yaml."),
|
|
163
|
+
dataset_path: Path | None = typer.Option(
|
|
164
|
+
None, "--dataset", help="Cross-check against this dataset manifest (pin + organism)."
|
|
165
|
+
),
|
|
166
|
+
) -> None:
|
|
167
|
+
"""Validate a processing manifest. Exit 3 on a Blocker."""
|
|
168
|
+
processing = _load_processing(processing_path)
|
|
169
|
+
dataset = _load_manifest(dataset_path) if dataset_path is not None else None
|
|
170
|
+
report = validate_processing(processing, dataset=dataset)
|
|
171
|
+
typer.echo(json.dumps(report.model_dump(mode="json"), indent=2))
|
|
172
|
+
raise typer.Exit(exit_code_for_report(report))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@processing_app.command("hash")
|
|
176
|
+
def processing_hash_cmd(
|
|
177
|
+
processing_path: Path = typer.Argument(..., help="Path to a processing.yaml."),
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Print the processing manifest's content hash and whether it matches the recorded one."""
|
|
180
|
+
processing = _load_processing(processing_path)
|
|
181
|
+
content = processing_content_hash(processing)
|
|
182
|
+
typer.echo(
|
|
183
|
+
json.dumps(
|
|
184
|
+
{
|
|
185
|
+
"processing_hash": content,
|
|
186
|
+
"recorded_hash": processing.provenance.processing_hash,
|
|
187
|
+
"matches": content == processing.provenance.processing_hash,
|
|
188
|
+
"pinned_to": processing.dataset.dataset_hash if processing.dataset else None,
|
|
189
|
+
},
|
|
190
|
+
indent=2,
|
|
191
|
+
)
|
|
192
|
+
)
|
seqforge/cli/project.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""`seqforge project` -- project-level views over a multi-assay compile (sample_metadata + index)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from ._common import _load_manifest
|
|
11
|
+
from .root import project_app
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@project_app.command("metadata")
|
|
15
|
+
def project_metadata(
|
|
16
|
+
workspace: Path = typer.Option(Path("."), "-C", "--workspace", help="Root holding seqforge/."),
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Regenerate seqforge/sample_metadata.tsv + project.yaml from the per-assay manifest(s).
|
|
19
|
+
|
|
20
|
+
Reads whatever manifests are already under seqforge/ — a single top-level manifest.yaml, or one
|
|
21
|
+
per assay subdir — and unions their samples into the flat one-row-per-sample table + the assay
|
|
22
|
+
index. Deterministic: same manifests in, same files out. `run` writes these automatically; this
|
|
23
|
+
verb rebuilds them (after editing a manifest, say) without recompiling.
|
|
24
|
+
"""
|
|
25
|
+
from ..project import discover_assays, write_project_views
|
|
26
|
+
|
|
27
|
+
assays = discover_assays(workspace)
|
|
28
|
+
if not assays:
|
|
29
|
+
typer.echo(json.dumps({"error": "no manifest.yaml found under seqforge/"}), err=True)
|
|
30
|
+
raise typer.Exit(3)
|
|
31
|
+
infos: list[dict[str, object]] = []
|
|
32
|
+
for subdir, manifest_path in assays:
|
|
33
|
+
manifest = _load_manifest(manifest_path)
|
|
34
|
+
infos.append(
|
|
35
|
+
{
|
|
36
|
+
"chemistry": manifest.library.chemistry.value[0],
|
|
37
|
+
"subdir": subdir,
|
|
38
|
+
"n_samples": len(manifest.experiment.samples),
|
|
39
|
+
"manifest": str(manifest_path),
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
tsv_path, project_path = write_project_views(workspace, infos)
|
|
43
|
+
typer.echo(
|
|
44
|
+
json.dumps(
|
|
45
|
+
{
|
|
46
|
+
"sample_metadata": str(tsv_path),
|
|
47
|
+
"project": str(project_path),
|
|
48
|
+
"n_assays": len(infos),
|
|
49
|
+
},
|
|
50
|
+
indent=2,
|
|
51
|
+
)
|
|
52
|
+
)
|
seqforge/cli/resolve.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""`seqforge resolve` -- score FASTQ bytes + KB into a ranked, escalated chemistry decision."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from ..resolve import Hypothesis, resolve_dataset
|
|
11
|
+
from ._common import _auto_cpus
|
|
12
|
+
from .root import resolve_app
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@resolve_app.command("score")
|
|
16
|
+
def resolve_score(
|
|
17
|
+
files: list[Path] = typer.Argument(..., help="The dataset's FASTQ .gz files."),
|
|
18
|
+
workspace: Path = typer.Option(
|
|
19
|
+
Path("."), "-C", "--workspace", help="Root for seqforge/ state."
|
|
20
|
+
),
|
|
21
|
+
assert_chemistry: str | None = typer.Option(
|
|
22
|
+
None,
|
|
23
|
+
"--assert-chemistry",
|
|
24
|
+
help="A metadata-asserted chemistry (the span-verified hypothesis).",
|
|
25
|
+
),
|
|
26
|
+
explain: bool = typer.Option(
|
|
27
|
+
False, "--explain", help="Also emit the JSON-safe evidence matrices."
|
|
28
|
+
),
|
|
29
|
+
no_cache: bool = typer.Option(
|
|
30
|
+
False, "--no-cache", help="Do not read/write seqforge/ artifacts."
|
|
31
|
+
),
|
|
32
|
+
max_reads: int = typer.Option(200_000, help="Bounded read budget."),
|
|
33
|
+
max_bytes: int = typer.Option(256 * 1024 * 1024, help="Bounded decompressed-byte cap."),
|
|
34
|
+
cpus: int = typer.Option(
|
|
35
|
+
0, "--cpus", help="Parallel probe workers. 0 = auto (min(8, CPUs)); 1 = sequential."
|
|
36
|
+
),
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Score FASTQ bytes + KB into a ResolveResult. Exit 3 on a Blocker, 4 on an open Conflict/question."""
|
|
39
|
+
hypothesis = Hypothesis(value=assert_chemistry) if assert_chemistry else None
|
|
40
|
+
output = resolve_dataset(
|
|
41
|
+
[str(f) for f in files],
|
|
42
|
+
hypothesis=hypothesis,
|
|
43
|
+
workspace=workspace,
|
|
44
|
+
max_reads=max_reads,
|
|
45
|
+
max_bytes=max_bytes,
|
|
46
|
+
use_cache=not no_cache,
|
|
47
|
+
cpus=_auto_cpus(cpus),
|
|
48
|
+
)
|
|
49
|
+
payload: dict[str, object] = output.result.model_dump(mode="json")
|
|
50
|
+
if explain:
|
|
51
|
+
payload = {"result": payload, "matrices": output.matrices}
|
|
52
|
+
typer.echo(json.dumps(payload, indent=2))
|
|
53
|
+
code = output.exit_code()
|
|
54
|
+
if code != 0:
|
|
55
|
+
raise typer.Exit(code)
|
seqforge/cli/root.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""The root Typer app and every command group's sub-Typer, wired together.
|
|
2
|
+
|
|
3
|
+
Defined in one place so a command module can import exactly the group it registers onto without
|
|
4
|
+
pulling in its siblings. The ``add_typer`` wiring here fixes the CLI's shape; the verbs attach when
|
|
5
|
+
each command module is imported (see this package's ``__init__``). Introspected by
|
|
6
|
+
``test_skills.py`` -- a renamed verb goes red there, not here.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from .. import __version__
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
name="seqforge",
|
|
17
|
+
help="Compile FASTQ + metadata into a validated library manifest and a Snakemake config.",
|
|
18
|
+
no_args_is_help=True,
|
|
19
|
+
add_completion=False,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
schema_app = typer.Typer(help="Export JSON Schema from the Pydantic models (the source of truth).")
|
|
23
|
+
app.add_typer(schema_app, name="schema")
|
|
24
|
+
|
|
25
|
+
kb_app = typer.Typer(help="The executable, self-testing knowledge base.")
|
|
26
|
+
app.add_typer(kb_app, name="kb")
|
|
27
|
+
|
|
28
|
+
io_app = typer.Typer(help="The network + onlist surface (pooch-cached, sha256-verified).")
|
|
29
|
+
app.add_typer(io_app, name="io")
|
|
30
|
+
|
|
31
|
+
onlist_app = typer.Typer(help="Barcode-whitelist (onlist) registry.")
|
|
32
|
+
io_app.add_typer(onlist_app, name="onlist")
|
|
33
|
+
|
|
34
|
+
resolve_app = typer.Typer(help="Score bytes + KB into a ranked, escalated chemistry decision.")
|
|
35
|
+
app.add_typer(resolve_app, name="resolve")
|
|
36
|
+
|
|
37
|
+
manifest_app = typer.Typer(
|
|
38
|
+
help="The DATASET manifest: what the data IS. Immutable, one per dataset."
|
|
39
|
+
)
|
|
40
|
+
app.add_typer(manifest_app, name="manifest")
|
|
41
|
+
processing_app = typer.Typer(
|
|
42
|
+
help="The PROCESSING manifest: what to DO with a dataset. Many per dataset."
|
|
43
|
+
)
|
|
44
|
+
app.add_typer(processing_app, name="processing")
|
|
45
|
+
|
|
46
|
+
harvest_app = typer.Typer(
|
|
47
|
+
help="Prose/metadata -> span-verified Assertions (the one LLM touchpoint)."
|
|
48
|
+
)
|
|
49
|
+
app.add_typer(harvest_app, name="harvest")
|
|
50
|
+
|
|
51
|
+
eval_app = typer.Typer(help="The evals harness: measure what unit tests cannot (brief §9).")
|
|
52
|
+
app.add_typer(eval_app, name="eval")
|
|
53
|
+
|
|
54
|
+
hook_app = typer.Typer(help="Agent hooks: the rules as mechanism, not aspiration (design §4.2).")
|
|
55
|
+
app.add_typer(hook_app, name="hook")
|
|
56
|
+
|
|
57
|
+
project_app = typer.Typer(
|
|
58
|
+
help="Project-level views over a multi-assay compile (sample_metadata.tsv + project.yaml)."
|
|
59
|
+
)
|
|
60
|
+
app.add_typer(project_app, name="project")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@app.command()
|
|
64
|
+
def version() -> None:
|
|
65
|
+
"""Print the seqforge version."""
|
|
66
|
+
typer.echo(__version__)
|