@r1z4x/tezgah 1.8.3 → 1.9.0

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.
@@ -267,7 +267,10 @@ def _toplevel(real):
267
267
 
268
268
 
269
269
  def linked_main(path):
270
- """The main checkout of the nearest linked worktree holding `path`, else None.
270
+ """The main checkout of the nearest linked worktree holding `path`, else None;
271
+ for a worktree of a bare repository, which has no main checkout, the bare
272
+ git directory (`_bare_repo`): its `.tezgah` is the workspace no `git worktree
273
+ remove`, detach or switch touches (plan 098).
271
274
 
272
275
  Nearest only: `worktrees` asks it about the repository it was handed, so a
273
276
  repo nested inside a worktree (a vendored clone, a submodule) answers None and
@@ -275,8 +278,8 @@ def linked_main(path):
275
278
  uses. Read from the `.git` pointer file (`gitdir: <main>/.git/worktrees/
276
279
  <name>`), never from git: `root_for` runs on every gate call and a session
277
280
  start's git forks are pinned. A plain checkout (`.git` is a directory), a
278
- submodule (a `/modules/` pointer) and a bare main (no `.git` component)
279
- answer None."""
281
+ submodule (a `/modules/` pointer) and a worktree of a git directory kept
282
+ apart from its main checkout (`--separate-git-dir`) answer None."""
280
283
  top = _toplevel(os.path.realpath(path))
281
284
  if not top:
282
285
  return None
@@ -314,27 +317,72 @@ def _linked_main_at(top):
314
317
  gitdir = os.path.normpath(os.path.join(top, line[len("gitdir:"):].strip()))
315
318
  dotgit = os.path.dirname(os.path.dirname(gitdir))
316
319
  if os.path.basename(os.path.dirname(gitdir)) != "worktrees" \
317
- or os.path.basename(dotgit) != ".git" \
318
320
  or not os.path.isfile(os.path.join(gitdir, "commondir")):
319
321
  return None
320
- return os.path.realpath(os.path.dirname(dotgit))
322
+ if os.path.basename(dotgit) == ".git":
323
+ return os.path.realpath(os.path.dirname(dotgit))
324
+ return os.path.realpath(dotgit) if _bare_repo(dotgit) else None
325
+
326
+
327
+ def _bare_repo(path):
328
+ """True when `path` is the git directory of a bare repository: a `HEAD`, an
329
+ `objects/` and `core.bare` set in its config, read without a git fork."""
330
+ if not (os.path.isfile(os.path.join(path, "HEAD"))
331
+ and os.path.isdir(os.path.join(path, "objects"))):
332
+ return False
333
+ section = ""
334
+ try:
335
+ with open(os.path.join(path, "config"), encoding="utf-8", errors="replace") as fh:
336
+ for line in fh:
337
+ line = line.strip()
338
+ if not line or line[0] in "#;":
339
+ continue
340
+ if line.startswith("["):
341
+ section = line.strip("[]").split('"')[0].strip().lower()
342
+ continue
343
+ key, eq, value = line.partition("=")
344
+ if section == "core" and key.strip().lower() == "bare":
345
+ return not eq or value.strip().lower() in ("true", "yes", "on", "1")
346
+ except OSError:
347
+ pass
348
+ return False
349
+
350
+
351
+ def _admin_dir(main):
352
+ """The git directory of the main checkout `main` (`<main>/.git`), or `main`
353
+ itself when it is a bare repository's; None for neither."""
354
+ dot = os.path.join(main, ".git")
355
+ if os.path.isdir(dot):
356
+ return dot
357
+ return main if _bare_repo(main) else None
358
+
359
+
360
+ def work_tree(repo, cwd=None):
361
+ """The checkout whose project files a command about `repo` reads: `repo`
362
+ itself, unless it is a bare repository's git directory, which holds the
363
+ workspace and no project file (plan 098) - then the worktree of it holding
364
+ `cwd` (the process's by default), else `repo`."""
365
+ if not _bare_repo(repo):
366
+ return repo
367
+ top = _toplevel(os.path.realpath(cwd or os.getcwd()))
368
+ return top if top and linked_main(top) == os.path.realpath(repo) else repo
321
369
 
322
370
 
323
371
  def worktrees(repo):
324
372
  """Every checkout of the repository `repo` is in, main first, as real paths;
325
- [] outside a git checkout.
373
+ [] outside a git checkout. A bare repository's git directory stands first,
374
+ in the main checkout's place (`linked_main`).
326
375
 
327
376
  Fork-free, from `<main>/.git/worktrees/*/gitdir` (each names a linked
328
377
  worktree's `.git`). An entry whose target is gone - deleted without `git
329
378
  worktree remove` - is skipped: git itself keeps it, marked prunable, until
330
- `git worktree prune`. Each checkout keeps its own `.tezgah`; this only names
331
- them."""
332
- main = linked_main(repo)
333
- if not main:
334
- main = _toplevel(os.path.realpath(repo))
335
- if not main or not os.path.isdir(os.path.join(main, ".git")):
336
- return []
337
- base = os.path.join(main, ".git", "worktrees")
379
+ `git worktree prune`."""
380
+ real = os.path.realpath(repo)
381
+ main = linked_main(real) or (real if _bare_repo(real) else _toplevel(real))
382
+ admin = _admin_dir(main) if main else None
383
+ if not admin:
384
+ return []
385
+ base = os.path.join(admin, "worktrees")
338
386
  out = [main]
339
387
  try:
340
388
  names = sorted(os.listdir(base))
@@ -1225,14 +1273,18 @@ def _index_tracks_workspace(root):
1225
1273
 
1226
1274
  def ensure_workspace(repo):
1227
1275
  """`<repo>/.tezgah`, created, ignored by the project and holding its own git
1228
- repository; None when `repo` is not a git work tree or a step failed.
1276
+ repository; None when `repo` is not a git work tree or a bare repository's
1277
+ git directory, or a step failed.
1229
1278
 
1230
1279
  The ignore lines are appended to the project's `.gitignore` only when
1231
- missing, after every line the user wrote, and nothing is staged. A second
1232
- call costs three stats and one file read: the subprocess runs only when
1233
- `.tezgah/.git` does not exist yet. Fail-open: a read-only tree returns None
1234
- and the session goes on."""
1235
- if not os.path.exists(os.path.join(repo, ".git")):
1280
+ missing, after every line the user wrote, and nothing is staged; a bare
1281
+ repository gets none, since no worktree reads its git directory as content
1282
+ (its worktrees' links are excluded by `link_workspace`). A second call costs
1283
+ three stats and one file read: the subprocess runs only when `.tezgah/.git`
1284
+ does not exist yet. Fail-open: a read-only tree returns None and the session
1285
+ goes on."""
1286
+ bare = _bare_repo(repo)
1287
+ if not bare and not os.path.exists(os.path.join(repo, ".git")):
1236
1288
  return None
1237
1289
  ws = workspace(repo)
1238
1290
  # A `.tezgah` that is a symlink (or anything but a directory) came with the
@@ -1255,7 +1307,8 @@ def ensure_workspace(repo):
1255
1307
  except FileNotFoundError:
1256
1308
  body = ""
1257
1309
  have = {line.strip().strip("/") for line in body.splitlines()}
1258
- missing = [line for line in WORKSPACE_IGNORES if line.strip("/") not in have]
1310
+ missing = [] if bare else [line for line in WORKSPACE_IGNORES
1311
+ if line.strip("/") not in have]
1259
1312
  if missing:
1260
1313
  with open(path, "a", encoding="utf-8") as fh:
1261
1314
  fh.write(("\n" if body and not body.endswith("\n") else "")
@@ -1273,16 +1326,17 @@ def ensure_workspace(repo):
1273
1326
 
1274
1327
  def link_workspace(checkout, main):
1275
1328
  """`<checkout>/.tezgah` as a link to the main checkout's workspace, for a
1276
- linked worktree (plan 100): a relative `.tezgah/...` write from the worktree
1277
- then lands in the one workspace every checkout reads, instead of a second
1329
+ linked worktree (plan 100), or to a bare repository's when `main` is its git
1330
+ directory (plan 098): a relative `.tezgah/...` write from the worktree then
1331
+ lands in the one workspace every checkout reads, instead of a second
1278
1332
  `.tezgah` that `git worktree remove` deletes. `/.tezgah` goes to the
1279
1333
  repository's `info/exclude`, since the project's `/.tezgah/` ignores only a
1280
1334
  directory. Leaves any existing `.tezgah` alone; None when nothing was made."""
1281
1335
  link, target = workspace(checkout), workspace(main)
1282
- exclude = os.path.join(main, ".git", "info", "exclude")
1283
- if (os.path.lexists(link) or not os.path.isdir(target)
1284
- or not os.path.isdir(os.path.join(main, ".git"))):
1336
+ admin = _admin_dir(main)
1337
+ if os.path.lexists(link) or not os.path.isdir(target) or not admin:
1285
1338
  return None
1339
+ exclude = os.path.join(admin, "info", "exclude")
1286
1340
  try:
1287
1341
  os.makedirs(os.path.dirname(exclude), exist_ok=True)
1288
1342
  try:
@@ -1319,8 +1373,10 @@ def _git_config_paths(repo):
1319
1373
  gitdir = gitdir if os.path.isabs(gitdir) else os.path.join(repo, gitdir)
1320
1374
  out += [os.path.join(gitdir, "config"),
1321
1375
  os.path.join(gitdir, "..", "..", "config")]
1322
- elif os.path.isdir(dot):
1323
- out.append(os.path.join(dot, "config"))
1376
+ else:
1377
+ admin = _admin_dir(repo)
1378
+ if admin:
1379
+ out.append(os.path.join(admin, "config"))
1324
1380
  out.append(os.path.join(HOME, ".gitconfig"))
1325
1381
  out.append(os.path.join(CONFIG_DIR_GIT, "git", "config"))
1326
1382
  return out
@@ -233,7 +233,10 @@ in a cheaper, deferred, stubbed or partial stand-in for a requested item;
233
233
  silently narrowing the scope; deciding an item is unnecessary, "YAGNI" or "out
234
234
  of scope" and dropping it; a token gesture (a comment, a TODO, a mention)
235
235
  reported as if the item were done; stopping early because the work got long or
236
- the rest is hard. A requested item may leave scope only when the user takes it
236
+ the rest is hard. Its kind is the ceiling: a research, analysis, review or plan
237
+ ask ends at the written plan, and building that plan is a class-F step (see
238
+ "Ask policy"): asked, and never started in the turn that wrote it. A finding
239
+ outside the ask becomes a plan too, never code. A requested item may leave scope only when the user takes it
237
240
  out. If an item looks redundant, impossible or genuinely out of scope, put it in
238
241
  the approval batch with "keep it" as the recommended default and keep working on
239
242
  it: the work never proceeds on a cut. An unsafe item is a class-F step (see "Ask
@@ -298,7 +301,7 @@ whether the user already decided it (named it this session, or granted it).
298
301
  | C. Reversible choice the evidence settles | a fix's scope inside the ask, naming, a palette when tokens exist | decide from evidence; real taste goes in the batch with a default, taken this turn |
299
302
  | D. Intent fork | two readings of the ask with different deliverables | batch with a default; do the shared part first and proceed on the default |
300
303
  | E. Outward on the user's own remote | push, PR, tag, release, publishing the user's own package | act on a grant written in chat ({GRANT_EXAMPLES}); else ask once per session, never again after a grant |
301
- | F. Never on a default | force push or history rewrite on a shared ref; deleting data the session did not make; production deploy, stop or migration; a credential, account or identity change; sending to a third party; a spend over `spend_cap` | do all other work first, ask, and wait: silence is no consent, and only an answer that names the action and its target clears it |
304
+ | F. Never on a default | force push or history rewrite on a shared ref; deleting data the session did not make; production deploy, stop or migration; a credential, account or identity change; sending to a third party; a spend over `spend_cap`; building a plan this turn wrote from a research, analysis, review or plan ask | do all other work first, ask, and wait: silence is no consent, and only an answer that names the action and its target clears it |
302
305
  | G. {ACCESS} | a credential or grant no tool supplies | take every route that needs none first, then ask once, naming what it unlocks |
303
306
 
304
307
  A grant is read from the user's prompt and recorded for the session; "artık
@@ -836,7 +839,9 @@ examples. Off: `tezgah-adhd off`, or the repo's `.no-adhd`.
836
839
 
837
840
  **Deliver the whole ask; never the shortcut.** The request defines the
838
841
  deliverable: every named item is in scope until the user says otherwise, and the
839
- ask is a floor, not a ceiling. Ponytail shrinks the solution, never the request.
842
+ ask is a floor, not a ceiling - its kind is one: a research, analysis, review or
843
+ plan ask ends at the written plan, and building it waits for the user's answer.
844
+ Ponytail shrinks the solution, never the request.
840
845
  FORBIDDEN: swapping in a cheaper, deferred or partial stand-in for what was
841
846
  asked; silently narrowing scope; deciding a requested item is
842
847
  "unnecessary"/"YAGNI" and dropping it; a token gesture reported as done; stopping
@@ -880,7 +885,8 @@ lines: item - blocker - evidence - what unblocks it. No blockers: no heading.
880
885
  Work and process - the order, "shall I go on", a check, a plan, a consult
881
886
  member, a spend within `spend_cap`, a local merge of the session's branch,
882
887
  removing its worktree - are taken and stated in one line, never asked; a taste
883
- or intent fork goes in the batch with a default taken this turn. An outward step
888
+ or intent fork goes in the batch with a default taken this turn, but building a
889
+ plan this turn wrote waits for the answer. An outward step
884
890
  on the user's own remote (push, PR, release, their package) runs on a grant
885
891
  ({GRANT_EXAMPLES}), else is asked once. Never on a
886
892
  default - ask and wait for an answer naming action and target: a force push,
@@ -2095,7 +2095,8 @@ def append_claim(repo, slug, claim):
2095
2095
  # The judgement runs under the lock and reads the ids from the descriptor
2096
2096
  # holding it (`_claim_ids`): the ids a claim is judged against are the ids
2097
2097
  # the append commits beside.
2098
- problems = claim_problems(claim, base, repo, held=_claim_ids(base, handle))
2098
+ problems = claim_problems(claim, base, tp.work_tree(repo),
2099
+ held=_claim_ids(base, handle))
2099
2100
  if problems:
2100
2101
  if created and os.path.getsize(path) == 0:
2101
2102
  os.remove(path)
@@ -3556,7 +3557,9 @@ def check_line(repo, slug, git=True, strict=False, code=None):
3556
3557
  `strict=True` turns every guarantee the checker cannot decide into an error.
3557
3558
  `code` is the checkout the check runs in when it is not `repo`: the line
3558
3559
  lives in the main checkout's workspace (plan 097) and its citations and
3559
- proofs resolve against the checkout whose branch holds the code."""
3560
+ proofs resolve against the checkout whose branch holds the code; unnamed, a
3561
+ bare repository's worktree holding the cwd (`tp.work_tree`, plan 098)."""
3562
+ code = code or tp.work_tree(repo)
3560
3563
  base = line_dir(repo, slug)
3561
3564
  errors, warnings = [], []
3562
3565
  if not os.path.isdir(base):
@@ -3723,12 +3726,13 @@ def done_seal(repo, slug):
3723
3726
  return _check_seal(base)[0], True
3724
3727
 
3725
3728
 
3726
- def failing(repo, git=False):
3729
+ def failing(repo, git=False, code=None):
3727
3730
  """(slug, error) pairs, for a one-line session note. Structural only by
3728
3731
  default: the session context must not pay for git history. A line under
3729
3732
  `done/` is read by its seal hashes alone (`check` with `all_lines=False`), and
3730
- an unsealed one adds nothing (plan 058 part 4)."""
3731
- return [(slug, err) for slug, row in check(repo, git=git, all_lines=False).items()
3733
+ an unsealed one adds nothing (plan 058 part 4). `code` as for `check_line`."""
3734
+ return [(slug, err) for slug, row in check(repo, git=git, all_lines=False,
3735
+ code=code).items()
3732
3736
  for err in row["errors"]]
3733
3737
 
3734
3738
 
@@ -4337,6 +4341,7 @@ def check_orx(repo, orx, strict=False):
4337
4341
  return {"errors": [], "warnings":
4338
4342
  ["orx is not installed, so the run command registered against this "
4339
4343
  "repository was not read (TEZGAH_ORX_BIN overrides the lookup)"]}
4344
+ repo = tp.work_tree(repo)
4340
4345
  out, err = _run([orx, "projects", "--json"], cwd=repo)
4341
4346
  if err:
4342
4347
  return {"errors": [], "warnings": ["orx projects could not be read (%s)" % err]}
@@ -4526,7 +4531,7 @@ def _migrate_claims(base, repo, dry_run, problems):
4526
4531
  if claim.get("kind") in KINDS:
4527
4532
  out.append(raw)
4528
4533
  continue
4529
- kind = derive_kind(claim, (base, repo))
4534
+ kind = derive_kind(claim, (base, tp.work_tree(repo)))
4530
4535
  if kind is None:
4531
4536
  problems.append("claim %s: no kind derived - its proof names no "
4532
4537
  "artifact to be a proof of" % label)
@@ -5498,7 +5503,8 @@ def arena_record(repo, problem, budget, evaluator="", metric=""):
5498
5503
  arena = {"problem": problem, "budget": budget}
5499
5504
  if evaluator:
5500
5505
  arena.update({"evaluator": evaluator,
5501
- "evaluator_sha256": _digest(os.path.join(repo, evaluator)),
5506
+ "evaluator_sha256": _digest(os.path.join(tp.work_tree(repo),
5507
+ evaluator)),
5502
5508
  "metric": metric})
5503
5509
  return arena
5504
5510
 
@@ -5544,7 +5550,7 @@ def evaluator_problem(repo, state):
5544
5550
  if not _text(path):
5545
5551
  return None
5546
5552
  try:
5547
- now = _digest(os.path.join(repo, path))
5553
+ now = _digest(os.path.join(tp.work_tree(repo), path))
5548
5554
  except OSError:
5549
5555
  now = None
5550
5556
  locked = str(arena.get("evaluator_sha256") or "")
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env python3
2
- """The Stop rule's evidence fold as a declared past-time temporal spec (plan 063,
3
- roadmap R16), run in shadow beside the imperative `_evidence_block`.
2
+ """The Stop rule's evidence fold, declared as a past-time temporal spec (plan 063,
3
+ roadmap R16; the fold since plan 101 replaced the imperative one it ran in
4
+ shadow beside).
4
5
 
5
6
  **What is declared.** `ROW_ATOMS` are the ten row-local predicates the spec reads,
6
7
  each computed once per row by the readers `tezgah_integrity` already has.
@@ -8,28 +9,26 @@ each computed once per row by the readers `tezgah_integrity` already has.
8
9
  of the trace: `("S", a, b)` is "a since b" (b held at some row and a at every row
9
10
  after it), `("O", a)` is "once a", plus `not`/`and`/`or`. `ORDER` is the
10
11
  decision list: the first class whose condition holds is the verdict, in the
11
- order the imperative fold returns them.
12
+ order the Stop rule refuses them (docs/evidence.md, classes 5-10).
12
13
 
13
14
  **What is not a formula, stated so the bounds cannot be gamed.**
14
- - The scope selector is `tezgah_integrity._stop_block`, shared with the
15
- imperative fold: which rows the fold reads (this turn, the session before this
16
- turn, the settled-bookkeeping and idle-turn claim fallbacks), the lost-began
17
- path, and the "evidence tampered" guard (a `ledger_damage` row says the trace
18
- is not a trace; it is checked before any fold). The spec replaces only the
19
- fold the selector calls.
15
+ - The scope selector is `tezgah_integrity._stop_block`, which calls `fold`:
16
+ which rows the fold reads (this turn, the session before this turn, the
17
+ settled-bookkeeping and idle-turn claim fallbacks), the lost-began path, and
18
+ the "evidence tampered" guard (a `ledger_damage` row says the trace is not a
19
+ trace; it is checked before any fold).
20
20
  - `SCOPE_ATOMS`: the turn marker `T`, which scopes "partial failure" to the
21
21
  newest turn of the rows given (`_partial_state`).
22
22
  - The reply atoms - the external-state claim and the selector's work flag - are
23
23
  constants over one trace, passed in, never read off a row.
24
- - `ESCAPES` lists, before any shadow row exists, the predicates that are not
25
- row-local and count against the "<= 3" bar; `NOT_COUNTED` lists the rest of
26
- the candidates plan 063 named and why each does not count.
27
-
28
- **The monitor** (`fold`) is one in-process pass over the rows at Stop: atoms per
29
- row, then each formula's value from its value at the previous row. Nothing is
30
- persisted between Stops. `shadow` records its verdict beside the imperative one
31
- as a `stop_spec` row and never feeds the verdict; `TEZGAH_STOPSPEC_STRICT` turns a
32
- disagreement into an exception, for the test suite.
24
+ - `ESCAPES` lists, fixed before the first shadow row, the predicates that are
25
+ not row-local and count against the "<= 3" bar; `NOT_COUNTED` lists the rest
26
+ of the candidates plan 063 named and why each does not count.
27
+
28
+ **The monitor** (`verdict`) is one in-process pass over the rows at Stop: atoms
29
+ per row, then each formula's value from its value at the previous row. Nothing
30
+ is persisted between Stops. `fold` hands the class to
31
+ `tezgah_integrity._evidence_text`, which words the refusal.
33
32
  """
34
33
  import os
35
34
  import sys
@@ -39,8 +38,6 @@ if HERE not in sys.path:
39
38
  sys.path.insert(0, HERE)
40
39
  import tezgah_integrity as ti # noqa: E402
41
40
 
42
- STRICT = "TEZGAH_STOPSPEC_STRICT"
43
- ROW_KIND = "stop_spec"
44
41
 
45
42
  ROW_ATOMS = (
46
43
  "Vany", # a verify-family row: verify, verify_ok, verify_fail (`_last_verify`)
@@ -107,11 +104,7 @@ NOT_COUNTED = (
107
104
  # given (`tezgah_integrity._admitted`: a check-repair class cleared after a check
108
105
  # that failed since the newest change), and `residual work` is a selector verdict
109
106
  # asked at every exit that would end the turn (`_residual_block`). Both reach the
110
- # spec's verdict through the shared selector, so neither is a formula here.
111
-
112
- # GO 2's live floor: shadow rows needed before the 99% bar is read. At 400 a
113
- # 99% rate is 4 disagreements, so one more or less moves it by 0.25 pp.
114
- MIN_LIVE_N = 400
107
+ # spec's verdict through the selector, so neither is a formula here.
115
108
 
116
109
 
117
110
  def _nodes(formula, out):
@@ -249,13 +242,22 @@ class Trace:
249
242
  return out
250
243
 
251
244
 
252
- def verdict(rows, worked, external, table=None):
245
+ def verdict(rows, worked, external):
253
246
  """The class `ORDER` picks, or None to allow; a formula is evaluated only
254
- when the list reaches it."""
255
- table, trace = table or _TABLE, Trace(rows, external)
247
+ when the list reaches it. `worked` is the selector's work flag, `external`
248
+ the reply's external-state claim or None.
249
+
250
+ The order is the classes' contract: the newest check failing decides before
251
+ anything else; a partial failure, then a UI write with no screen or design
252
+ proof newer than it, are stricter questions about the same rows than
253
+ freshness; a stale pass has another repair than none at all; and the
254
+ external read is asked last, only where the turn would otherwise end, so
255
+ it never changes the class another rule refused under. A screen proof
256
+ stands where a pass stands, and an external read only for its own claim."""
257
+ trace = Trace(rows, external)
256
258
 
257
259
  def holds(name):
258
- return table[name](trace)
260
+ return _TABLE[name](trace)
259
261
 
260
262
  for cls in ("check failed", "partial failure", "no ui_ok"):
261
263
  if holds(cls):
@@ -271,57 +273,8 @@ def verdict(rows, worked, external, table=None):
271
273
  return "no external read" if unread else None
272
274
 
273
275
 
274
- def fold(rows, worked, external, where="this turn", table=None):
275
- """`_evidence_block`'s signature and contract, the class from the spec. The
276
- text is the class itself: shadow reads only the class (part 10, the switch,
277
- would carry the imperative texts over)."""
278
- cls = verdict(rows, worked, external, table)
279
- return (cls, cls)
280
-
281
-
282
- def judge(text, session_id, rows, cwd, shape, table=None):
283
- """(the spec's verdict class, whether the selector reached the fold) for one
284
- Stop: the shared selector with the spec as its fold. A verdict the selector
285
- made alone (a shape class, "evidence tampered", a lost check) is the
286
- imperative one by construction."""
287
- reached = []
288
-
289
- def spec_fold(r, w, e, where="this turn"):
290
- reached.append(True)
291
- return fold(r, w, e, where, table)
292
-
293
- cls = ti._stop_block(text, session_id, rows=rows, cwd=cwd, shape=shape,
294
- fold=spec_fold)[0]
295
- return cls, bool(reached)
296
-
297
-
298
- def check(imperative, text, session_id, rows, cwd, shape):
299
- """(spec class, agree, fold reached) for one Stop; raises under
300
- `TEZGAH_STOPSPEC_STRICT` when the two disagree."""
301
- spec, reached = judge(text, session_id, rows, cwd, shape)
302
- agree = spec == imperative
303
- if not agree and os.environ.get(STRICT):
304
- raise AssertionError("stop spec disagrees: imperative %r, spec %r"
305
- % (imperative, spec))
306
- return spec, agree, reached
307
-
308
-
309
- def shadow(imperative, text, session_id, rows, cwd, shape, key):
310
- """Record the spec's verdict beside the imperative one as one `stop_spec`
311
- row per reply per turn: `agree fold: <class>`, `disagree selector: <imp> ->
312
- <spec>` and so on, where `fold` says the selector reached the fold and
313
- `selector` says it decided alone (GO 2 reads only the first). Never returns
314
- a verdict: the caller's is final. A failure of the spec is recorded as
315
- `error: <type>` and re-raised only under the strict switch."""
316
- if any(r.get("kind") == ROW_KIND and r.get("id") == key for r in rows):
317
- return
318
- try:
319
- spec, agree, reached = check(imperative, text, session_id, rows, cwd, shape)
320
- where = "fold" if reached else "selector"
321
- detail = ("agree %s: %s" % (where, imperative or "ok") if agree else
322
- "disagree %s: %s -> %s" % (where, imperative or "ok", spec or "ok"))
323
- except Exception as exc:
324
- if os.environ.get(STRICT):
325
- raise
326
- detail = "error: %s" % type(exc).__name__
327
- ti.note(session_id, ROW_KIND, detail, id=key)
276
+ def fold(rows, worked, external, where="this turn"):
277
+ """The evidence fold `tezgah_integrity._stop_block` calls: (class, block
278
+ text) or (None, None). The class is the spec's (`verdict`); the text is the
279
+ one `tezgah_integrity._evidence_text` words that class with."""
280
+ return ti._evidence_text(verdict(rows, worked, external), rows, external, where)
@@ -54,6 +54,13 @@ TASTE_ROWS = {
54
54
  "defects": ("id", "session", "day"),
55
55
  "labels": ("id", "provider", "day"),
56
56
  "injected": ("session", "day"),
57
+ # the verification-cost decision (plan 109): one row per measured run of one
58
+ # command at one sha, and one row per push of one sha. The tier map is read
59
+ # from these measured medians rather than declared, and the coalescing window
60
+ # is read from the pushes - so a second session in the same checkout reads the
61
+ # first one's measurements instead of re-deriving them.
62
+ "checks": ("repo", "sha", "command", "seconds", "exit", "ts"),
63
+ "pushes": ("repo", "sha", "branch", "at"),
57
64
  }
58
65
  IMPORT_SCHEMA = """
59
66
  CREATE TABLE IF NOT EXISTS imported (name TEXT NOT NULL, dev INTEGER NOT NULL,