File size: 5,299 Bytes
761d149
830d137
feb1b1c
 
830d137
bb77312
830d137
 
 
 
fc87e83
830d137
 
 
 
bb77312
830d137
 
 
fc87e83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
830d137
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fc87e83
 
 
 
 
 
 
 
 
 
 
 
 
830d137
 
bb77312
830d137
 
 
 
 
 
 
 
 
 
 
 
 
 
bb77312
fc87e83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb77312
 
830d137
 
 
 
 
 
 
bb77312
 
 
 
830d137
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145


from __future__ import annotations

import importlib
import time
from pathlib import Path
from typing import Any, cast

import typer
import yaml

from redstack.config.determinism import apply_determinism
from redstack.config.loader import ConfigLoadError, load_config
from redstack.config.schema import Profile, RunMode
from redstack.observability.timing import format_duration

__all__: tuple[str, ...] = ("build",)

#: Default online ``ScoringPolicy.neutral_prior`` (config/schema.py), reused
#: when packaging the locked-heuristics scoring_weights artifact.
_DEFAULT_NEUTRAL_PRIOR: float = 0.5


def _load_seed_weights(configs_root: Path) -> tuple[dict[str, float], float]:
    """Read ``weights/scoring_weights.yaml`` -> (component weights, neutral prior).

    This is the locked-heuristics default: the human-authored seed weights,
    used verbatim (no gold-label search) when ``--golden-labels`` is not given.
    """
    path = configs_root / "weights" / "scoring_weights.yaml"
    doc = yaml.safe_load(path.read_text(encoding="utf-8"))
    weights = {str(k): float(v) for k, v in doc["weights"].items()}
    neutral_prior = float(doc.get("neutral_prior", _DEFAULT_NEUTRAL_PRIOR))
    return weights, neutral_prior


def _resolve_configs_root(config_arg: Path) -> Path:
    """Resolve a ``--config`` argument (file or directory) to the configs root.

    Accepts the root directory itself, ``<root>/base.yaml``, or
    ``<root>/runtime/<mode>.yaml`` — every form used by the Makefile / CLAUDE.md
    invocations — by walking up to find the directory holding ``base.yaml``.

    Raises:
        typer.BadParameter: no ``base.yaml`` found within two levels up.
    """
    start = config_arg if config_arg.is_dir() else config_arg.parent
    for probe in (start, start.parent):
        if (probe / "base.yaml").is_file():
            return probe
    raise typer.BadParameter(
        f"cannot locate a configs root (base.yaml) from {config_arg}"
    )


def _code_version() -> str:
    """Return the build's code provenance for the report (package version)."""
    import redstack

    return redstack.__version__


def build(
    config: Path = typer.Option(
        Path("configs"),
        "--config",
        help="Configs root directory, or a YAML file inside it.",
    ),
    profile: str | None = typer.Option(
        None, "--profile", help="Optional override layer: ci | local."
    ),
    force: str | None = typer.Option(
        None,
        "--force",
        help="Comma-separated stage ids to force-recompute (e.g. 'O9,O15').",
    ),
    golden_labels: bool = typer.Option(
        True,
        "--golden-labels/--no-golden-labels",
        help=(
            "Run the real O8-O10 gold-label calibration search against the "
            "committed data/golden/golden_labels.csv (default — fails loudly "
            "via GoldLabelSeedMissingError if it isn't committed). Pass "
            "--no-golden-labels to skip O8-O10's search and use the locked "
            "heuristic weights from configs/weights/scoring_weights.yaml "
            "as-is; this is an uncalibrated dev shortcut, not a substitute "
            "for the real search."
        ),
    ),
) -> None:
    """Run the offline O0-O18 build: artifacts/ + MANIFEST.json."""
    cumulative_started = time.perf_counter()
    configs_root = _resolve_configs_root(config)
    profile_enum = Profile(profile) if profile else None
    try:
        resolved = load_config(configs_root, RunMode.OFFLINE, profile_enum)
    except ConfigLoadError as exc:
        typer.secho(str(exc), fg=typer.colors.RED, err=True)
        raise typer.Exit(code=1) from exc

    apply_determinism(resolved.determinism)

    # Dynamic on purpose — see module docstring (layering seam + import order).
    compose = importlib.import_module("redstack.pipelines.offline.compose")

    force_ids = tuple(s.strip() for s in force.split(",")) if force else None
    pass_started = time.perf_counter()
    if golden_labels:
        run_offline_build = cast("Any", compose.run_offline_build)
        report = run_offline_build(
            resolved,
            configs_root=configs_root,
            code_version=_code_version(),
            force=force_ids,
        )
    else:
        component_weights, neutral_prior = _load_seed_weights(configs_root)
        run_offline_build_with_locked_heuristics = cast(
            "Any", compose.run_offline_build_with_locked_heuristics
        )
        report = run_offline_build_with_locked_heuristics(
            resolved,
            configs_root=configs_root,
            code_version=_code_version(),
            component_weights=component_weights,
            neutral_prior=neutral_prior,
            force=force_ids,
        )
    pass_elapsed = time.perf_counter() - pass_started
    cumulative_elapsed = time.perf_counter() - cumulative_started

    typer.echo(
        "offline build complete: "
        f"executed={len(report.executed)} "
        f"skipped={len(report.skipped)} "
        f"failed={len(report.failed)}"
    )
    typer.echo(f"offline pass duration:  {format_duration(pass_elapsed)} (MM:SS.ms)")
    typer.echo(
        f"cumulative wall-clock:  {format_duration(cumulative_elapsed)} (MM:SS.ms)"
    )
    if report.failed:
        raise typer.Exit(code=1)