presentation-signals · git:20260627.a70aad5 · 2026-06-27 · sha256 be6e9ad8d35e131f

presentation-signals git:20260627.a70aad5A

Immutable. This exact content is served forever at /api/v1/blob/be6e9ad8d35e131f.

---
name: presentation-signals
description: "Surface presentation signals a reviewer notices first — duplicate/near-identical tables, too-few or LLM-looking figures, page-padding, jargon-stuffing, generic AI-flavored prose, defensive 'not-X-but-Y' hedging, and broken abstract/intro narrative arc. AUXILIARY ONLY and weak by design: a deterministic dup-table pass (tools/check_presentation.py) plus a fresh cross-model GROSS-cases-only semantic pass, every above-info finding span-anchored to the evidence ledger (claims.json). The adjudicator CAPS everything at minor (SURFACE_ONLY_SKILLS + SURFACE_PATTERNS) — these contribute at most SOFT_FLAGS, never a HARD verdict — default false_positive_risk:high, and this is NOT an AI-generation classifier. Emits presentation-signals.findings.json; NEVER computes the verdict. Triggers: \"presentation signals\", \"AI-flavor check\", \"surface check\", \"排版/AI味\"."
argument-hint: [paper-dir | claims.json]
allowed-tools: Bash(*), Read, Write, mcp__codex__codex
---

# Presentation Signals — the surface tells (auxiliary, never a verdict)

Run surface signal checks for: **$ARGUMENTS** (requires `claims.json` from
`/evidence-ledger`). Emit span-anchored `presentation-signals.findings.json`. This
skill computes **no verdict**.

> ⚠️ **This skill is deliberately weak by design.** A polished paper can be
> fraudulent and a rough paper can be honest, so surface signals must **never** drive
> a verdict. Everything here is emitted under skill `presentation-signals`, which the
> adjudicator **caps at `minor`** (`SURFACE_ONLY_SKILLS` + `SURFACE_PATTERNS` in
> `tools/adjudicate_findings.py`) — at most `SOFT_FLAGS`, **never** `HARD_FLAGS`. This
> is **not an AI-text classifier**; for authorship detection use a dedicated tool
> (Pangram / GPTZero / Binoculars). Our only job is to add *"combine with the
> substantive findings and look closer"* context. See
> `references/hack-pattern-taxonomy.md` §F.

> 🔒 **Do not wrap this skill in `/loop`, `/schedule`, or `CronCreate`.** It is
> verdict-bearing input — it proposes the surface findings the deterministic
> adjudicator turns into the report. Re-firing it on a wall-clock timer adds no
> signal: its output changes only when the **paper / ledger** changes, not with the
> clock. Schedule the *external wait that precedes it* — ledger built → check **once**.
> (Mirrors ARIS's external-cadence doctrine.)

## Why this exists

Real reviewers notice surface tells before they read a single number — and they say
so out loud: *"两张表一模一样"* (two tables are identical), *"图还是大模型生成的"*
(the figure is LLM-generated), *"就这还没写满9页"* (couldn't even fill 9 pages),
*"堆砌名词吗"* (just stuffing jargon?), *"本文不是什么什么,而是什么什么…论文应该直接表达
做了什么"* (stop hedging "this paper is not X but rather Y" — just say what you did),
*"摘要写的像实验分析,读不到引言"* (the abstract reads like an experiment log; the
introduction is unreadable). An autoresearch pipeline (or a rushed human) produces
exactly these artifacts: a table copy-pasted and never updated, an oversized float to
pad the page limit, a decorative generated illustration in place of a real results
plot, paragraphs of generic LLM boilerplate, draft text so densely over-hedged that
every sentence defends against an objection, and an abstract that dumps experiment
notes instead of telling a background → contribution → evidence story.

These signals are **real** in the sense that reviewers react to them — but they are
**weak evidence of misconduct**. A concise honest paper has few floats; a careful
honest author uses LLM assistance for prose; a legitimate teaser figure can look
"generated". So this skill's contract is narrow and permanent:

- it emits only the nine §F surface patterns, all **capped at `minor`**;
- it defaults every *semantic* finding to `false_positive_risk: high` (the deterministic
  `HP-DUP-TABLE` / `HP-PIPELINE-ARTIFACT` checks set their own — the latter is low-FP);
- it **never** says a paper is "AI-generated" or implies fabrication;
- **silence is the common, correct output** — most papers should produce few or zero
  surface findings.

It exists to add *context* to the substantive auditors (`consistency-audit`,
`experiment-forensics`, `baseline-comparison-audit`, `citation-forensics`), not to
stand alone. If a surface tell sits next to a real numeric contradiction, the
substantive finding carries the weight; the surface note just says "look closer."

## Core principle

**Ledger-anchored, span-verified, capped-at-minor, reviewer ≠ adjudicator, NOT a
detector.** Two passes feed the pipeline:

1. a **deterministic** dup-table pass (no model) — objective, reproducible, the only
   surface signal computable without judgment (`HP-DUP-TABLE`);
2. a **fresh cross-model GROSS-cases-only** semantic pass — the seven judgment-call
   signals (`HP-THIN-FLOAT`, `HP-LLM-FIGURE`, `HP-PAGE-PADDING`, `HP-JARGON-STUFF`,
   `HP-AI-FLAVOR`, `HP-DEFENSIVE-HEDGE`, `HP-NARRATIVE-ARC-BREAK`), each span-anchored
   and tagged `false_positive_risk: high`.

Both emit findings conforming to `schemas/finding.schema.json`. **Every above-info
finding cites a ledger `claim_id` + a verbatim span** (`references/integrity-forensics-contract.md`
rules 1–2). Because the ledger holds only *checkable* claims (numbers, scope,
captions, citations, table cells) and almost no free prose, a surface signal that
cannot land on an extracted claim **stays `info`** — a note, never a flag. That is the
design working: AI-flavor and jargon prose structurally almost never become even a
`minor` flag. The model **proposes**; `tools/adjudicate_findings.py` **decides**
(`references/reviewer-independence.md` Layer 2). This skill computes **no verdict**.

## How this differs from the other auditors (route correctly)

| Auditor | Question it answers | Level |
|---------|---------------------|------|
| **`presentation-signals`** (this) | **Surface tells a reviewer notices first (dup tables, thin/LLM figures, padding, jargon, AI-flavor, defensive hedging, broken narrative arc) — AUXILIARY, capped at `minor`** | **L0** |
| `consistency-audit` | Does the paper contradict ITSELF / described method = evaluated method? | L0 |
| `experiment-forensics` | Are the reported numbers what the code actually computes? (fake GT, self-norm, phantom) | L2 |
| `baseline-comparison-audit` | Are the right baselines present, tuned, and is "SOTA" earned? | L0 stated / L2 verified |
| `citation-forensics` | Do the cited papers exist and support the claim they are used for? | L0 |
| `adversarial-case-builder` | Strongest evidence-bound rejection memo (no verdict weight) | any |

**Stay in lane.** This skill emits **only** the nine surface (`HP-DUP-TABLE`,
`HP-THIN-FLOAT`, `HP-LLM-FIGURE`, `HP-PAGE-PADDING`, `HP-JARGON-STUFF`,
`HP-AI-FLAVOR`, `HP-DEFENSIVE-HEDGE`, `HP-NARRATIVE-ARC-BREAK`, `HP-PIPELINE-ARTIFACT`)
patterns — nothing else. **Do NOT raise here** (hand off instead): if
two "duplicate" tables actually report *contradictory* numbers for the same setting →
that is a numeric self-contradiction for `consistency-audit`, not `HP-DUP-TABLE`; if a
figure *misrepresents* a result vs its caption → `consistency-audit`
(`HP-CAPTION-MISMATCH`); if a thin float count sits under a "SOTA / comprehensive"
*empirical* claim → the substantive scope question goes to
`baseline-comparison-audit` / `consistency-audit` (you may *also* emit `HP-THIN-FLOAT`
as a capped surface note); a fabricated reference → `citation-forensics`. A surface
finding is the weakest thing in the report by construction — never use it to carry a
substantive accusation.

## Constants & Reviewer Calling Convention

```
REVIEWER_MODEL          = gpt-5.5                  # different family from executor (Claude)
REVIEWER_REASONING      = xhigh                    # always; effort never lowers reviewer quality
REVIEWER_SANDBOX        = read-only                # detect-only; never mutate the paper
REVIEWER_CWD            = <paper-dir>              # so it can read claims.json + pdf-text + the PDF directly
THREAD_POLICY           = fresh mcp__codex__codex per run; NEVER mcp__codex__codex-reply
TAXONOMY_VERSION        = 0.4                      # references/hack-pattern-taxonomy.md §F
DETERMINISTIC_PATTERNS  = HP-DUP-TABLE, HP-PIPELINE-ARTIFACT             # Step 1 (tool)
SEMANTIC_PATTERNS       = HP-THIN-FLOAT, HP-LLM-FIGURE, HP-PAGE-PADDING, # Step 2 (reviewer)
                          HP-JARGON-STUFF, HP-AI-FLAVOR,
                          HP-DEFENSIVE-HEDGE, HP-NARRATIVE-ARC-BREAK
SEVERITY_CAP            = minor    # SURFACE_ONLY_SKILLS + SURFACE_PATTERNS in adjudicate_findings.py
DEFAULT_FP_RISK         = high     # every surface finding; this is not optional
OBS_REQUIRED            = 0        # every F-pattern is decidable at L0 (PDF-only)
DETERMINISTIC_FINDINGS  = presentation-signals.deterministic.findings.json   # Step 1, ids PRES###
SEMANTIC_FINDINGS       = presentation-signals.findings.json                 # Step 3, ids F### (validated)
TRACE_POLICY            = forensic (never silently dropped)
TRACE_DIR               = .aris/traces/presentation-signals/<YYYY-MM-DD>_run<NN>/
```

- **Executor (Claude)** builds none of the judgment: it locates the ledger + the PDF,
  passes **paths + the ledger + the checklist** to the reviewer, validates the
  reviewer's spans, caps severity, and writes the findings file. It never summarizes
  the paper, pre-judges "this looks AI-written", or leaks an opinion into the prompt
  (`reviewer-independence.md` Layer 1).
- **Reviewer (codex / gpt-5.5)** reads `claims.json` + the PDF-text + the PDF itself
  (visually only if it can render it; otherwise caption text only — see HP-LLM-FIGURE),
  proposes **gross-only** surface signals, and self-reports `false_positive_risk`. It
  is the evidence-extractor, not the judge.
- **Fresh thread per run.** `codex-reply` is intentionally absent from `allowed-tools`;
  never carry one run's conclusions into another (the bias guard).
- **Detect-only.** No `Edit` in `allowed-tools`; the reviewer sandbox is `read-only`.
  `Write` is used **only** for this skill's own findings / trace artifacts, never the
  audited paper. This is a third-party forensics tool, never a co-author.

---

## Step 0 — Preconditions: locate the ledger, read the level, find the PDF

The ledger is the **only** structure this skill reasons over for anchoring. Resolve
it, read the run's observability level **L** and `paper_id`, count the float-bearing
claims, and locate the PDF + text source the reviewer will read (each Bash block is
self-contained — shell state does not persist between calls, so re-derive paths):

```bash
ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
# $ARGUMENTS is a paper-dir OR a claims.json path:
LEDGER="$ARGUMENTS"; [ -d "$LEDGER" ] && LEDGER="$LEDGER/claims.json"
# Only the NO-ARGUMENT case defaults to the CWD ledger. An EXPLICIT argument that
# resolves to a missing claims.json must NOT silently fall back to $(pwd) — that
# could audit the wrong paper; let the NO_LEDGER check below fire instead.
[ -z "$ARGUMENTS" ] && LEDGER="$(pwd)/claims.json"
python3 - "$LEDGER" <<'PY'
import json, sys, os
p = sys.argv[1]
if not os.path.isfile(p):
    sys.exit("NO_LEDGER: claims.json not found. Run /evidence-ledger FIRST "
             "(it writes artifact_manifest.json + claims.json).")
d = json.load(open(p, encoding="utf-8"))
cl = d.get("claims", [])
caps = [c for c in cl if c.get("type") == "caption"]
tabs = sorted({(c.get("location") or {}).get("section","") for c in cl
               if c.get("type") == "table_cell"
               and str((c.get("location") or {}).get("section","")).startswith("table:")})
scope = [c for c in cl if c.get("type") == "scope"]
print("LEDGER       =", os.path.abspath(p))
print("PAPER_DIR    =", os.path.dirname(os.path.abspath(p)) or ".")
print("PAPER_ID     =", d.get("paper_id", "?"))
print("RUN_LEVEL_L  =", d.get("observability_level", 0))
print("CLAIMS       =", len(cl))
print("CAPTION_CL   =", len(caps), "  (anchors for HP-LLM-FIGURE)")
print("TABLE_SECS   =", len(tabs), tabs, "  (float count for HP-THIN-FLOAT / HP-DUP-TABLE)")
print("SCOPE_CL     =", len(scope), "  (anchors for HP-THIN-FLOAT / HP-PAGE-PADDING)")
paper_dir = os.path.dirname(os.path.abspath(p)) or "."
srcs = d.get("source_files", [])
for sf in srcs:
    print("SOURCE       =", sf.get("kind"), sf.get("path"))
# Deterministically pick the prose source + the PDF the reviewer will read, FROM the
# ledger's source_files (authoritative); fall back to a sorted glob. source_files paths
# may be relative to PAPER_DIR or absolute. This is the ONLY selection (no shell `ls`
# later), so the PDF-only/L0 path with no pdf source resolves to NONE.
import glob
def _resolve(rel):
    cand = rel if os.path.isabs(rel or "") else os.path.join(paper_dir, rel or "")
    return os.path.abspath(cand) if os.path.isfile(cand) else ""
def _pick(kinds, globs):
    for sf in srcs:
        if sf.get("kind") in kinds:
            r = _resolve(sf.get("path"))
            if r:
                return r
    for g in globs:
        hits = sorted(glob.glob(os.path.join(paper_dir, g)))
        if hits:
            return os.path.abspath(hits[0])
    return ""
print("PDF_TEXT_FILE=", _pick({"text", "latex"}, ["*.txt", "*.tex"])
      or "NONE (prose signals limited to ledger spans)")
print("PDF_FILE     =", _pick({"pdf"}, ["*.pdf"])
      or "NONE (HP-LLM-FIGURE limited to caption text)")
PY
```

**Failure / edge handling.**
- `NO_LEDGER` → stop; tell the user to run `/evidence-ledger` first. This skill never
  re-reads the raw PDF and invents its own structure (contract rule 1).
- `TABLE_SECS = 0` → `Step 1` (dup-table) will correctly emit `[]`: with no parsed
  `table_cell` claims there are no tables to compare (common on a pure PDF-text run).
  Keep the empty file; continue to Step 2.
- `CAPTION_CL = 0` and/or `PDF_FILE = NONE` → `HP-LLM-FIGURE` has no caption anchor and
  no image to inspect; the reviewer will almost certainly hold it at `info`. That is
  honest, not a failure.
- `CLAIMS = 0` (degenerate ledger) → every semantic surface signal will be unanchored
  → `info`. Run anyway; the file must exist.

Step 0 **prints** `RUN_LEVEL_L`, `PAPER_ID`, the absolute `LEDGER` / `PAPER_DIR`,
`PDF_FILE`, and `PDF_TEXT_FILE`. Shell variables do **not** persist across Bash calls,
so paste these **literal absolute values** into the `<...>` placeholders of each later
step — do not assume an exported `$LEDGER` survives between blocks.

## Step 1 — Deterministic surface check (no LLM)

The one objective, eval-testable surface signal: **duplicate tables** — two tables whose
ordered numeric cells are identical after rounding to 4 decimals (`HP-DUP-TABLE`),
computed purely from the ledger's `table_cell` values. Runs before any model:

```bash
ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
LEDGER="<abs path to claims.json from Step 0>"
python3 "$ROOT/tools/check_presentation.py" \
    --ledger "$LEDGER" \
    --out "$(dirname "$LEDGER")/presentation-signals.deterministic.findings.json"
```

This emits, per duplicate pair, a finding with `pattern_id: HP-DUP-TABLE`,
`severity: minor`, `false_positive_risk: high`, `observability_level_required: 0`,
`reviewer.deterministic: true`, `finding_id: PRES###`, and two `evidence` entries —
one per table section, each anchored to a **representative** `table_cell` claim of that
section (its verbatim `text_span`); the full matching ordered values appear in the
finding's `description`, not as per-cell evidence. Two tables qualify only if they share
an identical *ordered* sequence of ≥ `MIN_CELLS` (2) numeric cells (each rounded to 4
decimals before comparison).

**Failure handling.** If the tool errors, fix the invocation
(`python3 "$ROOT/tools/check_presentation.py" --help`) — do **not** hand-fabricate
deterministic findings. An empty output (`[]`) is a valid, expected result (no
identical tables, or no `table_cell` claims were extracted to compare); keep the file.

## Step 2 — Cross-model GROSS-cases-only semantic pass (reviewer ≠ adjudicator)

The other seven §F signals are judgment calls. Open a **fresh** `mcp__codex__codex`
thread (the Reviewer Calling Convention above), `cwd = PAPER_DIR` so it can read
`claims.json`, the PDF-text, and the PDF directly. First create the forensic trace dir
and fix the exact response path — shell state does not persist, so this prints the
literal paths to reuse:

```bash
LEDGER="<abs path to claims.json from Step 0>"; PAPER_DIR="$(dirname "$LEDGER")"
TS="$(date +%F)"; BASE="$PAPER_DIR/.aris/traces/presentation-signals"
NN=1; while [ -d "$(printf '%s/%s_run%02d' "$BASE" "$TS" "$NN")" ]; do NN=$((NN+1)); done
TRACE_DIR="$(printf '%s/%s_run%02d' "$BASE" "$TS" "$NN")"; mkdir -p "$TRACE_DIR"
echo "TRACE_DIR = $TRACE_DIR"
echo "PROPOSED  = $TRACE_DIR/001-surface-semantic.response.md   # save the raw reply here; reuse as PROPOSED in Step 3"
```

Replace the bracketed placeholders below with the real values from Step 0, send EXACTLY
this, and save the **verbatim** reviewer reply to the `PROPOSED` path above (the Step 3
input) **before** parsing:

```
mcp__codex__codex:
  model: gpt-5.5
  config: {"model_reasoning_effort": "xhigh"}
  sandbox: read-only
  cwd: <absolute PAPER_DIR from Step 0>
  prompt: |
    You are checking PRESENTATION signals only — the kind of surface tell a reviewer
    notices at a glance. You are explicitly NOT deciding whether the paper is
    AI-written, and NOT whether it is fraudulent. You are NOT an AI-text classifier.
    Your output is auxiliary "look closer" context that a deterministic adjudicator
    will CAP at severity "minor"; it can never raise a verdict on its own. Default to
    SILENCE: an empty array [] is the expected, correct output for most papers.

    INPUTS (in your working directory, read them directly):
      - claims.json        — the evidence ledger: the authoritative, span-anchored list
        of every checkable claim {claim_id, type, text_span (VERBATIM source text),
        location, value?}. This is the ONLY thing you may anchor a finding to.
      - <PDF_TEXT_FILE from Step 0>   — extracted PDF text (for prose / padding / jargon).
      - <PDF_FILE from Step 0, if any> — the rendered PDF (for figure inspection).
    RUN OBSERVABILITY LEVEL L = <L from Step 0>.

    HARD RULES (a finding that breaks any of these is worthless):
    1. GROSS ONLY. Flag only BLATANT cases. If you are unsure, do NOT flag. This is
       especially binding for HP-AI-FLAVOR and HP-JARGON-STUFF (the most FP-prone).
    2. ANCHOR. Every finding above severity "info" MUST carry >=1 evidence entry
       {claim_id, span}, where claim_id EXISTS in claims.json and span is a VERBATIM
       substring of THAT claim's text_span (no paraphrase, no added words). If you
       cannot quote a verbatim ledger span for a signal, emit it at severity "info"
       (a note) or drop it — it can NEVER be a flag. The ledger holds numbers, scope,
       captions, citations, and table cells; generic prose is usually NOT in it, so
       most AI-flavor / jargon observations will correctly remain "info".
    3. SEVERITY + FP. Surface flags are capped at "minor". For any ANCHORED finding
       (rule 2 satisfied) set severity = "minor"; an UNANCHORED signal stays "info"
       (rule 2) — never promote it to "minor". NEVER use "major"/"critical" (the
       adjudicator caps surface signals at minor regardless; do not argue past it). Set
       false_positive_risk = "high" and observability_level_required = 0 for EVERY
       finding (all L0-decidable).
    4. NO ACCUSATION, NO AUTHORSHIP VERDICT. description and recommended_reviewer_action
       say what a human should glance at / ask. NEVER write "AI-generated", "fabricated",
       "reject", or imply misconduct. A surface tell is a prompt to look, nothing more.
    5. STAY IN LANE. pattern_id MUST be exactly one of the seven below. If you notice a
       SUBSTANTIVE problem (numbers contradict, a citation looks fake, a baseline is
       missing), do NOT encode it here — that belongs to consistency-audit /
       citation-forensics / baseline-comparison-audit. Ignore it.

    CHECKLIST (the SEVEN semantic surface patterns; one finding per concrete, blatant case):
      HP-THIN-FLOAT   — a full-length paper claiming broad/comprehensive empirical
                        results while containing almost no figures/tables. Anchor to the
                        SCOPE claim (e.g. "comprehensive evaluation across diverse
                        benchmarks"); put the actual float count in the description.
                        FP (high): legitimately theoretical or short-format work.
      HP-LLM-FIGURE   — a "figure" that is a generated/decorative illustration rather
                        than a real plot/diagram of results. Anchor to the figure's
                        CAPTION claim. If you cannot VISUALLY inspect the PDF, judge only
                        from the caption text (e.g. it literally describes a generated
                        illustration) and otherwise leave it at "info" /
                        needs_external_check — do NOT guess from a filename.
                        FP (high): legitimate conceptual/teaser figures; good diagrams.
      HP-PAGE-PADDING — oversized floats, repeated content, or vacuous filler used to
                        reach (or conspicuously miss) the page limit. Anchor to a ledger
                        claim that the padding rests on (a caption / scope / table cell).
                        FP (high): legitimately concise work; venue length norms.
      HP-JARGON-STUFF — dense term-stuffing where the surrounding argument carries no
                        actual content. Anchor to the offending span IF it is a ledger
                        claim; else "info". FP (very high): genuinely dense but correct
                        technical writing.
      HP-AI-FLAVOR    — unedited generic LLM prose (boilerplate transitions, hedged
                        filler, uniform paragraph shapes). GROSS cases only. Anchor only
                        if the prose is a ledger claim's text_span; otherwise "info".
                        FP (HUGE — the single most FP-prone pattern): honest LLM-assisted
                        writing, non-native English, house style. If in doubt, do NOT
                        flag. Never treat as evidence of fabrication or authorship.
      HP-DEFENSIVE-HEDGE — pervasive "this paper is not X, but rather Y" / "we do not
                        claim …; instead …" framing that DEFENDS against objections
                        rather than directly stating what was built and shown. The
                        checkable signal is the DENSITY of this defensive-contrast shape,
                        NOT who wrote it — never infer AI authorship/editing. GROSS
                        cases only: the pattern is PERVASIVE hedging, so anchor >=2
                        representative hedge spans that are themselves ledger claims;
                        ONE deliberate scoping sentence is NOT the pattern — leave it
                        "info". FP (high): a single scoping/positioning sentence; a
                        genuine related-work "unlike X, we…" contrast. Never an
                        authorship verdict.
      HP-NARRATIVE-ARC-BREAK — the abstract reads like an experiment-log dump, or is
                        vague "general" language with no specifics, or stuffs so many
                        undefined new terms it cannot be read; no background ->
                        contribution -> evidence arc; an Introduction that does not go
                        problem -> why-hard -> approach -> validation. Anchor to the
                        abstract/intro span (a ledger claim) and name WHICH arc element
                        is missing; an unanchored "the intro reads thin" impression
                        stays "info". FP (high): a legitimately terse abstract;
                        non-native phrasing; field conventions. Never an authorship
                        verdict.

    OUTPUT: a single JSON array, and NOTHING ELSE (no prose, no code fence). Each
    element conforms to schemas/finding.schema.json:
      {
        "finding_id": "F001",
        "skill": "presentation-signals",
        "pattern_id": "HP-THIN-FLOAT | HP-LLM-FIGURE | HP-PAGE-PADDING | HP-JARGON-STUFF | HP-AI-FLAVOR | HP-DEFENSIVE-HEDGE | HP-NARRATIVE-ARC-BREAK",
        "title": "short, neutral",
        "description": "the surface observation, plus the explicit note that it is a weak signal to look closer (not evidence of AI-authorship or fraud)",
        "severity": "minor",
        "observability_level_required": 0,
        "evidence": [{"claim_id": "C0xx", "span": "verbatim substring of that claim",
                      "location": {"file": "...", "section": "..."}}],
        "verdict_local": "warn",
        "false_positive_risk": "high",
        "recommended_reviewer_action": "what to GLANCE AT or ASK — never 'reject', never 'AI-generated'"
      }
    If nothing is blatant, return []. That is the expected output for most papers.
```

**Failure handling.**
- *MCP stall / hang* (common in long sessions): re-invoke the **identical** prompt as a
  **fresh** `mcp__codex__codex` call (gpt-5.5, xhigh) — never `codex-reply`.
- *Reviewer returns prose, not a JSON array*: the Step 3 validator extracts the
  outermost `[...]`; if there is none, re-ask once with "Output ONLY the JSON array,
  nothing else." Do not hand-author findings on the reviewer's behalf.
- *Reviewer over-flags AI-flavor*: that is what the Step 3 anchor gate + the
  adjudicator's surface cap are for — unanchored AI-flavor falls to `info` and even
  anchored surface findings cap at `minor`. Do not pre-suppress; let the gates work.

## Step 3 — Validate + anchor + cap (the anti-detector gate)

The executor enforces the ANCHOR gate **and** the surface cap **before** keeping
anything — exactly the rules `tools/adjudicate_findings.py` re-applies, so nothing you
keep is silently rejected downstream. The span must be a verbatim,
whitespace-normalized **substring of** the cited claim (`span in base`, never
`base in span` — appending hallucinated text to a real claim must fail):

```bash
ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
LEDGER="<abs path to claims.json>"
PROPOSED="<the PROPOSED path printed in Step 2>"   # the verbatim reviewer reply you saved
OUT="$(dirname "$LEDGER")/presentation-signals.findings.json"
python3 - "$LEDGER" "$PROPOSED" "$OUT" <<'PY'
import json, re, sys
ledger_path, proposed_path, out_path = sys.argv[1], sys.argv[2], sys.argv[3]

def nw(s):                                   # mirror adjudicator _norm_ws (whitespace only)
    return " ".join((s or "").split())

# the SEVEN semantic surface patterns this skill's Step 2 may emit. HP-DUP-TABLE is
# deterministic-only (Step 1) and is DROPPED here to avoid double-counting under the
# orchestrator's *.findings.json glob; any non-surface (substantive) pattern is also
# dropped — it belongs to another skill and must never be smuggled in capped-at-minor.
SEMANTIC_SURFACE = {"HP-THIN-FLOAT", "HP-LLM-FIGURE", "HP-PAGE-PADDING",
                    "HP-JARGON-STUFF", "HP-AI-FLAVOR",
                    "HP-DEFENSIVE-HEDGE", "HP-NARRATIVE-ARC-BREAK"}
SEV = {"critical", "major", "minor", "info"}
VL  = {"fail", "warn", "clean", "needs_external_check"}
ABOVE_INFO = {"critical", "major", "minor"}

ledger = json.load(open(ledger_path, encoding="utf-8"))
claims = {c["claim_id"]: c for c in ledger.get("claims", []) if c.get("claim_id")}

raw = open(proposed_path, encoding="utf-8").read()
m = re.search(r"\[.*\]", raw, re.S)          # tolerate prose / code-fence wrapping
proposed = json.loads(m.group(0) if m else raw)
if isinstance(proposed, dict):               # tolerate {"findings": [...]}
    proposed = proposed.get("findings", [])

kept, dropped, demoted, capped = [], 0, 0, 0
n = 0
for f in proposed:
    if not isinstance(f, dict):
        dropped += 1; continue
    pid = f.get("pattern_id")
    if pid not in SEMANTIC_SURFACE:          # drop dup-table (deterministic) + any non-surface pattern
        dropped += 1; continue
    n += 1
    f["finding_id"] = f"F{n:03d}"
    f["skill"] = "presentation-signals"      # force-correct the skill tag
    f["pattern_id"] = pid
    if f.get("verdict_local") not in VL: f["verdict_local"] = "warn"
    f["false_positive_risk"] = "high"        # surface signals are high-FP by design (not optional)
    f["observability_level_required"] = 0    # every F-pattern is L0-decidable
    sev = f.get("severity")
    if sev not in SEV: sev = "info"
    if sev in {"critical", "major"}:         # SURFACE cap: a surface signal is NEVER above minor
        sev = "minor"; capped += 1
    # ANCHOR gate: span must be a verbatim ws-normalized SUBSTRING of its cited claim
    anchored = []
    for ev in (f.get("evidence") or []):
        cid, span = ev.get("claim_id"), nw(ev.get("span", ""))
        c = claims.get(cid)
        if c and span and span in nw(c.get("text_span", "")):   # span IN claim, not claim IN span
            ev.setdefault("location", c.get("location", {}))     # enrich for human navigation
            ev.setdefault("artifact_hash", c.get("evidence_anchor", ""))
            anchored.append(ev)
    f["evidence"] = anchored
    if sev in ABOVE_INFO and not anchored:
        sev = "info"; demoted += 1           # unanchored surface signal -> info note, NEVER a flag
    f["severity"] = sev
    # cross-model provenance (reviewer-independence: this is a proposal, not a verdict)
    f["reviewer"] = {"model": "gpt-5.5", "reasoning": "xhigh", "deterministic": False}
    kept.append(f)

json.dump(kept, open(out_path, "w", encoding="utf-8"), indent=2, ensure_ascii=False)
print(f"validated {len(kept)} surface findings "
      f"({capped} capped to minor, {demoted} demoted to info for unanchored span, "
      f"{dropped} dropped: non-surface/dup-table/malformed) -> {out_path}")
PY
```

Scope of this gate: **anchoring + the surface cap + enum/field coercion** (it does
**not** run `schemas/finding.schema.json`) — verbatim-span anchoring, severity cap to
`minor`, FP-risk forced to `high`, observability fixed to `0`, surface-pattern
allow-list (drop everything else), enum coercion, and cross-model provenance. It does
**not** compute the verdict; that belongs to
`tools/adjudicate_findings.py`, the single decider, which **independently re-applies**
the same `SURFACE_ONLY_SKILLS` + `SURFACE_PATTERNS` cap (double-belt: by skill AND by
pattern_id, so a surface signal cannot bypass the cap even if mis-tagged).

**Worked finding — `HP-THIN-FLOAT` (anchored → survives as `minor`):**

```json
{
  "finding_id": "F001",
  "skill": "presentation-signals",
  "pattern_id": "HP-THIN-FLOAT",
  "title": "Broad empirical scope claimed with very few floats",
  "description": "The abstract claims a 'comprehensive empirical evaluation across diverse benchmarks', but the paper contains only 2 tables and 1 figure (ledger: 2 table sections, 1 caption claim). This is a surface signal only — a concise paper can be honest; it is NOT evidence the results are weak or fabricated. Route the substantive scope question to baseline-comparison-audit / consistency-audit.",
  "severity": "minor",
  "observability_level_required": 0,
  "evidence": [{"claim_id": "C003",
                "span": "comprehensive empirical evaluation across diverse benchmarks",
                "location": {"file": "paper.txt", "section": "abstract"}}],
  "verdict_local": "warn",
  "false_positive_risk": "high",
  "recommended_reviewer_action": "Glance at whether the float count matches the breadth of the empirical claim; if it feels thin, route the substantive scope check to baseline-comparison-audit / consistency-audit."
}
```

**Worked non-finding — `HP-AI-FLAVOR` (unanchored → held at `info`, the design working):**
the reviewer flags a generic transition paragraph as AI-flavored, but no ledger claim
covers that prose (the extractor keeps numbers / scope / captions / citations, not
free transitions). With no verbatim ledger span to anchor to, the validator (and the
adjudicator) hold it at `info` — a note, never a flag. AI-flavor prose structurally
almost never lands on an extracted claim, so it almost never becomes even a `minor`
flag. **We are not an AI-text classifier.**

**Worked finding — `HP-DEFENSIVE-HEDGE` (the ≥2-span pervasiveness gate):** the
taxonomy's `min_evidence` for this pattern is *representative hedge spans (≥2)* — one
"we do not claim X; instead Y" sentence is legitimate scoping, so a single span must
stay `info`. The reviewer anchors **≥2** representative hedge spans that are themselves
ledger claims (e.g. two abstract/intro sentences captured as `scope` claims) to show
the hedging is *pervasive*, not a one-off positioning line. Even with ≥2 anchored spans
it survives only as `minor`, `false_positive_risk: high` — context to look closer, never
an authorship/editing-provenance claim about how the text was produced. Symmetrically,
**`HP-NARRATIVE-ARC-BREAK`** must
anchor to the actual abstract/intro span and name **which** arc element is missing
(background → contribution → evidence, or problem → why-hard → approach → validation);
an unanchored "the intro reads thin" impression falls to `info`. Neither is an
authorship verdict — for that, use a dedicated detector.

**Failure handling.** A `KeyError` / `JSONDecodeError` means the reviewer output was
malformed → re-run Step 2 once with the strict-JSON reminder. If it is **still**
unparseable, fail closed: write an empty array to `presentation-signals.findings.json`
(`printf '[]' > "$OUT"`) so the output contract's file exists — never hand-author
findings on the reviewer's behalf. If a finding loses all evidence, it is *kept as
`info`* (never silently dropped — the forensic record stays).

## Step 4 — Emit (two files, no merge)

Step 1 wrote `presentation-signals.deterministic.findings.json` (ids `PRES###`);
Step 3 wrote `presentation-signals.findings.json` (validated semantic findings, ids
`F###`). **Keep them separate. Do NOT copy the deterministic findings into the
semantic file** — the orchestrator concatenates `*.findings.json`, so merging would
double-count `HP-DUP-TABLE`. The id namespaces (`PRES###` vs `F###`) do not collide.

If the semantic pass found nothing blatant, `presentation-signals.findings.json` is
`[]` — write it anyway. **Silent skip is forbidden**: the orchestrator and the
standalone adjudicate command both expect the file to exist at a predictable path. For
this auxiliary skill, `[]` (or all-`info`) is the **common, correct** result.

## Step 5 — Trace (forensic; never silently dropped)

Save the raw reviewer call under the `TRACE_DIR` created in Step 2
(`.aris/traces/presentation-signals/<YYYY-MM-DD>_run<NN>/`). This repo ships no
`save_trace.sh`, so write the files directly:

```
.aris/traces/presentation-signals/<date>_run<NN>/
  run.meta.json                       # {skill, paper_id, run_level_L, ledger_sha?, generated_at}
  001-surface-semantic.request.json   # the EXACT prompt sent (paths + checklist; no paper digest)
  001-surface-semantic.response.md    # the FULL raw reviewer response (input to Step 3)
  001-surface-semantic.meta.json      # {model:"gpt-5.5", reasoning:"xhigh", thread_id, sandbox:"read-only"}
```

The `request.json` is the independence audit trail — it must show the executor sent
only **paths + the ledger + the checklist**, never a hunch like "this looks
AI-generated". (Step 1 is deterministic and needs no trace beyond its output file.)

## Step 6 — Hand off, or adjudicate standalone

Within `/anti-autoresearch`, **stop here**: the orchestrator globs every
`*.findings.json`, runs the adjudicator, and emits `REPORT.md` + `report.json`. When
running this skill **alone**, you may produce the report yourself — `--ledger` is
**required** (it is what re-verifies each finding quotes a real ledger span; without it
every above-info finding fails closed to `info`):

```bash
ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
LEDGER="<abs path to claims.json>"; D="$(dirname "$LEDGER")"
# derive paper-id + level from the ledger so this block is self-contained (no carried vars):
PAPER_ID="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["paper_id"])' "$LEDGER")"
L="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("observability_level",0))' "$LEDGER")"
python3 "$ROOT/tools/adjudicate_findings.py" \
    --findings "$D/presentation-signals.deterministic.findings.json" \
               "$D/presentation-signals.findings.json" \
    --ledger "$LEDGER" \
    --paper-id "$PAPER_ID" --observability-level "$L" --taxonomy-version 0.4 \
    --out "$D/report.json" --md "$D/REPORT.md"
```

The adjudicator applies, in order: ANCHOR → OBSERVABILITY → FP-RISK → MEMO → SURFACE
gates, then computes `overall_verdict` ∈ {CLEAN_GIVEN_EVIDENCE, SOFT_FLAGS,
HARD_FLAGS}. **Because every finding here is a surface signal, the strongest possible
contribution is `SOFT_FLAGS`** — a span-anchored, surviving `minor`. A
presentation-signals-only run can **never** produce `HARD_FLAGS`. No model is in the
final decision.

## Output contract

This skill **always** writes, into the ledger's directory:

- `presentation-signals.deterministic.findings.json` — Step 1, a JSON array
  (`schemas/finding.schema.json`); `HP-DUP-TABLE` findings, `reviewer.deterministic:true`,
  ids `PRES###` (or `[]`).
- `presentation-signals.findings.json` — Step 3, a JSON array; validated semantic
  surface findings, ids `F###` (or `[]`). Each above-info finding carries
  `evidence[].claim_id` + a verbatim `span`, `severity: minor`,
  `false_positive_risk: high`, `observability_level_required: 0`, and a
  `pattern_id` ∈ {HP-THIN-FLOAT, HP-LLM-FIGURE, HP-PAGE-PADDING, HP-JARGON-STUFF,
  HP-AI-FLAVOR, HP-DEFENSIVE-HEDGE, HP-NARRATIVE-ARC-BREAK}.
- `.aris/traces/presentation-signals/<date>_run<NN>/` — Step 5, the raw reviewer call.

It writes **no verdict and no report** of its own — `report.json` / `REPORT.md` come
only from `tools/adjudicate_findings.py` (Step 6 / the orchestrator).

## Key rules

- **Auxiliary only; never a verdict.** Surface signals contribute at most
  `SOFT_FLAGS` and are **capped at `minor`** by the adjudicator (by skill AND by
  pattern_id). They are context for the substantive findings, not standalone evidence.
- **Not authorship detection.** Never label a paper "AI-generated" or imply misconduct
  from a surface signal. This repo is **not** an AI-text classifier — it audits
  integrity, not provenance.
- **Default to silence.** Most papers should produce few or zero surface findings; an
  empty (or all-`info`) result is the common, correct output.
- **No span → no flag.** Reject unanchored / paraphrased findings to `info` here (the
  adjudicator re-enforces). `span in claim`, whitespace-normalized — never
  `claim in span`. The sparse ledger is what keeps AI-flavor from ever becoming a flag.
- **FP-risk is always `high`; severity is always `minor`.** Forced by the validator;
  do not argue past the cap.
- **Stay in lane.** Emit only the nine §F surface patterns. Substantive problems
  (numeric contradiction, fake citation, missing baseline) are handed to the
  substantive auditors, never encoded as a capped surface note.
- **Two files, no merge.** Deterministic (`PRES###`) and semantic (`F###`) findings
  stay in separate files to avoid double-counting under the orchestrator's glob.
- **Cross-model, fresh thread.** Reviewer is a different family (gpt-5.5 xhigh); every
  run is a new `mcp__codex__codex` thread; `codex-reply` is never used.
- **Detect-only.** Never edit the audited paper (no `Edit` in `allowed-tools`; reviewer
  sandbox is `read-only`).
- **Reproducible.** Same ledger + same findings → same verdict.

## When NOT to use this skill

- **No `claims.json` yet** → run `/evidence-ledger` first; this skill never invents
  structure from the raw PDF.
- **You want an AI-text / "looks machine-written" verdict** → out of scope by design.
  This skill is auxiliary and capped at `minor`; for authorship detection use a
  dedicated tool (Pangram / GPTZero / Binoculars).
- **You need numeric self-contradiction / method drift** → `/consistency-audit`.
- **You need citation existence / wrong-context** → `/citation-forensics`.
- **You need "SOTA / first" or baseline integrity** → `/baseline-comparison-audit`.
- **You need code/result-level fraud** (fake GT, self-normalization, phantom numbers)
  → `/experiment-forensics` at **L2**.
- **As the basis for a reject / accusation** → never. The strongest thing a surface
  signal can do is say "combine with the substantive findings and look closer."
- **On a timer** → never `/loop` / `/schedule` / `CronCreate` this skill; re-fire only
  when the paper or ledger changes (see the fence at the top).

## Review tracing

Forensic trace policy and file layout are defined once in **Step 5** (Policy:
**forensic** — never silently skipped). The `request.json` records only the paths +
ledger + checklist that were sent (the reviewer-independence audit trail); the
`response.md` is the immutable input that Step 3 validates.