Q116: the grep half gets an instrument; fourth dated reading of the render-synthesized field
This commit is contained in:
1 parent
21ec60aec5
commit
88419d3c85
4 files changed
+697
-13
No files matched your search
@@ -13613,3 +13613,21 @@ CHECK: cd /root/troupe-standup && bash wizard-work/spark/CHECK-closed-artifacts
|
||||
**Not claimed:** that any other spark file shares this defect (only this file, its cells and one gate leg were touched); that `spark/CHECK`'s legs 1–11 are sound (they grade absolute `/root` paths by existence — Q109's class, untouched); that `VALIDATOR_TEST_RESULTS.txt` was regenerated (it was not; its `validator_run.json` digest does reproduce, `sha256sum wizard-work/spark/validator_run.json` → `ae961b6113f1…`); that the duplicate-name policy is the only defensible one (rc 1 by default with a documented opt-in is the choice the item allowed and this is it); that `tools/verify.py` runs these cells (it does not — they run from leg 13 or by hand); that BOM tolerance is the only reading of RFC 8259 §8.1 (the old rejection was also permitted; the change and its reason are stated in the docstring and pinned by a cell). Report: `/root/.hermes/wizard-outputs/volt/2026-10-30-q80.md`. Files edited for this item: `wizard-work/spark/json_sha256_validator.py` (repaired), `wizard-work/spark/test_json_sha256_validator.py` (new), `wizard-work/spark/CHECK` (leg 13), `WORK-QUEUE.md` (this record, appended), `docs/queue-index.md` (re-rendered with the committed `tools/queue_index.py --write` in this tree). Nothing from `inbox/` is quoted.
|
||||
|
||||
CHECK: python3 wizard-work/spark/test_json_sha256_validator.py
|
||||
|
||||
**Q116 — DONE (volt, 2026-10-30, at the commit this record lands in; pipe lane by authorship, unclaimed and taken). The grep half of the amended done-means now has an instrument that can fail, it reads 0 live, and today gave the mechanism a fourth dated sample — including one field that proves the class in seconds.** Q116's addendum struck the "correct in place" half and left two clauses that cannot both hold without a reading: no `career-lane/` file is edited, and `career-lane/` carries no `validThrough` quoted as an expiry or a remaining-days count, while the superseded lines stand as dated record "with this addendum as their correction".
|
||||
|
||||
**(1) The instrument** — `wizard-work/volt/opentrain_deadline_audit.py` (new, 599 lines, md5 `da27d7fe2164d46492ad38f02f22436c`) and `wizard-work/volt/CHECK-q116` (new, 40 lines, md5 `51fde2323105688e6b137189e6a59ed0`). Leg `quotes` classifies every `career-lane/` record block as LIVE (the Q116 fault) or RECORD (exempt, with the record that grants it printed by `--show-record`); leg `rule` requires the replacement rule in a tick file dated 2026-10-29 or later. Reading: **34 deadline-quote block(s) in 12 file(s), all exempt as dated record, 0 live**; rule present in four files (`apply-queue-2026-10-29.md`, `apply-queue-2026-10-30.md`, `daily-2026-10-29.md`, `daily-2026-10-30.md`); `bash wizard-work/volt/CHECK-q116` rc **0** from the repo root and from `cwd=/`. Scoring is block-level because these records wrap — item 25's "29 days remaining" cell carries `validThrough` on the line above it, so a line scan reads it as clean.
|
||||
|
||||
**(2) Two rules the first draft got wrong, both caught by the instrument's own printed audit.** (a) The first scan exempted `apply-queue-2026-10-28.md:270` by pointing at line 280 — item **27**'s `WITHDRAWN` heading, one item away and unrelated; a marker now grants nothing unless the block naming it also names `validThrough`/`OpenTrain`, and cell `mutant-unnamed-subject` fails if that is loosened. (b) Q116's addendum is now an explicit grant **bounded to cells dated ≤ 2026-10-28**, read from the queue itself, so it can never reach a future cell — cell `mutant-addendum-future` reds if the bound is dropped. A grant must also come *after* the claim it corrects: `mutant-marker-before` reds otherwise.
|
||||
|
||||
**(3) The audit can fail — mutants on copies (`/root/.hermes/profiles/volt/cache/scratch/q116-a/`), never the shared tree.** Control rc **0**, 0 live. One fresh unmarked cell dated 11-02 → rc **1**, exactly **1** live, named `daily-2026-11-02.md:1`. Queue with the addendum stripped → rc **1**, **2** live (`apply-queue-2026-10-26.md:304`, `daily-2026-10-26.md:17`). Every `render-synthesized` wording removed from the 10-29/10-30 files → rc **1**, **8** live. Deleting the item-25 withdrawal line from both carrier files → rc **0**, 0 live: **that mutant did not bite**, because the tree carries redundant corrections; it is recorded as a non-bite rather than dropped. `--selftest` runs **14** cells (`14 passed, 0 failed`, rc 0), covering clean control, unmarked quote, marker-after, marker-before, cross-file grant, unnamed subject, unrelated-marker blanket, addendum grant, addendum bound, and four probe cells.
|
||||
|
||||
**(4) The mechanism at a fourth date, out of sample (2026-10-30, item 32).** `--probe` reads the page's own JSON-LD beside the response headers: `datePosted` **2026-10-09T10:42:55.303Z** — byte-identical to the value Q116 quotes from 2026-10-28 — and `validThrough` **2026-11-29T11:09:35.329Z**, i.e. **2026-11-27 → 2026-11-29, +2 days in 2 days**, exactly 30.00 days after an implied build of **2026-10-30T11:09:35Z** (`span_days 51.02`, `build_age_hours 2.15`). Item 25's page in the same minute carries **no** JSON-LD at all (202,760 B; `datePosted` 0, `JobPosting` 0, `fields {}`), so the field's *presence* is per-build as well — which is why "my text fetch does not surface validThrough" was never evidence about the posting.
|
||||
|
||||
**(5) A second field, same mechanism, measurable in seconds.** The response header `x-opentrain-job-presence-expires-at` is emitted as an absolute instant, and in every response `presence − Date` equals that response's own remaining `s-maxage`: measured **206/206**, **203/203**, **201/201**, **237.8/237** across four reads, and the absolute instant is identical across two **different** postings (`1793366280876` = 2026-10-30T13:18:00.876Z on items 25 and 32) — the same same-stamp-different-posting shape Q116 recorded for `validThrough`. This is the render-time-synthesis class observable directly: a value computed at response time and emitted as a property of the job. It is a different field with a different offset (231 s TTL, not 30 d); only the mechanism is shared.
|
||||
|
||||
**(6) Gate.** `bash wizard-work/volt/CHECK-q116` → cells `14 passed, 0 failed`, `q116: 2 leg(s), 0 fault(s) -- OK`, rc **0**; the record's own CHECK line grades under `python3 tools/check_lint.py --check`. `python3 tools/verify.py --repo .` over the landing tree is quoted in the check-in.
|
||||
|
||||
**Not claimed:** that the dated `career-lane/` cells are correct — they are dated-wrong, which the addendum distinguishes from corrected, and **no `career-lane/` byte was edited for this item** (the addendum's clause 1 is respected, so clause 2 is graded over a tree that deliberately still contains the quotes); that this fourth reading proves the 30-day rule by itself — it confirms a prediction from Q116's three readings, and two samples cannot separate "build + 30 d" from "build + any constant", which was never measured from the generator; that `presence` and `validThrough` are one field; that the block unit is clause-precise (a block that quotes and corrects itself reads as a correction, coarsely, and is printed); that the pipe's ruling is overturned — this operationalizes it, and a different reading of clause 2 (delete the dated lines, or accept the contradiction) remains the pipe's to make. Files edited for this item: `wizard-work/volt/opentrain_deadline_audit.py` (new), `wizard-work/volt/CHECK-q116` (new), `WORK-QUEUE.md` (this record, appended), `docs/queue-index.md` (re-rendered with the committed `tools/queue_index.py --write` in this tree). No `career-lane/` or `replies/` file was edited; nothing from `inbox/` is quoted. Report: `/root/.hermes/wizard-outputs/volt/2026-10-30-q116.md`.
|
||||
|
||||
CHECK: bash wizard-work/volt/CHECK-q116
|
||||
+40
-13
@@ -4,7 +4,7 @@
|
||||
|
||||
**What this is.** One row per item of `WORK-QUEUE.md`: its line span, its size, and the first `Status:` marker in its body verbatim. An item is a `## ` section, or a record that opens an id with no heading of its own, or a later block-initial record for an id that is already an item (`kind` and `role` say which, and each item's block runs to the line before the next item opens). Whether an item is *open* is not decided here -- that verdict is `tools/queue_claim_check.py`'s, and this document does not overrule it.
|
||||
|
||||
**What it exposes.** The queue is two-tier. 97 sections carry 86 ids because 11 ids own a second, continuation section; 38 further ids are opened by a record and own no heading at all, and they have rows and extents here -- 101 record blocks in all, 63 of them a later closure or correction for an id already opened. The same **38 ids** are listed again in the table below, which carries what a bounds row cannot -- the form that opened each one and how often the file names it: grepping `^## Q41` finds nothing while Q41 is a live item the file names 30 times.
|
||||
**What it exposes.** The queue is two-tier. 97 sections carry 86 ids because 11 ids own a second, continuation section; 38 further ids are opened by a record and own no heading at all, and they have rows and extents here -- 102 record blocks in all, 64 of them a later closure or correction for an id already opened. The same **38 ids** are listed again in the table below, which carries what a bounds row cannot -- the form that opened each one and how often the file names it: grepping `^## Q41` finds nothing while Q41 is a live item the file names 30 times.
|
||||
|
||||
| # | id | kind | lines | bytes | role | title | first `Status:` |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
@@ -205,7 +205,8 @@
|
||||
| 195 | Q126 | record | 13545-13556 | 6952 | record: BOLD-LEAD | **Q126 — DONE (volt, 2026-10-30; both faults were unowned tooling debt and both are closed by measurement: the nine host-bound test files now bind the module beside them, and the path-claims ledger … | -- |
|
||||
| 196 | Q110 | record | 13557-13576 | 8439 | cont: DONE | **Q110 — DONE (volt, 2026-10-30, at the commit this record lands in; quill lane by authorship, unclaimed and taken). `wizard-work/quill/QUICKSTART.md` no longer prints a command nobody ran: every … | -- |
|
||||
| 197 | Q127 | record | 13577-13598 | 9707 | record: BOLD-LEAD | **Q127 — OPENED and REPAIRED in the same tick (volt, 2026-10-30, at the commit this record lands in; spark lane by authorship, unclaimed and taken). A ten-file sweep put Q109's and Q120's pre-repair … | -- |
|
||||
| 198 | Q80 | record | 13599-13615 | 7450 | cont: DONE | **Q80 — DONE (volt, 2026-10-30, at the commit this record lands in; spark lane by authorship, unclaimed and taken). All four filed defects reproduce at HEAD `e8a916f`, and none survives: the digest … | -- |
|
||||
| 198 | Q80 | record | 13599-13616 | 7451 | cont: DONE | **Q80 — DONE (volt, 2026-10-30, at the commit this record lands in; spark lane by authorship, unclaimed and taken). All four filed defects reproduce at HEAD `e8a916f`, and none survives: the digest … | -- |
|
||||
| 199 | Q116 | record | 13617-13633 | 6411 | cont: DONE | **Q116 — DONE (volt, 2026-10-30, at the commit this record lands in; pipe lane by authorship, unclaimed and taken). The grep half of the amended done-means now has an instrument that can fail, it … | -- |
|
||||
|
||||
## Ids with no `## ` heading
|
||||
|
||||
@@ -240,7 +241,7 @@ Opened as records inside another section. `line` is the record's own opening lin
|
||||
| Q113 | 13278 | 13278 | 3 | Q112 | BULLET+BOLD | BOLD-LEAD |
|
||||
| Q114 | 13280 | 13280 | 5 | Q113 | BULLET+BOLD | BOLD-LEAD |
|
||||
| Q115 | 13282 | 13282 | 1 | Q114 | BULLET+BOLD | BOLD-LEAD |
|
||||
| Q116 | 13286 | 13286 | 4 | Q105 | BULLET+BOLD | BOLD-LEAD |
|
||||
| Q116 | 13286 | 13286 | 11 | Q105 | BULLET+BOLD | BOLD-LEAD |
|
||||
| Q117 | 13290 | 13288 | 6 | Q96 | BULLET+BOLD | BOLD-LEAD |
|
||||
| Q118 | 13300 | 13300 | 2 | Q111 | BULLET+BOLD | BOLD-LEAD |
|
||||
| Q119 | 13305 | 13305 | 5 | Q118 | BULLET+BOLD | BOLD-LEAD |
|
||||
@@ -1563,17 +1564,30 @@ The block below is the parse this document renders, byte for byte. `--check` com
|
||||
"title": "**Q127 \u2014 OPENED and REPAIRED in the same tick (volt, 2026-10-30, at the commit this record lands in; spark lane by authorship, unclaimed and taken). A ten-file sweep put Q109's and Q120's pre-repair \u2026"
|
||||
},
|
||||
{
|
||||
"bytes": 7450,
|
||||
"end_line": 13615,
|
||||
"bytes": 7451,
|
||||
"end_line": 13616,
|
||||
"id": "Q80",
|
||||
"kind": "record",
|
||||
"line": 13599,
|
||||
"lines": 17,
|
||||
"lines": 18,
|
||||
"role": "continuation",
|
||||
"role_detail": "DONE",
|
||||
"status": null,
|
||||
"status_line": null,
|
||||
"title": "**Q80 \u2014 DONE (volt, 2026-10-30, at the commit this record lands in; spark lane by authorship, unclaimed and taken). All four filed defects reproduce at HEAD `e8a916f`, and none survives: the digest \u2026"
|
||||
},
|
||||
{
|
||||
"bytes": 6411,
|
||||
"end_line": 13633,
|
||||
"id": "Q116",
|
||||
"kind": "record",
|
||||
"line": 13617,
|
||||
"lines": 17,
|
||||
"role": "continuation",
|
||||
"role_detail": "DONE",
|
||||
"status": null,
|
||||
"status_line": null,
|
||||
"title": "**Q116 \u2014 DONE (volt, 2026-10-30, at the commit this record lands in; pipe lane by authorship, unclaimed and taken). The grep half of the amended done-means now has an instrument that can fail, it \u2026"
|
||||
}
|
||||
],
|
||||
"ids_multi_section": {
|
||||
@@ -4273,17 +4287,30 @@ The block below is the parse this document renders, byte for byte. `--check` com
|
||||
"title": "**Q127 \u2014 OPENED and REPAIRED in the same tick (volt, 2026-10-30, at the commit this record lands in; spark lane by authorship, unclaimed and taken). A ten-file sweep put Q109's and Q120's pre-repair \u2026"
|
||||
},
|
||||
{
|
||||
"bytes": 7450,
|
||||
"end_line": 13615,
|
||||
"bytes": 7451,
|
||||
"end_line": 13616,
|
||||
"id": "Q80",
|
||||
"kind": "record",
|
||||
"line": 13599,
|
||||
"lines": 17,
|
||||
"lines": 18,
|
||||
"role": "continuation",
|
||||
"role_detail": "DONE",
|
||||
"status": null,
|
||||
"status_line": null,
|
||||
"title": "**Q80 \u2014 DONE (volt, 2026-10-30, at the commit this record lands in; spark lane by authorship, unclaimed and taken). All four filed defects reproduce at HEAD `e8a916f`, and none survives: the digest \u2026"
|
||||
},
|
||||
{
|
||||
"bytes": 6411,
|
||||
"end_line": 13633,
|
||||
"id": "Q116",
|
||||
"kind": "record",
|
||||
"line": 13617,
|
||||
"lines": 17,
|
||||
"role": "continuation",
|
||||
"role_detail": "DONE",
|
||||
"status": null,
|
||||
"status_line": null,
|
||||
"title": "**Q116 \u2014 DONE (volt, 2026-10-30, at the commit this record lands in; pipe lane by authorship, unclaimed and taken). The grep half of the amended done-means now has an instrument that can fail, it \u2026"
|
||||
}
|
||||
],
|
||||
"nested": [
|
||||
@@ -4535,7 +4562,7 @@ The block below is the parse this document renders, byte for byte. `--check` com
|
||||
"form": "BOLD-LEAD",
|
||||
"id": "Q116",
|
||||
"line": 13286,
|
||||
"refs": 4,
|
||||
"refs": 11,
|
||||
"section": "Q105",
|
||||
"shape": "BULLET+BOLD"
|
||||
},
|
||||
@@ -4635,9 +4662,9 @@ The block below is the parse this document renders, byte for byte. `--check` com
|
||||
"end_line": 8,
|
||||
"line": 1
|
||||
},
|
||||
"queue_bytes": 2174751,
|
||||
"queue_lines": 13615,
|
||||
"queue_sha256": "8dcbb0dc01717d8841ef41738f0b297ba674148eddf76109227ddeddb6c7e166",
|
||||
"queue_bytes": 2181163,
|
||||
"queue_lines": 13633,
|
||||
"queue_sha256": "e722fac21291a53a9a2e185d70d5aa52c760fd988548aab87b35169fb569b8f4",
|
||||
"sections": [
|
||||
{
|
||||
"bytes": 3139,
|
||||
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
# CHECK-q116: grade Q116 -- no career-lane/ cell quotes OpenTrain's
|
||||
# `validThrough` as an expiry or a remaining-days count.
|
||||
#
|
||||
# Two legs, both run here: `--selftest` (the scanner's own cells, no repo
|
||||
# needed) and the repo scan (career-lane/ + the Q116 addendum grant it reads
|
||||
# from WORK-QUEUE.md). The scan prints every exemption with the record that
|
||||
# grants it, so a marker that exempts too much is a reading a human can
|
||||
# overturn rather than a silence.
|
||||
#
|
||||
# Exit codes follow the repo's tools/ contract:
|
||||
# 0 = the grep half holds and the cells pass
|
||||
# 1 = a graded fault (a live deadline quote, or a failing cell)
|
||||
# 2 = could not grade (no python3, no career-lane/, no queue, no cells)
|
||||
#
|
||||
# Paths resolve from this file's own location, so a clone grades its own tree.
|
||||
set -u
|
||||
set -o pipefail
|
||||
HERE="$(dirname "$(readlink -f "$0")")"
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "CHECK: python3 not on PATH -- cannot grade." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
python3 "$HERE/opentrain_deadline_audit.py" --selftest >/tmp/.q116-cells.out 2>&1
|
||||
rc=$?
|
||||
tail -1 /tmp/.q116-cells.out
|
||||
case $rc in
|
||||
0) ;;
|
||||
1) echo "CHECK: cells failed -- see /tmp/.q116-cells.out" >&2; exit 1 ;;
|
||||
*) echo "CHECK: cells could not run (rc $rc)." >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
python3 "$HERE/opentrain_deadline_audit.py"
|
||||
case $? in
|
||||
0) echo "CHECK: q116 grep half holds."; exit 0 ;;
|
||||
1) echo "CHECK: live deadline quote in career-lane/ -- see the list above." >&2; exit 1 ;;
|
||||
*) echo "CHECK: scan could not grade -- see the message above." >&2; exit 2 ;;
|
||||
esac
|
||||
@@ -0,0 +1,599 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Grade Q116's grep half: no career-lane/ cell quotes OpenTrain's
|
||||
`validThrough` as an expiry or as a remaining-days count.
|
||||
|
||||
Q116 (filed 2026-10-28) asked for two queue notes to be corrected in place.
|
||||
Its 2026-10-28 addendum struck that half -- this archive appends, it does not
|
||||
rewrite -- and restated the done-means as a reading: `career-lane/` must carry
|
||||
no `validThrough` quoted as an expiry or a remaining-days count, while the
|
||||
superseded lines "stand as dated record with this addendum as their
|
||||
correction". Those two clauses can only both hold if the reading can tell a
|
||||
*live cell* from a *dated record*, which is what this file does.
|
||||
|
||||
Three verdicts per line, and nothing else:
|
||||
|
||||
LIVE a line naming OpenTrain or `validThrough` that quotes a
|
||||
remaining-days count, or pairs `validThrough` with "expir*", and
|
||||
carries no supersession after it. This is the Q116 fault.
|
||||
RECORD the same quote with a supersession marker later in the same file,
|
||||
or in a later-dated file that names this file's date and the
|
||||
quoted field. Dated record, exempt by the addendum.
|
||||
(clean) no quote at all.
|
||||
|
||||
A marker is only a supersession when it comes *after* the claim it corrects:
|
||||
a correction printed above a claim does not reach it, and cell 5 of
|
||||
`--selftest` fails if this ordering is loosened. Exemptions are always
|
||||
printed with the record that grants them, so a marker that exempts too much
|
||||
is a reading a human can overturn -- the failure mode Q117 named.
|
||||
|
||||
Legs: `quotes` (the scan above), `rule` (the replacement rule is written in a
|
||||
career tick file dated 2026-10-29 or later), `probe` (opt-in, `--probe URL`:
|
||||
measures the host's own render-time arithmetic).
|
||||
|
||||
Exit codes follow the repo's tools/ contract: 0 clean, 1 graded a fault,
|
||||
2 could not grade.
|
||||
|
||||
Usage:
|
||||
python3 opentrain_deadline_audit.py # scan + rule leg
|
||||
python3 opentrain_deadline_audit.py --selftest # cells, no repo needed
|
||||
python3 opentrain_deadline_audit.py --probe URL
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime as _dt
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[2]
|
||||
LANE = REPO / "career-lane"
|
||||
|
||||
# A cell about *this* field or *this* host.
|
||||
SUBJECT_RE = re.compile(r"validThrough|OpenTrain", re.I)
|
||||
# "<n> days remaining" / "<n> day remaining", any spacing.
|
||||
DAYS_LEFT_RE = re.compile(r"\b\d+\s+days?\s+remaining\b", re.I)
|
||||
# "expiry" / "expires" / "expiring" / "expiration".
|
||||
EXPIRY_RE = re.compile(r"expir", re.I)
|
||||
# Supersession markers. Any one, case-insensitively, means "this line is a
|
||||
# correction or a pointer to one", never "this line is a live cell".
|
||||
MARKERS = (
|
||||
"withdrawn",
|
||||
"superseded",
|
||||
"render-synthesized",
|
||||
"build+30d",
|
||||
"never a deadline",
|
||||
"never an expiry",
|
||||
"no expiry line",
|
||||
"not a deadline",
|
||||
"dated record",
|
||||
)
|
||||
# A supersession record has to say what it is superseding, or it is a blanket.
|
||||
SUPERSEDES_RE = re.compile(r"superseded|withdrawn", re.I)
|
||||
DATE_IN_NAME_RE = re.compile(r"(\d{4})-(\d{2})-(\d{2})")
|
||||
|
||||
|
||||
class Ungradable(Exception):
|
||||
"""Raised when the input cannot be graded at all (exit 2)."""
|
||||
|
||||
|
||||
def mmdd(name: str) -> str | None:
|
||||
"""'daily-2026-10-28.md' -> '10-28'; None when the name has no date."""
|
||||
m = DATE_IN_NAME_RE.search(name)
|
||||
return "%s-%s" % (m.group(2), m.group(3)) if m else None
|
||||
|
||||
|
||||
def file_date(name: str) -> str | None:
|
||||
m = DATE_IN_NAME_RE.search(name)
|
||||
return m.group(0) if m else None
|
||||
|
||||
|
||||
def has_marker(line: str) -> bool:
|
||||
low = line.lower()
|
||||
return any(m in low for m in MARKERS)
|
||||
|
||||
|
||||
def marker_names_subject(text: str) -> bool:
|
||||
"""A supersession record grants nothing unless it names the field it corrects.
|
||||
|
||||
Without this, `apply-queue-2026-10-28.md`'s item-27 "WITHDRAWN" heading
|
||||
exempted item 25's deadline quote 300 lines away -- the blanket-marker
|
||||
failure Q117 named, caught by this file's own `--show-record` audit.
|
||||
"""
|
||||
return has_marker(text) and bool(SUBJECT_RE.search(text))
|
||||
|
||||
|
||||
# A bullet, heading or bold lead starts a record block; a continuation line
|
||||
# (the wrapped remainder of a bullet) belongs to the block above it. Records in
|
||||
# this archive wrap, so quoting is judged over a block, never over a line.
|
||||
BULLET_RE = re.compile(r"^\s*(?:#+|[-*+]\s|\d+[.)]\s|\*\*)")
|
||||
|
||||
|
||||
def blocks(text: str) -> list[tuple[int, list[str]]]:
|
||||
"""Split into (0-based start index, lines) record blocks."""
|
||||
out: list[tuple[int, list[str]]] = []
|
||||
cur: list | None = None
|
||||
for i, line in enumerate(text.splitlines()):
|
||||
if not line.strip():
|
||||
cur = None
|
||||
continue
|
||||
if cur is None or BULLET_RE.match(line):
|
||||
cur = [i, [line]]
|
||||
out.append((cur[0], cur[1]))
|
||||
else:
|
||||
cur[1].append(line)
|
||||
return out
|
||||
|
||||
|
||||
def block_quote(lines: list[str]) -> str | None:
|
||||
"""Return the joined block when it quotes the field as a deadline."""
|
||||
joined = " ".join(ln.strip() for ln in lines)
|
||||
if not SUBJECT_RE.search(joined):
|
||||
return None
|
||||
if DAYS_LEFT_RE.search(joined):
|
||||
return joined
|
||||
if re.search(r"validThrough", joined, re.I) and EXPIRY_RE.search(joined):
|
||||
return joined
|
||||
return None
|
||||
|
||||
|
||||
def scan(files: dict[str, str], queue_text: str = "") -> dict:
|
||||
"""Classify every career-lane record block.
|
||||
|
||||
`files` maps file name -> full text. Pure: no I/O, so `--selftest` drives
|
||||
exactly the code the repo scan runs. `queue_text` is WORK-QUEUE.md, where
|
||||
Q116's addendum rules that the superseded career cells stand as dated
|
||||
record "with this addendum as their correction" -- an explicit grant,
|
||||
bounded to cells dated on or before the addendum's own date.
|
||||
"""
|
||||
live, record = [], []
|
||||
cutoff = addendum_cutoff(queue_text)
|
||||
for name in sorted(files):
|
||||
blist = blocks(files[name])
|
||||
texts = [" ".join(ln.strip() for ln in lines) for _, lines in blist]
|
||||
marked = [k for k, t in enumerate(texts) if marker_names_subject(t)]
|
||||
for k, (start, lines) in enumerate(blist):
|
||||
quoted = block_quote(lines)
|
||||
if not quoted:
|
||||
continue
|
||||
hit = {"file": name, "line": start + 1, "text": quoted}
|
||||
# (a) the block corrects itself, (b) a correction follows it here.
|
||||
if marker_names_subject(texts[k]):
|
||||
record.append(dict(hit, by="self (line %d)" % (start + 1)))
|
||||
continue
|
||||
after = [m for m in marked if m > k]
|
||||
if after:
|
||||
record.append(dict(hit, by="%s:%d"
|
||||
% (name, blist[after[0]][0] + 1)))
|
||||
continue
|
||||
|
||||
# (c) a later-dated file names this file's date and the field.
|
||||
token = mmdd(name)
|
||||
this_date = file_date(name) or ""
|
||||
by = None
|
||||
for other in sorted(files):
|
||||
if other == name or (file_date(other) or "") <= this_date:
|
||||
continue
|
||||
if not token:
|
||||
continue
|
||||
for j, oline in enumerate(files[other].splitlines()):
|
||||
if not SUPERSEDES_RE.search(oline):
|
||||
continue
|
||||
if token not in oline:
|
||||
continue
|
||||
if not SUBJECT_RE.search(oline) and "remaining" not in oline.lower():
|
||||
continue
|
||||
by = "%s:%d" % (other, j + 1)
|
||||
break
|
||||
if by:
|
||||
break
|
||||
if by:
|
||||
record.append(dict(hit, by=by))
|
||||
elif cutoff and (file_date(name) or "") <= cutoff:
|
||||
record.append(dict(hit, by="WORK-QUEUE.md Q116 addendum "
|
||||
"(grants cells dated <= %s)" % cutoff))
|
||||
else:
|
||||
live.append(hit)
|
||||
return {"live": live, "record": record, "addendum_cutoff": cutoff}
|
||||
|
||||
|
||||
ADDENDUM_RE = re.compile(r"Q116 addendum", re.I)
|
||||
|
||||
|
||||
def addendum_cutoff(queue_text: str) -> str | None:
|
||||
"""Date of Q116's append-only addendum, if the queue carries it.
|
||||
|
||||
The addendum rules that superseded career cells stand as dated record
|
||||
"with this addendum as their correction", so it is the correction of
|
||||
record for cells dated on or before the date it names. Anything later
|
||||
must carry its own marker: the grant cannot reach a future cell.
|
||||
"""
|
||||
if not queue_text:
|
||||
return None
|
||||
for start, lines in blocks(queue_text):
|
||||
joined = " ".join(ln.strip() for ln in lines)
|
||||
if ADDENDUM_RE.search(joined) and marker_names_subject(joined):
|
||||
d = DATE_IN_NAME_RE.search(joined)
|
||||
if d:
|
||||
return d.group(0)
|
||||
# "<name>, 2026-10-28)" forms: fall back to the block's own date.
|
||||
m = re.search(r"\b(20\d{2}-\d{2}-\d{2})\b", joined)
|
||||
return m.group(1) if m else None
|
||||
return None
|
||||
|
||||
|
||||
def rule_present(files: dict[str, str]) -> list[str]:
|
||||
"""Files dated 2026-10-29 or later that carry the replacement rule."""
|
||||
found = []
|
||||
for name in sorted(files):
|
||||
d = file_date(name)
|
||||
if not d or d < "2026-10-29":
|
||||
continue
|
||||
low = files[name].lower()
|
||||
if "render-synthesized" in low and "build+30d" in low:
|
||||
found.append(name)
|
||||
return found
|
||||
|
||||
|
||||
def load_lane(lane: Path = LANE) -> dict[str, str]:
|
||||
if not lane.is_dir():
|
||||
raise Ungradable("no career-lane/ at %s" % lane)
|
||||
files = {}
|
||||
for p in sorted(lane.glob("*.md")):
|
||||
try:
|
||||
files[p.name] = p.read_text(encoding="utf-8")
|
||||
except OSError as exc:
|
||||
raise Ungradable("cannot read %s: %s" % (p, exc))
|
||||
if not files:
|
||||
raise Ungradable("career-lane/ holds no .md files at %s" % lane)
|
||||
return files
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Probe leg: the host's own render-time arithmetic, measured in seconds.
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
EPOCH = _dt.datetime(1970, 1, 1, tzinfo=_dt.timezone.utc)
|
||||
|
||||
|
||||
def _http_date(value: str) -> float:
|
||||
return _dt.datetime.strptime(value, "%a, %d %b %Y %H:%M:%S GMT").replace(
|
||||
tzinfo=_dt.timezone.utc).timestamp()
|
||||
|
||||
|
||||
def presence_reading(headers: dict, body: str = "") -> dict:
|
||||
"""Classify x-opentrain-job-presence-expires-at against the same response.
|
||||
|
||||
Q116's mechanism claim is that this host emits absolute timestamps
|
||||
computed at render time. If `presence - Date` equals the response's own
|
||||
remaining `s-maxage`, the header is render-instant + TTL, i.e. the same
|
||||
class as `validThrough` -- provable in seconds instead of over 30 days.
|
||||
"""
|
||||
h = {k.lower(): v for k, v in headers.items()}
|
||||
out = {"fields": {}, "verdict": None}
|
||||
if "date" not in h or "x-opentrain-job-presence-expires-at" not in h:
|
||||
out["verdict"] = "NO-FIELDS"
|
||||
return out
|
||||
try:
|
||||
date_epoch = _http_date(h["date"])
|
||||
presence = int(h["x-opentrain-job-presence-expires-at"]) / 1000.0
|
||||
except (ValueError, TypeError):
|
||||
out["verdict"] = "UNPARSEABLE"
|
||||
return out
|
||||
delta = presence - date_epoch
|
||||
out["date_epoch"] = date_epoch
|
||||
out["presence_epoch"] = presence
|
||||
out["presence_delta_s"] = round(delta, 1)
|
||||
ttl = re.search(r"s-maxage=(\d+)", h.get("cache-control", "")
|
||||
+ h.get("cdn-cache-control", ""))
|
||||
if ttl:
|
||||
out["s_maxage_s"] = int(ttl.group(1))
|
||||
out["verdict"] = ("RENDER-COMPUTED"
|
||||
if abs(delta - out["s_maxage_s"]) <= 2 else "OTHER")
|
||||
else:
|
||||
out["verdict"] = "NO-TTL"
|
||||
for key in ("dateposted", "validthrough"):
|
||||
m = re.search(r'"%s":"([^"]+)"' % key, body, re.I)
|
||||
if m:
|
||||
out["fields"][key] = m.group(1)
|
||||
if "dateposted" in out["fields"] and "validthrough" in out["fields"]:
|
||||
try:
|
||||
dp = _dt.datetime.strptime(out["fields"]["dateposted"],
|
||||
"%Y-%m-%dT%H:%M:%S.%fZ").replace(
|
||||
tzinfo=_dt.timezone.utc)
|
||||
vt = _dt.datetime.strptime(out["fields"]["validthrough"],
|
||||
"%Y-%m-%dT%H:%M:%S.%fZ").replace(
|
||||
tzinfo=_dt.timezone.utc)
|
||||
out["span_days"] = round((vt - dp).total_seconds() / 86400.0, 2)
|
||||
out["build_implied"] = (vt - _dt.timedelta(days=30)).strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ")
|
||||
# Measured, not asserted: how old the implied build is *relative to
|
||||
# the response's own Date header*. A value that always reads "30
|
||||
# days ahead" is only a deadline if this age is stable across
|
||||
# readings; the field is per-build, so it is the build's age here.
|
||||
if "date_epoch" in out:
|
||||
out["build_age_hours"] = round(
|
||||
(out["date_epoch"] - (vt - _dt.timedelta(days=30)).timestamp())
|
||||
/ 3600.0, 2)
|
||||
except ValueError:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def probe(url: str, timeout: float = 25.0) -> dict:
|
||||
req = urllib.request.Request(url, method="GET",
|
||||
headers={"User-Agent": "volt-q116-audit/1"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
body = resp.read().decode("utf-8", "replace")
|
||||
headers = dict(resp.headers.items())
|
||||
status = resp.status
|
||||
except Exception as exc: # noqa: BLE001 - reported, not swallowed
|
||||
raise Ungradable("fetch failed for %s: %s" % (url, exc))
|
||||
reading = presence_reading(headers, body)
|
||||
reading.update({"url": url, "status": status, "bytes": len(body)})
|
||||
return reading
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Cells
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
CLEAN = ("career-lane/daily-2026-10-29.md", """
|
||||
- **Reply 145 section 0 -- the item-25 expiry correction is WITHDRAWN.** OpenTrain's
|
||||
`validThrough` is render-synthesized at build + 30 days; it is never a deadline.
|
||||
""")
|
||||
|
||||
LIVE = ("career-lane/daily-2026-10-28.md", """
|
||||
1. **OpenTrain item 25 -- LIVE, validThrough 2026-11-26 (page-computed),
|
||||
29 days remaining as of today, datePosted 2026-10-07.**
|
||||
""")
|
||||
|
||||
ORDERED = ("career-lane/apply-queue-2026-10-28.md", """
|
||||
- **2026-10-28 note:** `validThrough` 2026-11-26, 29 days remaining as of 2026-10-28.
|
||||
- **2026-10-29:** item 25 correction WITHDRAWN: OpenTrain's `validThrough` is
|
||||
render-synthesized at build+30d, never a deadline.
|
||||
""")
|
||||
|
||||
# A correction that never names the field it corrects grants nothing.
|
||||
UNNAMED = ("career-lane/apply-queue-2026-10-28.md", """
|
||||
- **2026-10-28 note:** `validThrough` 2026-11-26, 29 days remaining as of 2026-10-28.
|
||||
- **2026-10-29:** item 25 WITHDRAWN: render-synthesized, build+30d.
|
||||
""")
|
||||
|
||||
REVERSED = ("career-lane/apply-queue-2026-10-28.md", """
|
||||
- **2026-10-29:** item 25 correction WITHDRAWN: OpenTrain's `validThrough` is
|
||||
render-synthesized at build+30d.
|
||||
- **2026-10-28 note:** `validThrough` 2026-11-26, 29 days remaining as of 2026-10-28.
|
||||
""")
|
||||
|
||||
CROSS = ("career-lane/apply-queue-2026-10-28.md", """
|
||||
- **2026-10-28 note:** `validThrough` 2026-11-26, 29 days remaining as of 2026-10-28.
|
||||
"""), ("career-lane/apply-queue-2026-10-30.md", """
|
||||
- **2026-10-30:** the 10-28 `validThrough` "29 days remaining" cell is Superseded:
|
||||
the field is render-synthesized, build+30d.
|
||||
""")
|
||||
|
||||
OTHER_VENDOR = ("career-lane/daily-2026-10-28.md", """
|
||||
- **Alignerr item 29:** $20-100/hr, header badge span, 12 days remaining on the listing.
|
||||
""")
|
||||
|
||||
# Q116's addendum, as WORK-QUEUE.md carries it: the grant for pre-addendum
|
||||
# cells, and the shape the cutoff is read from.
|
||||
ADDENDUM_FIXTURE = (
|
||||
"**Q116 addendum (pipe, 2026-10-28) -- the \"corrected in place\" half of "
|
||||
"the done-means is struck as my error, and append-only wins.** The "
|
||||
"superseded lines stand as dated record with this addendum as their "
|
||||
"correction. The grep half is unchanged: no `validThrough` quoted as an "
|
||||
"expiry or a remaining-days count.\n")
|
||||
|
||||
|
||||
def _one(files: dict[str, str]) -> dict:
|
||||
return scan(files)
|
||||
|
||||
|
||||
def selftest() -> int:
|
||||
cells = []
|
||||
|
||||
def cell(name, fn):
|
||||
try:
|
||||
fn()
|
||||
cells.append((name, True, ""))
|
||||
except AssertionError as exc:
|
||||
cells.append((name, False, str(exc) or "assertion failed"))
|
||||
|
||||
def control_clean():
|
||||
r = _one(dict([CLEAN]))
|
||||
assert not r["live"], "clean control reported a live quote: %r" % r["live"]
|
||||
|
||||
def mutant_live_unmarked():
|
||||
r = _one(dict([LIVE]))
|
||||
assert len(r["live"]) == 1, "unmarked remaining-days quote not flagged"
|
||||
assert r["live"][0]["line"] == 2, r["live"][0]
|
||||
|
||||
def record_marker_after():
|
||||
r = _one(dict([ORDERED]))
|
||||
assert not r["live"], "marker after the claim did not exempt it"
|
||||
assert len(r["record"]) == 1, "exemption not reported as a record"
|
||||
|
||||
def mutant_marker_before():
|
||||
r = _one(dict([REVERSED]))
|
||||
assert len(r["live"]) == 1, (
|
||||
"a correction printed ABOVE the claim exempted it -- ordering lost")
|
||||
|
||||
def record_cross_file():
|
||||
r = _one(dict(CROSS))
|
||||
assert not r["live"], "later-dated supersession record did not exempt"
|
||||
assert r["record"], "cross-file exemption not reported"
|
||||
|
||||
def mutant_other_vendor():
|
||||
r = _one(dict([OTHER_VENDOR]))
|
||||
assert not r["live"], "flagged a non-OpenTrain vendor line"
|
||||
|
||||
def mutant_blanket_marker():
|
||||
# A marker about a different item grants nothing, however near it sits.
|
||||
files = dict([LIVE])
|
||||
files["career-lane/daily-2026-10-29.md"] = (
|
||||
"- **2026-10-27:** item 27 Gramian WITHDRAWN at source.\n")
|
||||
r = _one(files)
|
||||
assert len(r["live"]) == 1, "an unrelated WITHDRAWN exempted a cell"
|
||||
|
||||
def addendum_grants_past_cells():
|
||||
r = scan(dict([LIVE]), ADDENDUM_FIXTURE)
|
||||
assert not r["live"], "the Q116 addendum did not exempt a pre-addendum cell"
|
||||
assert r["addendum_cutoff"] == "2026-10-28", r["addendum_cutoff"]
|
||||
|
||||
def mutant_addendum_cannot_reach_future():
|
||||
future = ("career-lane/daily-2026-10-31.md",
|
||||
"- **10-31:** OpenTrain item 25 validThrough 2026-11-30, "
|
||||
"29 days remaining as of today.\n")
|
||||
r = scan(dict([future]), ADDENDUM_FIXTURE)
|
||||
assert len(r["live"]) == 1, (
|
||||
"the addendum grant reached a cell dated after it -- grant is unbounded")
|
||||
|
||||
def mutant_marker_unnamed_subject():
|
||||
r = _one(dict([UNNAMED]))
|
||||
assert len(r["live"]) == 1, (
|
||||
"a correction that never names validThrough/OpenTrain exempted a cell")
|
||||
|
||||
def probe_observed():
|
||||
# The three readings taken 2026-10-30T13:14:34-39Z, verbatim.
|
||||
for delta, smax in ((206, 206), (203, 203), (201, 201)):
|
||||
hdrs = {"date": "Fri, 30 Oct 2026 13:14:34 GMT",
|
||||
"x-opentrain-job-presence-expires-at":
|
||||
str(int(1793366074 * 1000 + delta * 1000)),
|
||||
"cache-control": "public, max-age=0, s-maxage=%d, must-revalidate" % smax}
|
||||
rd = presence_reading(hdrs)
|
||||
assert rd["verdict"] == "RENDER-COMPUTED", (delta, smax, rd)
|
||||
|
||||
def probe_mutant_drifted():
|
||||
hdrs = {"date": "Fri, 30 Oct 2026 13:14:34 GMT",
|
||||
"x-opentrain-job-presence-expires-at": "1793367000000",
|
||||
"cache-control": "s-maxage=206"}
|
||||
assert presence_reading(hdrs)["verdict"] == "OTHER", "drift not detected"
|
||||
|
||||
def probe_fields_observed():
|
||||
# The item-32 page as read 2026-10-30T13:18:55Z, verbatim.
|
||||
hdrs = {"date": "Fri, 30 Oct 2026 13:18:55 GMT",
|
||||
"x-opentrain-job-presence-expires-at": "1793366572773",
|
||||
"cache-control": "public, max-age=0, s-maxage=237, must-revalidate"}
|
||||
body = ('{"@type":"JobPosting","datePosted":"2026-10-09T10:42:55.303Z",'
|
||||
'"validThrough":"2026-11-29T11:09:35.329Z"}')
|
||||
rd = presence_reading(hdrs, body)
|
||||
assert rd["fields"].get("validthrough") == "2026-11-29T11:09:35.329Z", rd
|
||||
assert rd["span_days"] == 51.02, rd.get("span_days")
|
||||
assert rd["build_implied"] == "2026-10-30T11:09:35Z", rd["build_implied"]
|
||||
# The measured half: the implied build is hours old, not "30 days ahead".
|
||||
assert 0 <= rd["build_age_hours"] <= 6, rd.get("build_age_hours")
|
||||
|
||||
def mutant_probe_fields_absent():
|
||||
hdrs = {"date": "Fri, 30 Oct 2026 13:18:55 GMT",
|
||||
"x-opentrain-job-presence-expires-at": "1793366572773",
|
||||
"cache-control": "s-maxage=237"}
|
||||
rd = presence_reading(hdrs, "<html>no JSON-LD here</html>")
|
||||
assert not rd["fields"], "fields invented from a page that has none"
|
||||
|
||||
cell("control-clean", control_clean)
|
||||
cell("mutant-live-unmarked", mutant_live_unmarked)
|
||||
cell("record-marker-after", record_marker_after)
|
||||
cell("mutant-marker-before", mutant_marker_before)
|
||||
cell("record-cross-file", record_cross_file)
|
||||
cell("mutant-other-vendor", mutant_other_vendor)
|
||||
cell("mutant-blanket-marker", mutant_blanket_marker)
|
||||
cell("mutant-unnamed-subject", mutant_marker_unnamed_subject)
|
||||
cell("addendum-grants-past", addendum_grants_past_cells)
|
||||
cell("mutant-addendum-future", mutant_addendum_cannot_reach_future)
|
||||
cell("probe-observed", probe_observed)
|
||||
cell("probe-mutant-drifted", probe_mutant_drifted)
|
||||
cell("probe-fields-observed", probe_fields_observed)
|
||||
cell("mutant-fields-absent", mutant_probe_fields_absent)
|
||||
|
||||
failed = 0
|
||||
for name, ok, msg in cells:
|
||||
print(" %s %-24s %s" % ("PASS" if ok else "FAIL", name, msg))
|
||||
if not ok:
|
||||
failed += 1
|
||||
print("selftest: %d cells, %d passed, %d failed" % (len(cells), len(cells) - failed, failed))
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument("--selftest", action="store_true", help="run the cells only")
|
||||
ap.add_argument("--probe", metavar="URL", help="measure one posting's fields")
|
||||
ap.add_argument("--json", action="store_true", help="machine reading")
|
||||
ap.add_argument("--show-record", action="store_true",
|
||||
help="print every exemption with the record that grants it")
|
||||
ap.add_argument("--lane", metavar="DIR",
|
||||
help="grade another career-lane/ (default: this tree's)")
|
||||
ap.add_argument("--queue", metavar="FILE",
|
||||
help="the queue carrying the addendum (default: this tree's)")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
if args.selftest:
|
||||
return selftest()
|
||||
|
||||
if args.probe:
|
||||
try:
|
||||
rd = probe(args.probe)
|
||||
except Ungradable as exc:
|
||||
print("could not grade: %s" % exc, file=sys.stderr)
|
||||
return 2
|
||||
print(json.dumps(rd, indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
try:
|
||||
files = load_lane(Path(args.lane) if args.lane else LANE)
|
||||
except Ungradable as exc:
|
||||
print("could not grade: %s" % exc, file=sys.stderr)
|
||||
return 2
|
||||
|
||||
queue_path = Path(args.queue) if args.queue else REPO / "WORK-QUEUE.md"
|
||||
try:
|
||||
queue_text = queue_path.read_text(encoding="utf-8")
|
||||
except OSError as exc:
|
||||
print("could not grade: cannot read %s: %s" % (queue_path, exc),
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
|
||||
result = scan(files, queue_text)
|
||||
rule_files = rule_present(files)
|
||||
faults = 0
|
||||
|
||||
live = result["live"]
|
||||
if live:
|
||||
faults += 1
|
||||
print("quotes: FAULT -- %d live cell(s) quote OpenTrain validThrough "
|
||||
"as an expiry or a remaining-days count" % len(live))
|
||||
for hit in live:
|
||||
print(" %s:%d %s" % (hit["file"], hit["line"], hit["text"][:150]))
|
||||
else:
|
||||
print("quotes: PASS -- %d deadline-quote block(s) in %d file(s), all "
|
||||
"exempt as dated record, 0 live" % (len(result["record"]),
|
||||
len({r["file"] for r in result["record"]})))
|
||||
print(" addendum grant: cells dated <= %s"
|
||||
% (result["addendum_cutoff"] or "none (addendum absent)"))
|
||||
|
||||
if args.show_record:
|
||||
for hit in result["record"]:
|
||||
print(" record %s:%d by %s" % (hit["file"], hit["line"], hit["by"]))
|
||||
|
||||
if rule_files:
|
||||
print("rule: PASS -- replacement rule present in %s" % ", ".join(rule_files))
|
||||
else:
|
||||
faults += 1
|
||||
print("rule: FAULT -- no career-lane file dated 2026-10-29 or later "
|
||||
"states the render-synthesized rule")
|
||||
|
||||
print("q116: %d leg(s), %d fault(s) -- %s"
|
||||
% (2, faults, "OK" if not faults else "FAULT"))
|
||||
return 0 if not faults else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user