Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/backlog-location-labels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Added

- `health backlog` reads a row's `per_lf_locations` (1-based LF index to label) and `location` (Waypost 0.5.x `BacklogRow`) and hands them to the drive as `--per-lf-location <i>=<label>` / `--location <label>`, so a GPS-less recording with no day track (`nctopa-2026-03-05-rec0063`) launches from the backlog with the operator's labels typed once into the row instead of a hand launch. The row is checked against the drive's own label rules before any render (`location_labels_skip_reason`): a label the parser would refuse skips the row at the backlog with the parser's reason, and the row behind it still launches. The drive still waits for a person at the OST audit gate.
2 changes: 1 addition & 1 deletion ci/siblings.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# The two sibling packages this repo imports, pinned to one reviewed revision
# each. Bump both the revision and the tests that depend on it in one change.
seo-manager @ git+https://github.com/Back-Road-Creative/waypost@2ce9f47098e64f3b1a218a69e665252b031180f9
seo-manager @ git+https://github.com/Back-Road-Creative/waypost@e63c463504b55bf53a05b7aae3879aa1a73e1e2c
gps-journey @ git+https://github.com/Back-Road-Creative/gps-journey@10f9f96ae2fdffdd7372fcb117a1c80ae72c1d35
10 changes: 8 additions & 2 deletions docs/pipeline/156-health-check.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,9 @@ written. The text output, which is what the cron logs, prints `retired <project>
`held <stages dir> (no-auto-resume)`, or `would retire` and `would hold` under `--dry-run`.
The row is the first `drive_backlog.yaml` row for the region that is not skipped. Each skip is
reported with its reason: already in the store (with its status), already in the content ledger,
a missing source file, a missing `day_gpx` track, an operator hold (a `no-auto-resume` file under `<stages root>/<project>`,
a missing source file, a missing `day_gpx` track, a `placeless_label` that claims something, typed
labels (`per_lf_locations` / `location`) the drive's own `--per-lf-location` rules would refuse
(`location labels: <the parser's reason>`), an operator hold (a `no-auto-resume` file under `<stages root>/<project>`,
or a held earlier stage whose sources overlap the row's), or every source already staged by an
earlier run's `resume.json`.

Expand Down Expand Up @@ -193,7 +195,11 @@ python> -m gomoveshift drive <subset> --region R --publish D --lf-day D --ingest
--force --no-tmux`) with `GMS_INTERMEDIATE_DIR=S/<project>`, and registers the drive `auditing`
in the store. A row with a `day_gpx` (footage with no GPS of its own) adds `--day-gpx <track>
--camera-utc-offset-hours <N>` from the row; Waypost refuses a row that sets one without the
other. The `--expect` makes an exit 0 that wrote no fresh shorts `failed-no-output`. Uploads stay off; publishing still needs the normal approval and re-run.
other. A row with `per_lf_locations` (footage whose place the camera never recorded, with the
operator's label typed per LF into the row) adds `--per-lf-location <i>=<label>` per entry and
`--location <label>` when the row sets one, so nothing is typed at launch time; the labels are
checked against the drive's own rules before any render (`location_labels_skip_reason`) and the
drive still waits for a person at the OST audit gate, since no GPS gives it a system review. The `--expect` makes an exit 0 that wrote no fresh shorts `failed-no-output`. Uploads stay off; publishing still needs the normal approval and re-run.
`--dry-run` takes no lease, writes nothing and prints the exact argv, env and storage table.

**Supersede.** `--supersede <project> --reason <text>` replaces that row's own earlier run, for
Expand Down
4 changes: 3 additions & 1 deletion docs/unattended-cadence.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ refuses and names them. An unreadable decision store halts planning with
raw GoPro chapters on D:. It is tenant data, so it lives in the operator's inputs directory (`GMS_INPUTS_DIR`) at
`gomoveshift/console/brands/gomoveshift/drive_backlog.yaml` and is read through
`gomoveshift.paths.drive_backlog_path()`. Its
header names the re-cuts already public and the footage left out.
header names the re-cuts already public and the footage left out. A row for footage with no
GPS and no track carries the operator's labels (`per_lf_locations`, `location`) so the
launcher hands them to the drive; nothing is typed at launch time.
`tests/test_drive_backlog_manifest.py` checks that the consumer resolves to it, that it
parses, and that no chapter is queued twice.

Expand Down
36 changes: 36 additions & 0 deletions gomoveshift/health/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,36 @@ def placeless_label_skip_reason(label: str) -> Optional[str]:
)


def location_labels_skip_reason(row) -> Optional[str]:
"""Why a backlog row's typed labels (``per_lf_locations`` / ``location``) may not launch,
or ``None``: the drive's own ``--per-lf-location`` / ``--location`` rules
(``cli.parser._validate_per_lf_location_args``), applied before any render so a refused row
is skipped at the backlog with the parser's reason instead of failing the launch job at
argparse hours later. Wording is the operator's; this judges only what the drive would.
"""
per_lf = list(getattr(row, "per_lf_locations", ()) or ())
location = getattr(row, "location", "") or ""
if not per_lf and not location:
return None
import argparse

from ..cli.parser import _per_lf_location_kv, _validate_per_lf_location_args

try:
pairs = [_per_lf_location_kv(f"{idx}={label}") for idx, label in per_lf]
_validate_per_lf_location_args(
argparse.Namespace(
per_lf_locations=pairs,
location=location,
placeless_label=getattr(row, "placeless_label", ""),
day_gpx=getattr(row, "day_gpx", ""),
)
)
except argparse.ArgumentTypeError as exc:
return f"location labels: {exc}"
return None


def _skip_reason(row, store_status, known, staged, stages_root, own_ok=False) -> Optional[str]:
"""Why ``row`` is not launched, or ``None``. ``own_ok``: the row's own stage root is being
superseded, so a hold or recipe under it does not skip the row (``staged`` excludes it)."""
Expand All @@ -223,6 +253,8 @@ def _skip_reason(row, store_status, known, staged, stages_root, own_ok=False) ->
label = getattr(row, "placeless_label", "")
if label and (label_why := placeless_label_skip_reason(label)):
return label_why
if labels_why := location_labels_skip_reason(row):
return labels_why
for marker in sorted(Path(stages_root).glob(f"{row.project}/**/_stages/{HOLD_NAME}")):
if not own_ok:
return f"operator hold: {marker}"
Expand Down Expand Up @@ -703,6 +735,10 @@ def _run(res, region, rows, drives, free_slots, known, stages_root, output_root,
drive_argv += ["--camera-utc-offset-hours", str(row.camera_utc_offset_hours)]
if getattr(row, "placeless_label", ""): # claim-free only: _skip_reason refused the rest
drive_argv += ["--placeless-label", row.placeless_label]
for idx, label in getattr(row, "per_lf_locations", ()) or (): # _skip_reason validated
drive_argv += ["--per-lf-location", f"{idx}={label}"]
if getattr(row, "location", ""):
drive_argv += ["--location", row.location]
argv = _job_argv(code_root, jobs_dir, row.project, out_dir, drive_argv)
env = {
"GMS_INTERMEDIATE_DIR": str(root),
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ dependencies = [
# happens to be installed or 404s on PyPI. ci/siblings.txt pins the same
# commits. Like the four libraries below, requirements.lock carries no entry
# for them (an unnamed `git+` line is rejected by `uv pip install --constraint`).
"seo-manager @ git+https://github.com/Back-Road-Creative/waypost@2ce9f47098e64f3b1a218a69e665252b031180f9",
"seo-manager @ git+https://github.com/Back-Road-Creative/waypost@e63c463504b55bf53a05b7aae3879aa1a73e1e2c",
"gps-journey @ git+https://github.com/Back-Road-Creative/gps-journey@10f9f96ae2fdffdd7372fcb117a1c80ae72c1d35",
# Extracted libraries — code that used to live in this tree and now ships as
# standalone repos: sun-track (solar elevation + golden hour, GPX position at
Expand Down
85 changes: 85 additions & 0 deletions tests/test_health_backlog_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,91 @@ def test_a_row_without_a_placeless_label_passes_no_label(tmp_path):
assert "--placeless-label" not in rig.launches[0][0]


def _labeled_row(rig, per_lf, location=""):
row = rig.rows[0]
return BacklogRow(
row.project, row.region, row.sources, "generic", row.lf_mode,
per_lf_locations=tuple(per_lf), location=location,
) # fmt: skip


# nctopa-2026-03-05-rec0063 (GPS-less, no track): its labels are the operator's, typed once into
# the backlog row, and the launcher hands them to the drive. Nothing is typed at launch time.
def test_a_row_with_per_lf_locations_launches_with_each_label_and_the_drive_wide_one(tmp_path):
rig = Rig(tmp_path)
rig.rows[0] = _labeled_row(
rig,
[(1, "Example Town to Beta Pass, Example County, Exampleland"),
(2, "Beta Pass to Gamma Falls, Example County, Exampleland")],
location="Example County, Exampleland",
) # fmt: skip
res = rig.run()
assert res["action"] == "launched", res
drive_args = rig.launches[0][0][rig.launches[0][0].index("--") + 1 :]
per_lf = [drive_args[i + 1] for i, a in enumerate(drive_args) if a == "--per-lf-location"]
assert per_lf == [
"1=Example Town to Beta Pass, Example County, Exampleland",
"2=Beta Pass to Gamma Falls, Example County, Exampleland",
]
assert drive_args[drive_args.index("--location") + 1] == "Example County, Exampleland"
assert "--placeless-label" not in drive_args and "--day-gpx" not in drive_args


def test_a_row_with_per_lf_locations_and_no_drive_wide_label_passes_only_the_per_lf_flags(tmp_path):
rig = Rig(tmp_path)
rig.rows[0] = _labeled_row(rig, [(1, "Example Town, Example County")])
res = rig.run()
assert res["action"] == "launched", res
drive_args = rig.launches[0][0]
assert "--per-lf-location" in drive_args and "--location" not in drive_args


def test_a_row_without_location_fields_passes_neither_flag(tmp_path):
rig = Rig(tmp_path)
rig.run()
drive_args = rig.launches[0][0]
assert "--per-lf-location" not in drive_args and "--location" not in drive_args


# The drive's own label rules (``cli.parser._validate_per_lf_location_args``) judge the row
# before any render: a refused row is skipped at the backlog with the parser's reason, never
# failed hours later at the launch job's argparse, and the row behind it still launches.
@pytest.mark.parametrize(
"per_lf, location, expected",
[
([(1, "To Beta Pass, Example County")], "", "starts with a bare preposition"),
([(1, "Beta Pass, Example County")], "Exampleland", "must have >= 2 components"),
([(1, "Beta Pass, Example County")], "Beta Pass, Example County", "equals --location"),
(
[(1, "Beta Pass, Example County, Exampleland")],
"Other County, Exampleland",
"is not a comma-suffix",
),
],
)
def test_a_row_the_drive_would_refuse_is_skipped_before_any_render(
tmp_path, per_lf, location, expected
):
rig = Rig(tmp_path)
rig.rows[0] = _labeled_row(rig, per_lf, location=location)
res = rig.run()
assert (res["action"], res["project"]) == ("launched", "day-b"), res
(skip,) = res["skipped"]
assert skip["project"] == "day-a" and skip["reason"].startswith("location labels: ")
assert expected in skip["reason"], skip["reason"]
assert rig.gates == [(rig.sources / "day-b", "day-b")] # never reached the render gate
assert "--per-lf-location" not in rig.launches[0][0]


def test_location_labels_skip_reason_is_none_for_a_row_the_drive_accepts():
row = BacklogRow(
"p", "usa", ("/v.mp4",), "generic", "full_drive",
per_lf_locations=((1, "Beta Pass, Example County, Exampleland"),),
location="Example County, Exampleland",
) # fmt: skip
assert launch.location_labels_skip_reason(row) is None


def test_a_missing_day_gpx_skips_the_row(tmp_path):
rig = Rig(tmp_path)
rig.rows[0] = _gpx_row(rig, tmp_path / "gone.gpx")
Expand Down
Loading