@r1z4x/tezgah 1.0.0 → 1.1.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +71 -1
  2. package/MANIFEST +8 -0
  3. package/bin/tezgah-agents +10 -1
  4. package/bin/tezgah-docs +1 -1
  5. package/bin/tezgah-doctor +11 -0
  6. package/bin/tezgah-research +31 -14
  7. package/bin/tezgah-setup +154 -29
  8. package/bin/tezgah-skill +59 -0
  9. package/bin/tezgah-skill.py +59 -0
  10. package/bin/tezgah-status +62 -1
  11. package/bin/tezgah-triage +5 -3
  12. package/docs/README.md +1 -0
  13. package/docs/architecture.md +5 -2
  14. package/docs/citations-baseline.json +1 -1
  15. package/docs/clarity-baseline.json +9 -9
  16. package/docs/contract.md +68 -26
  17. package/docs/environment.md +6 -3
  18. package/docs/evidence.md +7 -0
  19. package/docs/gate.md +13 -7
  20. package/docs/glossary.md +1 -1
  21. package/docs/hosts.md +48 -1
  22. package/docs/index.json +29 -1
  23. package/docs/judge.md +61 -55
  24. package/docs/models.md +14 -8
  25. package/docs/operations.md +10 -5
  26. package/docs/orca.md +126 -0
  27. package/docs/research.md +38 -3
  28. package/docs/skills.md +74 -0
  29. package/docs/status-line.md +3 -1
  30. package/docs/testing.md +33 -21
  31. package/docs/vision.md +12 -8
  32. package/hooks/projects-pretooluse.py +17 -5
  33. package/hooks/tezgah_agents.py +135 -13
  34. package/hooks/tezgah_components.py +3 -0
  35. package/hooks/tezgah_context.py +65 -22
  36. package/hooks/tezgah_gate.py +133 -17
  37. package/hooks/tezgah_integrity.py +60 -13
  38. package/hooks/tezgah_judge.py +239 -56
  39. package/hooks/tezgah_models.py +41 -5
  40. package/hooks/tezgah_orca.py +144 -0
  41. package/hooks/tezgah_paths.py +40 -6
  42. package/hooks/tezgah_policy.py +109 -35
  43. package/hooks/tezgah_research.py +104 -8
  44. package/hooks/tezgah_skill_pick.py +459 -8
  45. package/hooks/tezgah_stack.py +382 -0
  46. package/hooks/tezgah_stopspec.py +2 -2
  47. package/hosts/codex/hook.py +7 -1
  48. package/hosts/cursor/hook.py +7 -4
  49. package/hosts/omp/hook.py +12 -6
  50. package/hosts/omp/tezgah-hook.ts.in +20 -7
  51. package/hosts/opencode/plugins/tezgah.js +18 -9
  52. package/output-styles/tezgah.md +34 -19
  53. package/package.json +1 -1
  54. package/skills/i-have-adhd/SKILL.md +2 -2
  55. package/skills/research/SKILL.md +60 -17
  56. package/skills/tezgah-contract/SKILL.md +55 -13
@@ -273,13 +273,25 @@ discovery, review, impact analysis (research goes to OpenResearch - see the
273
273
  Research section - with a host subagent only as the fallback when `orx` is
274
274
  absent). Subtasks with no data dependency
275
275
  between them MUST be spawned in ONE message so they run in parallel; dependent
276
- ones run sequentially, each briefed with the previous result. Fan out only when
276
+ ones run sequentially, each briefed with the previous result. Fanning out is the
277
+ default, not a favour the user has to ask for: two or more independent items -
278
+ a list, several files or areas, todo entries that share nothing - go to one
279
+ subagent each, and every writing slice gets its own branch and checkout (see
280
+ below), never the main checkout. The
281
+ orchestrator is the main thread itself: it does not wait idle on a slice while
282
+ independent work remains. Fan out only when
277
283
  the subtasks share no mutable file and no interface: if two of them would edit
278
284
  the same file, or one's answer decides the other's, keep them in one context or
279
285
  sequence them. The shared artifact is the coordination channel, not chatter -
280
286
  naming a lead coordinates nothing by itself. If the work
281
287
  cannot be split - one file, one bounded change, a strictly serial chain - do it
282
288
  directly. Never spawn a subagent whose briefing is bigger than the work.
289
+ A slice that needs its own checkout: inside Orca (the session-start `Orca:`
290
+ line) create it with `orca worktree create --name <slug> --parent-worktree
291
+ active --json` and track it with `orca worktree ps --json` or `tezgah-status
292
+ --orca` - a raw `git worktree add` gets no card, terminal tracking or agent
293
+ status in Orca; outside Orca, `git worktree add`. Either way the brief names
294
+ absolute paths under that checkout.
283
295
  Routing: `{ROUTE_BIN} "<brief>"` names the worker - tezgah-cheap, -standard or
284
296
  -frontier (overrides for stored data, credentials, security first; then a Jev
285
297
  judgement; the static phase table with no key). A worker that answers
@@ -400,12 +412,30 @@ experiments/variants, or producing a research artifact (report, figure, dataset)
400
412
  It is NOT "where is X defined" or "who calls Y" - that is code discovery and
401
413
  stays on the codegraph index.
402
414
 
415
+ **Lock the line to the question asked.** Restate the user's question verbatim as
416
+ the line's scope (`--ask`) and name what is in it and what is out (`--in`,
417
+ `--out`: state.json `scope`). Every finding answers an in-scope item - a claim's
418
+ `trace` names its `S<n>`, and `{RESEARCH_BIN} check` refuses one that names none -
419
+ and a finding that answers nothing asked is dropped, not reported as a bonus. Do
420
+ not widen the question, add sub-questions or chase an adjacent topic; a tangent
421
+ worth having is one line under Open questions for the user to pick. The question
422
+ picks the method, not the tooling: a qualitative, conceptual or reference question
423
+ is a source review (`--method qualitative`: no metric, no experiment), and a
424
+ metric, a locked evaluation and the experiment tree belong only to a question
425
+ whose answer is a measurement (`--method measured`). A plain factual or reference
426
+ question gets no line at all: answer it in the reply from primary sources. Depth
427
+ is the primary source read, not more breadth - the spec, the paper, the code, the
428
+ vendor's own documentation - each finding citing the source it rests on and
429
+ stating its confidence (high, medium or low, and why); a summary of summaries is
430
+ not an answer.
431
+
403
432
  When the task is research and the OpenResearch CLI (`{ORX_BIN}`) is installed,
404
433
  drive it through that CLI instead of ad-hoc local scripting. Load the operating
405
434
  manual first: the `orx` skill if the host has it, otherwise run
406
- `{ORX_BIN} skill` from the shell; then
407
- the named modules (`{ORX_BIN} skill experiment-tree`, `{ORX_BIN} skill lit-review`,
408
- `{ORX_BIN} skill evidence`, ...). Its cardinal rules are not style preferences - they
435
+ `{ORX_BIN} skill` from the shell; then the module the method needs -
436
+ `{ORX_BIN} skill lit-review` for sources, `{ORX_BIN} skill experiment-tree` and
437
+ `{ORX_BIN} skill evidence` for a measured line. On a measured line its cardinal
438
+ rules are not style preferences - they
409
439
  are what keeps results comparable, and breaking one silently invalidates the run:
410
440
  never edit a node once a run has answered it (branch a child instead); the run
411
441
  command and environment are a fixed contract identical on every node; vary the
@@ -420,7 +450,8 @@ afterwards - and `{RESEARCH_BIN} check` refuses a claim that claims a wider scop
420
450
  than the rows it rests on.
421
451
 
422
452
  If the CLI is not installed, say the research tooling is unavailable and do not
423
- improvise its protocol; fall back to a host subagent and say so. Kill switch:
453
+ improvise its protocol; fall back to a host subagent and say so, briefed with the
454
+ question verbatim, its in- and out-of-scope items and its method. Kill switch:
424
455
  `research-off`.
425
456
 
426
457
  Keep the line auditable in the repository, not in your head:
@@ -437,11 +468,13 @@ into a refusal. A report owes an independent review (`reviewer` is not the
437
468
  construct and conclusion validity threats, whatever `phase` says. `state.json`
438
469
  `deliverable` names what the line delivers and lists the ask's items, each
439
470
  answered in the report, a decision or a claim `trace`, or written `A<n> not
440
- delivered: <reason>`. A design, plan or analysis is produced as >= 3 variants in
471
+ delivered: <reason>`. A design or plan the user will choose between is produced
472
+ as >= 3 variants in
441
473
  `decisions/<id>/` - `criteria.json` committed before any cell, `variants.jsonl`
442
474
  (V0 the status quo), cells appended by `{RESEARCH_BIN} compare`, `decision.md`
443
475
  naming the chosen id, each rejected id with the criterion that ruled it out, and a
444
- `flip:` condition; a new version of a line is opened `init --supersedes <old>` and
476
+ `flip:` condition (an analysis answers its question once); a new version of a
477
+ line is opened `init --supersedes <old>` and
445
478
  carries the old one as a variant. Code or config variants are sibling orx nodes
446
479
  under one head node. Literature is found with `{ORX_BIN} discover` and read with
447
480
  `{ORX_BIN} paper`; an agent's summary is indexed as `agent-report` and never alone
@@ -602,7 +635,7 @@ prefers shell tools; that preference does NOT cover definitions, callers, or
602
635
  blast radius - those go to the graph. The graph answers from a parsed call
603
636
  graph, so it beats text search on renames, dynamic dispatch and cross-file
604
637
  callers. On Claude a PreToolUse hook denies the Explore subagent here and nudges
605
- the first identifier-shaped Grep per session toward the graph. The MCP tool's
638
+ the first code-symbol search per session toward the graph. The MCP tool's
606
639
  schema is deferred: on the FIRST code-discovery step of the session load it -
607
640
  ToolSearch("select:mcp__codegraph__codegraph_explore,mcp__plugin_tezgah_codegraph__codegraph_explore")
608
641
  (the plain name on a checkout's own server, the `plugin_tezgah_` one on a plugin
@@ -645,27 +678,40 @@ it - no apology theater, no self-justifying phrasing.
645
678
  **Ponytail (minimal code).** Laziest solution that works: YAGNI -> reuse an
646
679
  existing helper -> stdlib -> native platform feature -> installed dependency ->
647
680
  one line -> minimum code. No unrequested abstractions, no scaffolding "for
648
- later", shortest working diff. Trace the problem fully before climbing; never
649
- simplify away validation, error handling, security or anything requested. Bug
650
- fix = root cause where all callers route through. A deliberate corner cut gets
651
- a `ponytail:` comment naming the ceiling. On the first non-trivial coding task,
652
- read the full `ponytail` skill from the router (levels lite|full|ultra, set with
653
- `tezgah-pony`; the level rides this reminder when it is not `full`) - this
654
- paragraph is not the whole contract. Off: "stop ponytail".
681
+ later", deletion over addition, fewest files, shortest working diff. Trace the
682
+ problem fully before climbing; never simplify away validation, error handling,
683
+ security, accessibility or anything requested, and build the full version
684
+ without re-arguing when the user insists. Bug fix = root cause where all
685
+ callers route through. A deliberate corner cut gets a `ponytail:` comment
686
+ naming the ceiling. Non-trivial logic leaves ONE runnable check (an assert
687
+ self-check or one small test); a one-liner needs none. A complex ask ships the
688
+ lazy version and names the fuller one in the same reply instead of stalling.
689
+ Reply: code first, then at most three lines - what was skipped, when to add it.
690
+ In force from the first turn; levels lite|full|ultra are set with `tezgah-pony`
691
+ and ride the per-turn reminder when not `full`; read the full `ponytail` skill
692
+ from the router for the level table and examples. Off: "stop ponytail".
655
693
 
656
694
  **Output shape: ADHD-friendly.** The answer or the next action is on the first
657
- line, prose after it. Multi-step work is a numbered list, one bounded action per
658
- step, and while it is in flight its position is restated in one line - the todo
659
- list is that source, never re-narrate the plan. End with one concrete next step.
660
- Finish the issue in hand before raising a second one; an error states location,
661
- cause and fix with no drama; after a change say what now works. A list shows at
662
- most five items, ranked, the rest kept in reserve rather than dropped. An
695
+ line - a command, path, snippet or the decision itself, never context or the
696
+ question restated - prose after it. Multi-step work is a numbered list, one
697
+ bounded action per step, and while it is in flight its position is restated in
698
+ one line - the todo list is that source, never re-narrate the plan. End with one
699
+ concrete next step. Finish the issue in hand before raising a second one; an
700
+ error states location, cause and fix with no drama, naming a cause only when the
701
+ evidence identifies it (else what is known and what would confirm it); after a
702
+ change say what now works. A list shows at most five items, ranked, the rest
703
+ kept in reserve rather than dropped - presentation only, never the analysis. An
663
704
  estimate is in concrete units and marked as an estimate, never presented as a
664
705
  measurement. No preamble, no recap, no closer, and a question the reader raises
665
- mid-work is answered rather than deferred as the second issue. On the FIRST
666
- non-trivial answer of the session, read the full `i-have-adhd` skill from the
667
- router - this paragraph is not the whole contract. Off: `tezgah-adhd off`, or
668
- the repo's `.no-adhd`.
706
+ mid-work is answered rather than deferred as the second issue. Break the shape
707
+ only where it would delete the answer: an explanation runs as long as it needs,
708
+ with headers; an options question gets two to four ranked options,
709
+ recommendation first; after three "still broken" turns, name the suspect
710
+ assumption and ask one diagnostic question. Before sending, delete an opening
711
+ sentence that announces, a closing one that recaps or asks "anything else?", any
712
+ by-the-way sidebar and any hedge that carries no real uncertainty. In force from
713
+ the first answer; read the full `i-have-adhd` skill from the router for the
714
+ examples. Off: `tezgah-adhd off`, or the repo's `.no-adhd`.
669
715
 
670
716
  **Deliver the whole ask; never the shortcut.** The request defines the
671
717
  deliverable: every named item is in scope until the user says otherwise, and the
@@ -756,19 +802,30 @@ Off: `consult-off`.
756
802
 
757
803
  **Research: route it to OpenResearch.** When a task is research - a literature
758
804
  or reference review, forming and testing hypotheses, running or comparing
759
- experiments, producing a research artifact - drive it through the OpenResearch
760
- CLI (`{ORX_BIN}`) and load its manual first (`{ORX_BIN} skill`), following its
761
- experiment-tree rules instead of improvising the protocol. Plain code discovery
762
- stays on the code graph, not OpenResearch. At most one line is open at a time:
805
+ experiments, producing a research artifact - lock it to the question asked:
806
+ restate the user's question verbatim as the scope, name what is in and out of it
807
+ (`init --ask --in --out`), and drop every finding that does not answer an
808
+ in-scope item; never widen it or chase an adjacent topic. The question picks the
809
+ method: a qualitative or reference question is a source review (`--method
810
+ qualitative`, no metric, no experiment); metrics and the experiment tree only
811
+ when the answer is a measurement (`--method measured`). A plain factual or
812
+ reference question gets no line and no orx experiment: answer it in the reply.
813
+ Depth means primary sources read and cited, each finding with its confidence,
814
+ not a thin summary. Drive the work through the OpenResearch CLI (`{ORX_BIN}`)
815
+ and load its manual first (`{ORX_BIN} skill`; `lit-review` for sources,
816
+ `experiment-tree` for a measured line) instead of improvising the protocol.
817
+ Plain code discovery stays on the code graph, not OpenResearch.
818
+ At most one line is open at a time:
763
819
  while one is under way - not `concluded`, or carrying a live proposal, an unrun
764
820
  experiment, a missing report or review, an unlabelled review finding - no new line
765
821
  opens until it is closed or `{RESEARCH_BIN} close <slug> --limit "<reason>"` records
766
822
  what is left. `{RESEARCH_BIN} init` refuses on that rule and names each open line;
767
823
  `--allow-open "<reason>"` passes it (never beside a line `check` fails) and logs why.
768
- A plan, design or analysis is >= 3 variants compared under criteria committed
769
- first (`decisions/`); one version is not a finished line.{OPEN_LINES}
824
+ A design or plan the user will choose between is >= 3 variants compared under
825
+ criteria committed first (`decisions/`); an analysis answers its question once.{OPEN_LINES}
770
826
  If the CLI is not installed, say the research tooling is unavailable and fall
771
- back to a host subagent. Off: `research-off`.
827
+ back to a host subagent briefed with the question verbatim, its in/out scope and
828
+ its method. Off: `research-off`.
772
829
 
773
830
  **Product analysis: five axes, one evidence class per finding.** A product
774
831
  question - "analyse this product", "why is retention falling", "which feature
@@ -826,7 +883,9 @@ Never install, upgrade, restart or kill anything for tezgah, and never open an
826
883
  issue for one of its tools mid-session. Name a missing capability in one line,
827
884
  use the documented fallback (grep/find, or the second opinion skipped), and carry
828
885
  on with the task in hand. Tezgah maintenance is in scope when the user asks for
829
- it, or when the repo IS the tezgah checkout.
886
+ it, or when the repo IS the tezgah checkout. Before working a topic an
887
+ installed skill covers, run `tezgah-skill <words>` and read only the range it
888
+ returns.
830
889
 
831
890
  **Workspace: `.tezgah/` only.** Every per-project tezgah artifact - plans,
832
891
  research lines, analysis, lessons - lives under `<repo>/.tezgah/`, which the
@@ -835,6 +894,21 @@ project's `.gitignore` excludes and which keeps its own private git repository
835
894
  project root and never `git add -f` anything under `.tezgah/`. Off:
836
895
  `workspace-off`.
837
896
 
897
+ **Parallel by default: fan out independent items.** A request with two or
898
+ more independent items - a numbered or bulleted list, several files or areas, a
899
+ todo list whose entries share no file and no interface - is delegated without
900
+ being asked: one subagent per item, all spawned in ONE message, while the main
901
+ thread decomposes, integrates, verifies and reports. Every writing slice gets
902
+ its own branch and checkout - inside Orca `orca worktree create --name <slug>
903
+ --parent-worktree active --json`, else `git worktree add -b <branch> <absolute
904
+ path>`; its brief names every read, edit and shell `cwd` as an absolute path
905
+ under that checkout, never the main checkout, whose `git status --short` must
906
+ not grow. Read-only slices need no worktree. Pick each agent by task class from
907
+ the specialists line; `{ROUTE_BIN}` names the tier for code work. Serialize
908
+ only a real dependency - one slice's output is another's input, or two slices
909
+ edit one file - and say which. Never wait idle on a spawned slice while
910
+ independent work remains. Off: `orchestrate-off`.
911
+
838
912
  **Kill switches:** each one removes its own rule from this text, not just the
839
913
  status mark. `~/.config/tezgah/`: `exec-mode.off`, `orchestrate-off`,
840
914
  `consult-off`, `research-off`, `ponytail-auto.off`, `adhd-off`, `spec-off`,
@@ -852,13 +926,13 @@ The ponytail intensity level is not a switch: `tezgah-pony lite|full|ultra`.
852
926
  # time (hooks/tezgah_context.classify_prompt), because a session that never asks
853
927
  # a structural or research question should not carry their text. Keys match the
854
928
  # CORE_RULES labels in hooks/tezgah_context.py.
855
- CONDITIONAL_KEYS = ("spec", "consult", "research", "product", "graph")
929
+ CONDITIONAL_KEYS = ("spec", "consult", "research", "product", "graph", "fanout")
856
930
 
857
931
  # The always-on replacement for the conditional paragraphs: one line each so a
858
932
  # host without a per-turn hook still knows the rule exists and where the full
859
933
  # text lives.
860
934
  POINTERS = """
861
- **On-demand rules (armed when the task class matches; full text in the `tezgah-contract` skill).** Spec-first for an underspecified or quality-only ask. A second opinion before a call that is hard to reverse or that one model would answer with unearned confidence. OpenResearch routing for research. Product analysis is a five-axis evidence task - value, usability (the running app, not the source), feasibility (a cited `path:line`), competition, and keep/fix/cut/bet triage - with one named evidence class per finding. The code graph for "who calls X" and "what breaks if Z changes". The design floor a UI turn is judged against is `.tezgah/design-contract.md`: `tezgah-design derive` writes it from the repository's own tokens, `tezgah-design check` measures a change against it, and the `design-contract` skill owns its shape.
935
+ **On-demand rules (armed when the task class matches; full text in the `tezgah-contract` skill).** Spec-first for an underspecified or quality-only ask. A second opinion before a call that is hard to reverse or that one model would answer with unearned confidence. OpenResearch routing for research. Product analysis is a five-axis evidence task - value, usability (the running app, not the source), feasibility (a cited `path:line`), competition, and keep/fix/cut/bet triage - with one named evidence class per finding. The code graph for "who calls X" and "what breaks if Z changes". Two or more independent items fan out to parallel subagents in one message, one git worktree per writing slice. The design floor a UI turn is judged against is `.tezgah/design-contract.md`: `tezgah-design derive` writes it from the repository's own tokens, `tezgah-design check` measures a change against it, and the `design-contract` skill owns its shape.
862
936
  """
863
937
 
864
938
  # The compact per-turn form, inside the <harness-reminder> envelope the hosts
@@ -136,12 +136,20 @@ SOURCE_CLASSES = ("formal", "grey", "agent-report")
136
136
  # those warns on it instead of refusing - its rows were written before the rule
137
137
  # existed. Each later set is gated on its own number, so a bump never demotes an
138
138
  # older set: 2 the standards rules, 3 the ask contract (keyed on the `ask` field),
139
- # 4 the protocol sections (`_check_protocol_sections`). A `rules` above `RULES` is
139
+ # 4 the protocol sections (`_check_protocol_sections`), 5 the scope lock
140
+ # (`scope_problems`, `trace_problem`, the method-gated evaluation and the analysis
141
+ # that owes no variants). A `rules` above `RULES` is
140
142
  # a line a later tezgah wrote, and every reader and writer here refuses it by name
141
143
  # (`newer_rules`) rather than judging it by rules it does not know.
142
144
  STANDARDS_RULES = 2
143
145
  PROTOCOL_RULES = 4
144
- RULES = PROTOCOL_RULES
146
+ SCOPE_RULES = 5
147
+ RULES = SCOPE_RULES
148
+ # How a rules-5 line answers its question. `qualitative` is a source or
149
+ # reference review and owes no metric; `measured` runs experiments and locks
150
+ # one. The question picks it, not the tooling: a qualitative ask answered with a
151
+ # metric table is the metric-first drift this field exists against.
152
+ METHODS = ("qualitative", "measured")
145
153
 
146
154
  # What a line delivers, and how many compared variants that takes. `finding` is a
147
155
  # line whose deliverable is its claims; the other four are an artifact a reader
@@ -925,6 +933,7 @@ def _check_state(base, errors, warnings, strict):
925
933
  errors.append("state.json direction %r is not one of %s"
926
934
  % (state.get("direction"), ", ".join(DIRECTIONS)))
927
935
  _check_evaluation(state, errors, warnings, strict)
936
+ _check_scope(base, state, errors, warnings, strict)
928
937
  _check_hypotheses(state, errors)
929
938
  _check_sessions(state, errors)
930
939
  _check_success(state, errors, warnings, strict)
@@ -1012,6 +1021,8 @@ def _check_evaluation(state, errors, warnings, strict):
1012
1021
  is written where a session reads it (`skills/research/SKILL.md`,
1013
1022
  `docs/research.md`); what it cannot catch is a tolerance its own author sets
1014
1023
  so wide that every candidate passes it."""
1024
+ if _qualitative(state):
1025
+ return
1015
1026
  phase = state.get("phase")
1016
1027
  locked = phase != "bootstrap"
1017
1028
  evaluation = state.get("evaluation")
@@ -1045,6 +1056,77 @@ def _check_evaluation(state, errors, warnings, strict):
1045
1056
  "move: fill it, or omit the two-gate pair" % (counter,))
1046
1057
 
1047
1058
 
1059
+ # --- scope lock (rules 5) ------------------------------------------------------
1060
+ # A line answers the question the user asked, by the method that question takes.
1061
+ # `scope.in` lists what the answer covers (S1, S2, ... in order) and `scope.out`
1062
+ # what it deliberately leaves alone; every claim's `trace` names an in-scope item,
1063
+ # so a finding about something nobody asked is refused instead of padding the
1064
+ # report. Owner's report, 2026-10-07: research turned every question into
1065
+ # metrics, grew past the ask, and wandered into unrelated topics.
1066
+
1067
+ def _qualitative(state):
1068
+ return _rules(state) >= SCOPE_RULES and state.get("method") == "qualitative"
1069
+
1070
+
1071
+ def _scope_items(state):
1072
+ scope = state.get("scope")
1073
+ items = scope.get("in") if isinstance(scope, dict) else None
1074
+ return [i for i in items if isinstance(i, str) and i.strip()] \
1075
+ if isinstance(items, list) else []
1076
+
1077
+
1078
+ def scope_problems(state):
1079
+ """What a rules-5 line is missing to be locked to its question; [] otherwise."""
1080
+ if _rules(state) < SCOPE_RULES:
1081
+ return []
1082
+ problems = []
1083
+ scope = state.get("scope") if isinstance(state.get("scope"), dict) else {}
1084
+ for side, what in (("in", "what the answer covers"),
1085
+ ("out", "what it deliberately leaves alone")):
1086
+ items = scope.get(side)
1087
+ if not isinstance(items, list) or not [
1088
+ i for i in items if isinstance(i, str) and i.strip()]:
1089
+ problems.append("state.json scope.%s names nothing: list %s, from the "
1090
+ "user's own question" % (side, what))
1091
+ if state.get("method") not in METHODS:
1092
+ problems.append("state.json method %r is not one of %s: the question picks "
1093
+ "it - a source or reference question is qualitative and "
1094
+ "owes no metric" % (state.get("method"), ", ".join(METHODS)))
1095
+ return problems
1096
+
1097
+
1098
+ def trace_problem(cid, claim, state):
1099
+ """The refusal for a rules-5 claim that answers no in-scope item, or None.
1100
+ The write path (`claim_problems`) and the checker (`_check_claims`) both
1101
+ call it, so they cannot disagree about what is on topic."""
1102
+ if _rules(state) < SCOPE_RULES:
1103
+ return None
1104
+ items = _scope_items(state)
1105
+ trace = claim.get("trace")
1106
+ for entry in trace if isinstance(trace, list) else [trace]:
1107
+ if not isinstance(entry, str):
1108
+ continue
1109
+ entry = entry.strip()
1110
+ if any(entry in ("S%d" % n, item.strip()) for n, item in enumerate(items, 1)):
1111
+ return None
1112
+ return ("claim %s traces to no in-scope item: its `trace` names S<n> or the "
1113
+ "text of a state.json scope.in entry - a finding that answers nothing "
1114
+ "the user asked is dropped, not reported" % cid)
1115
+
1116
+
1117
+ def _check_scope(base, state, errors, warnings, strict):
1118
+ for problem in scope_problems(state):
1119
+ _soft(errors, warnings, strict or state.get("phase") != "bootstrap", problem)
1120
+ if _qualitative(state):
1121
+ ran = [h for h in _experiment_names(base)
1122
+ if os.path.isfile(os.path.join(base, "experiments", h, "results.jsonl"))]
1123
+ if ran:
1124
+ errors.append("state.json method is qualitative and experiment(s) %s "
1125
+ "recorded results: a measured question is `method: "
1126
+ "measured` with its evaluation locked first"
1127
+ % ", ".join(ran))
1128
+
1129
+
1048
1130
  def _check_hypotheses(state, errors):
1049
1131
  """Every hypothesis states something.
1050
1132
 
@@ -1563,7 +1645,8 @@ def _check_claims(base, errors, warnings, roots=(), strict=False):
1563
1645
  superseded.update(_supersedes(give)[0])
1564
1646
  # the token-savings rule is a `PROTOCOL_RULES` rule, so an older line keeps
1565
1647
  # the rules it was opened under
1566
- sections = _rules(_state(base)) >= PROTOCOL_RULES
1648
+ line_state = _state(base)
1649
+ sections = _rules(line_state) >= PROTOCOL_RULES
1567
1650
  parsed = []
1568
1651
  for n, raw in enumerate(rows, 1):
1569
1652
  raw = raw.strip()
@@ -1599,6 +1682,9 @@ def _check_claims(base, errors, warnings, roots=(), strict=False):
1599
1682
  errors.append("claim %s status %r is not one of %s"
1600
1683
  % (cid, claim.get("status"), ", ".join(STATUSES)))
1601
1684
  _check_claim_scope(cid, claim, base, repo, errors, warnings, strict)
1685
+ problem = trace_problem(cid, claim, line_state)
1686
+ if problem:
1687
+ errors.append(problem)
1602
1688
  # A superseded claim is the historical row: the line has corrected it in
1603
1689
  # the row that supersedes it, and that row is the one whose numbers are
1604
1690
  # held to their proof here. Warning on the historical statement too would
@@ -1907,6 +1993,9 @@ def claim_problems(claim, base, repo, held=None):
1907
1993
  known = _claim_ids(base) if held is None else held
1908
1994
  problems.extend("claim %s supersedes %s, and no claim in this line carries "
1909
1995
  "that id" % (cid, ref) for ref in ids if ref not in known)
1996
+ problem = trace_problem(cid, claim, _state(base))
1997
+ if problem:
1998
+ problems.append(problem)
1910
1999
  # The scope rules the checker decides, so the write path cannot record a claim
1911
2000
  # `check` would refuse. The warn class (a claim over fixture rows that declares
1912
2001
  # nothing) is deliberately not repeated: a writer stricter than the checker
@@ -2848,7 +2937,7 @@ def _check_results_append_only(repo, base, state, errors, warnings, strict):
2848
2937
  # 2006), and serial versions fixate on the first option (Dow et al. 2010); the
2849
2938
  # layer used to hold exactly that, v3 -> v4 -> v5 of one plan never compared.
2850
2939
 
2851
- def deliverable_problems(deliverable):
2940
+ def deliverable_problems(deliverable, rules=RULES):
2852
2941
  """(problems, variants needed) for `state.json`'s deliverable declaration."""
2853
2942
  if not isinstance(deliverable, dict):
2854
2943
  return (["state.json deliverable is a %s, not the object naming what the line "
@@ -2862,7 +2951,11 @@ def deliverable_problems(deliverable):
2862
2951
  if ask is not None and (not isinstance(ask, list) or not all(
2863
2952
  isinstance(item, str) and item.strip() for item in ask)):
2864
2953
  problems.append("state.json deliverable ask is not a list of the ask's items")
2865
- default = {"code": 2}.get(kind, 3) if kind in VARIANT_KINDS else 0
2954
+ # rules 5: an analysis answers its question once; variants are for a design,
2955
+ # plan or code the user will choose between, not a way to triple the answer
2956
+ kinds = VARIANT_KINDS if rules < SCOPE_RULES else tuple(
2957
+ k for k in VARIANT_KINDS if k != "analysis")
2958
+ default = {"code": 2}.get(kind, 3) if kind in kinds else 0
2866
2959
  need = deliverable.get("min_variants", default)
2867
2960
  if not isinstance(need, int) or isinstance(need, bool) or need < 0:
2868
2961
  problems.append("state.json deliverable min_variants %r is not a count"
@@ -3250,7 +3343,7 @@ def _check_decisions(repo, slug, base, state, errors, warnings, strict, git):
3250
3343
  "line delivers a design, plan, analysis or code that is owed "
3251
3344
  "compared variants, or only findings")
3252
3345
  else:
3253
- problems, need = deliverable_problems(deliverable)
3346
+ problems, need = deliverable_problems(deliverable, _rules(state))
3254
3347
  errors.extend(problems)
3255
3348
  decisions = _decision_dirs(base)
3256
3349
  if need > 1 and not decisions:
@@ -3910,7 +4003,7 @@ def _variants_owed(base, state):
3910
4003
  deliverable = state.get("deliverable")
3911
4004
  if not isinstance(deliverable, dict):
3912
4005
  return None
3913
- _problems, need = deliverable_problems(deliverable)
4006
+ _problems, need = deliverable_problems(deliverable, _rules(state))
3914
4007
  if need < 2:
3915
4008
  return None
3916
4009
  for _name, ddir in _decision_dirs(base):
@@ -4688,7 +4781,8 @@ def verdict_problems(state, rows):
4688
4781
  return problems
4689
4782
 
4690
4783
 
4691
- def init(repo, slug, question="", created="", supersedes=None, ask="", tier=DEFAULT_TIER):
4784
+ def init(repo, slug, question="", created="", supersedes=None, ask="", tier=DEFAULT_TIER,
4785
+ method="", scope_in=(), scope_out=()):
4692
4786
  """Scaffold a research line. Returns the paths created (never overwrites).
4693
4787
 
4694
4788
  Refuses a slug `slugs()` cannot list - see `valid_slug` - so no caller can
@@ -4702,6 +4796,8 @@ def init(repo, slug, question="", created="", supersedes=None, ask="", tier=DEFA
4702
4796
  path = os.path.join(base, sub)
4703
4797
  os.makedirs(path, exist_ok=True)
4704
4798
  state = dict(STATE_TEMPLATE)
4799
+ state["method"] = method
4800
+ state["scope"] = {"in": list(scope_in), "out": list(scope_out)}
4705
4801
  state["question"] = question
4706
4802
  state["created"] = created
4707
4803
  # every new line is opened under the newest rule set (`RULES` in the