Novelty & Duplication Advisory — the overlap a reviewer should weigh

SkillAI & models

MEMO-ONLY prior-work overlap advisory: surfaces the two ADVISORY taxonomy signals neither a tool nor a model can decide from the paper alone — ADV-TRIVIAL-COMBINATION (standard A+B+C / 缝合 stapling) and ADV-DUPLICATE-PUBLICATION (repackaged / duplicate submission). The executor RETRIEVES candidate prior work (DBLP fuzzy-title + boolean · WebSearch · WebFetch) from the paper's own title + contribution spans in the evidence ledger; TWO fresh cross-model codex reviewers (one per axis) LAY OUT the overlap side-by-side against each anchored contribution claim. It NEVER rules 'trivial' or 'duplicate' (that is a human judgment) and absence of a match is NOT evidence of originality. Emits novelty-duplication-advisory.memo.md + an info-only findings mirror; carries NO verdict weight — tools/adjudicate_findings.py lists it in ZERO_WEIGHT_SKILLS and caps it at info. Detect-only. Adapted from ARIS novelty-check, reframed from 'is MY idea novel' to 'here is the overlap a reviewer should weigh'. Triggers: \"novelty advisory\", \"duplication check\", \"prior-work overlap\", \"is this stapling\", \"缝合\", \"查重\", \"重复发表\", \"duplicate submission\".

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Novelty & Duplication Advisory — the overlap a reviewer should weigh skill

What this skill tells your AI

The instructions your AI receives, as published by wanshuiyin/anti-autoresearch in skills/novelty-duplication-advisory/SKILL.md and read by ahel’s review.

Lay out, for $ARGUMENTS (a paper-dir or a claims.json from /evidence-ledger), the candidate prior-work overlap a human reviewer should weigh for two reviewer-judgment signals — trivial combination ("standard A+B+C") and duplicate publication ("repackaged prior work"). Retrieve candidates, map them side-by-side against the paper's ledger-anchored contribution, and emit novelty-duplication-advisory.memo.md. Run AFTER /evidence-ledger (so claims.json exists). This skill decides nothing — it never rules "trivial" or "duplicate", and the deterministic adjudicator caps it at info.

🔒 Do not wrap this skill in /loop, /schedule, or CronCreate. It retrieves external prior work once and synthesizes it into one advisory memo. Even though it is memo-only (the adjudicator caps it at info, so it adds no verdict weight), the no-new-signal cadence rule still applies: its output changes only when the ledger / the paper / the literature change, never with the wall clock. It is tempting to re-fire on a timer "to catch newly-posted prior work," but a wall-clock loop burns real DBLP + web + cross-model budget on every tick for a paper that has not changed. Schedule the work that precedes it — ledger built → run this once. (Mirrors ARIS's external-cadence doctrine: /loop·/schedule are fire-control, not a judge.)

Adapted from ARIS novelty-check, with one deliberate reframing and one deliberate downgrade. The reframing: ARIS novelty-check asks "is MY idea novel — should I PROCEED / ABANDON?" and hands the author a Score: X/10 + a recommendation; this skill asks "here is the overlap a third-party reviewer should weigh" and hands the human candidates, not a verdict. The downgrade: it is memo-only. Novelty is the textbook example of a judgment that is not decidable from the paper alone, and not decidable at any observability level — it depends on a corpus you can never prove you searched exhaustively. So this skill retrieves and lays out overlap; it refuses to grade it. tools/adjudicate_findings.py lists novelty-duplication-advisory in ZERO_WEIGHT_SKILLS and caps anything it emits at info. The memo informs; the human judges; the deterministic adjudicator owns the report verdict — and this skill never moves it.

Why this exists

Two complaints recur in real reviews of autoresearch (and rushed human) output, and neither is an internal-consistency failure the other auditors catch — they are relational to the wider literature:

  • "标准的 A + B + C,全是已知模块" / "缝合" (stapling) — the paper bolts together three well-known techniques and presents the bundle as the contribution. Whether that bundle is a genuine advance or a trivial staple is a reviewer judgment — a surprising combination is publishable, an obvious one is not, and no tool can draw that line.
  • "这不就是 X 换了个壳" (repackaged / duplicate submission) — the submission looks like a prior paper (often the authors' own) with a new title. An exact title/abstract/DOI match is reportable; the absence of a match proves nothing, because your search corpus is never complete.

Both are listed in references/hack-pattern-taxonomy.md (v0.5) under Advisory signals (NOT a hard pattern · zero verdict weight · reviewer-judgment only)ADV-TRIVIAL-COMBINATION and ADV-DUPLICATE-PUBLICATION. The taxonomy is explicit: "Novelty is a reviewer judgment; the tool can lay out the prior-work overlap, it cannot rule 'trivial'" and "the absence of a match is not evidence of originality."

So this skill does the one honest, high-leverage thing the other auditors do not: it reaches outside the paper to retrieve candidate prior work, and lays it out side-by-side against the paper's own contribution so a human can weigh novelty with the overlap in front of them. It is the only auditor that consults an external corpus — which is exactly why it can carry no verdict weight: the moment a tool grades novelty from an incomplete search, it manufactures the "AI slop grading AI slop" failure this repo exists to refuse. It is the literature-facing complement to citation-forensics: that skill audits the papers the submission does cite; this one surfaces prior work it may not have cited at all.

Core principle

MEMO-ONLY · retrieve-don't-rule · ledger-anchored on the paper side · real-record-bound on the prior-work side · cross-model, fresh thread per axis · reviewer ≠ adjudicator · never rules novelty · absence ≠ originality. Three honesty spines hold this skill up:

  1. Paper side is ledger-anchored. The contribution being compared is pulled from claims.json (claim_id + verbatim span) — never re-invented from the raw PDF (references/integrity-forensics-contract.md rule 1). The paper's title may be read from the source as a search seed only — it is never used as an anchor.
  2. Prior-work side is real-record-bound. Every candidate comes from a real retrieval call (DBLP / WebSearch / WebFetch) and carries a verifiable identifier (arXiv id / DOI / DBLP url). Nothing is recalled "from memory" — fabricating a prior paper here is the same sin as a hallucinated citation (mirrors ARIS novelty-check's anti-hallucination rule and citation-discipline).
  3. No verdict — by design. The cross-model reviewers propose an overlap map + the open questions; they are forbidden to conclude "trivial" or "duplicate." The executor validates anchors; tools/adjudicate_findings.py owns the verdict and caps this skill at info (references/reviewer-independence.md Layer 2). A retrieval that finds nothing is a valid output that says nothing about novelty — "no candidate overlap found" is not "the paper is original."

Deliberate exception to "the reviewer reads only the ledger." The other auditors reason strictly inside the paper. This one must consult an external corpus, so the executor performs the retrieval and hands each reviewer a structured candidates file (real records, with identifiers) alongside the ledger. That file is retrieval output, not a Claude opinion or digest of the paper — so the spirit of reviewer-independence (no executor judgment leaks into the reviewer prompt) still holds.

The anchor is the contribution sentence, not the prior work. Every surfaced item anchors to one of the submission's own contribution claims — a scope / method / comparison claim, OR (because the deterministic extractor types most abstract/intro contribution sentences as number / citation / scope, so the three contribution types alone are too thin to anchor to on a real ledger) any claim located in the abstract / intro section. The candidate prior work — its title, arXiv id / DOI / DBLP url, overlap kind — lives in the memo table and the finding's description, never as the anchor span: there is no ledger claim for an external paper (same shape as citation-forensics, where the DBLP facts go in description and the anchor is the citing sentence).

How this differs from the other auditors (route correctly)

AuditorQuestion it answersExternal lookup?Verdict weight
consistency-auditDoes the paper contradict ITSELF / described method = evaluated method?noyes (via adjudicator)
experiment-forensicsAre reported numbers what the code computes? (fake GT, self-norm, phantom)noyes (L2)
baseline-comparison-auditRight baselines present, tuned, "SOTA" earned?profile onlyyes
citation-forensicsDo the cited papers EXIST and support the claim they are used for?yes (existence/context of cited works)yes
proof-derivation-forensicsDoes the written proof / derivation hold?noyes
presentation-signalsSurface "AI-flavor" hints (auxiliary)nosurface-class label
adversarial-case-builderStrongest anchored rejection memo + defensenonone (memo-only)
novelty-duplication-advisory (this)What prior-work OVERLAP should a reviewer weigh for trivial-combination / duplicate?YES — retrieves uncited prior worknone (memo-only, zero verdict weight)

Route, do not overreach. citation-forensics checks the works the paper already cites; this skill goes looking for prior work the paper omits or overlaps. A wrong-context or fabricated citation belongs to citation-forensics. A "SOTA / first / beats prior work" claim that needs a baseline comparison belongs to baseline-comparison-audit (this skill can emit needs_external_check and hand it off). An internal scope-overclaim ("comprehensive" on thin scope) belongs to consistency-audit (HP-SCOPE-INFLATE). This skill owns only the two advisory overlap signals — and even those it only surfaces.

How this differs from ARIS novelty-check (the parent)

ARIS novelty-checknovelty-duplication-advisory (forensics)
Frame"is MY idea novel — PROCEED / ABANDON?""here is the overlap a REVIEWER should weigh"
Subjectthe author's own prospective ideaa third party's submitted paper
Paper sidea free-text method descriptionledger claims (claim_id + verbatim span)
Outputa Score: X/10 + recommendation + "suggested positioning"a side-by-side overlap memo — no score, no recommendation
Verdict"Novelty: HIGH/MEDIUM/LOW"none — never rules trivial/duplicate; zero verdict weight
Empty retrieval"looks novel, proceed""no candidate overlap found — this says nothing about novelty"
Prior-work anchoringverify_papers.py (anti-hallucination)every candidate a resolved record in candidates.json; unresolved → dropped

The multi-source retrieval and the cross-model verification are kept exactly (they are the load-bearing parts). What changes is the refusal to grade: a forensics tool that scored novelty from an incomplete corpus would be precisely the over-claim this repo refuses.

Constants & Reviewer Calling Convention

REVIEWER_MODEL       = gpt-5.6-sol                # different family from the 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 reads claims.json + candidates.json from cwd
THREAD_POLICY        = TWO fresh mcp__codex__codex calls — ONE per axis (duplicate / combination);
                       NEVER mcp__codex__codex-reply across them (the per-dimension bias guard)
CONCURRENCY          = serial                 # Codex MCP hangs on concurrent calls — Thread 2 waits for Thread 1
AXES                 = duplicate (ADV-DUPLICATE-PUBLICATION) | combination (ADV-TRIVIAL-COMBINATION)
OBS_REQUIRED         = 0 for both             # decidable-as-advisory from text + the public corpus (no repo/results)
RETRIEVAL_SOURCES    = DBLP (fuzzy_title_search + boolean search) · WebSearch · WebFetch (abstracts)
ANTI_HALLUCINATION   = every candidate comes from a REAL retrieval call + carries a verifiable id +
                       is a record in candidates.json; the reviewer cites candidate_id ONLY (never "from memory")
ANCHOR_UNIVERSE      = contribution claims = type∈{scope,method,comparison} OR section∈{abstract,intro,introduction};
                       prior work goes in the memo/description, NOT the anchor span
DISPOSITION          = candidate_overlap_surfaced | no_candidate_overlap_found | retrieval_incomplete
                       (INFORMATIONAL, NOT a verdict; "no overlap found" ≠ "the paper is original")
ADVISORY_PATTERNS    = ADV-TRIVIAL-COMBINATION · ADV-DUPLICATE-PUBLICATION   (zero verdict weight)
TAXONOMY_VERSION     = 0.5                     # references/hack-pattern-taxonomy.md
MEMO_FILE            = novelty-duplication-advisory.memo.md            # canonical human-facing output
FINDINGS_FILE        = novelty-duplication-advisory.findings.json      # info-only mirror (or []); globbed, zero verdict weight
PROFILE_FILE         = novelty-duplication-advisory.profile.json       # executor-built retrieval profile (from the ledger)
CANDIDATES_FILE      = novelty-duplication-advisory.candidates.json    # retrieved prior work (REAL records only)
TRACE_POLICY         = forensic (never silently dropped)
TRACE_DIR            = .aris/traces/novelty-duplication-advisory/<YYYY-MM-DD>_run<NN>/
  • Executor (Claude) owns the retrieval and none of the judgment: it pulls the contribution spans from the ledger, reads the title from the source as a search seed, runs the external searches, assembles a structured candidates.json of real returned records, passes the ledger + the candidates file + the per-axis checklist to each reviewer, validates every anchor the reviewers return, and renders the memo. It never summarizes the paper, pre-judges overlap, or leaks an opinion into a prompt (references/reviewer-independence.md).
  • Reviewer (codex / gpt-5.6-sol, xhigh, read-only) reads claims.json + candidates.json from its cwd, lays each candidate beside the contribution span it overlaps with, and lists the open questions. It is the overlap-mapper, not the judge — and it is forbidden to output a "trivial" / "duplicate" / "not novel" verdict. It cites only candidate_ids that exist in candidates.json.
  • Two fresh threads, serial, no codex-reply. The duplicate axis and the combination axis are independent mcp__codex__codex calls; never carry one axis's conclusion into the other (the per-dimension bias guard). codex-reply is intentionally absent from allowed-tools.
  • Detect-only. No Edit in allowed-tools; the reviewer sandbox is read-only. This skill never touches the audited paper.

Step 0 — Preconditions: locate the ledger, read the run level, open the trace

This skill reasons over the ledger (paper side) + retrieved candidates (prior-work side) — never the raw PDF for structure. Resolve the ledger and read the observability level L, paper_id, and the count of contribution claims (each Bash block is self-contained — shell state does not persist between calls, so re-derive paths every step):

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
# advise on 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"))
CONTRIB_TYPES = {"scope", "method", "comparison"}
CONTRIB_SECT  = {"abstract", "intro", "introduction"}
def sect(c): return ((c.get("location") or {}).get("section") or "").lower()
contrib = [c for c in d.get("claims", [])
           if c.get("claim_id") and c.get("text_span")
           and (c.get("type") in CONTRIB_TYPES or sect(c) in CONTRIB_SECT)]
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("CONTRIB_CLAIMS =", len(contrib), "(scope/method/comparison + abstract/intro — the anchor universe)")
PY

Carry forward the absolute LEDGER / PAPER_DIR, plus L and PAPER_ID, into every step below.

Failure / edge handling.

  • NO_LEDGER → stop and tell the user to run /evidence-ledger first. This skill never re-reads the raw PDF and invents its own structure (contract rule 1).
  • CONTRIB_CLAIMS == 0 → there is no contribution claim to anchor to. Recall scales with the ledger: an L0 PDF-text ledger extracts mostly number/scope spans, so contribution anchoring is thin; the richer abstract/intro spans enter via the L1 LaTeX path. Prefer re-running /evidence-ledger on the LaTeX source. If you cannot, skip the reviewer call, write the honest-null memo + an empty findings file directly (Step 4's honest-null snippet), and stop. Never invent a contribution claim.
  • Observability level does not gate whether this skill runs. Retrieval needs only the title/contribution text, available at L0+. There is no graded verdict to gate — the judgment is not decidable at any level (it is a human call). The info-only mirror carries observability_level_required: 0, and the MEMO gate caps it at info regardless.

Create the trace directory now (forensic; written before any reviewer/retrieval call):

PAPER_DIR="<abs PAPER_DIR from Step 0>"
DATE=$(date +%F); N=1
while [ -d "$PAPER_DIR/.aris/traces/novelty-duplication-advisory/${DATE}_run$(printf %02d $N)" ]; do N=$((N+1)); done
TRACE="$PAPER_DIR/.aris/traces/novelty-duplication-advisory/${DATE}_run$(printf %02d $N)"
mkdir -p "$TRACE"; echo "TRACE = $TRACE"   # carry this absolute path into every later step

Write "$TRACE/run.meta.json" (via Write) = {"skill":"novelty-duplication-advisory","paper_id":"<PAPER_ID>","run_level_L":<L>,"taxonomy_version":"0.5","retrieval":{"duplicate":"pending","combination":"pending"},"generated_at":"<UTC ISO-8601>"}.

Step 1 — Build the retrieval profile from the ledger (executor, deterministic)

Pull the paper's contribution (the thing whose novelty a reviewer weighs) straight from the ledger — scope / method / comparison claims plus anything in the abstract / intro sections — and read the title from the source as a search seed. These spans are the paper-side anchors; the executor never paraphrases them and never invents a contribution the ledger does not contain.

LEDGER="<abs claims.json from Step 0>"
PAPER_DIR="$(dirname "$LEDGER")"
python3 - "$LEDGER" "$PAPER_DIR" <<'PY'
import json, sys, os, re
ledger_path, paper_dir = sys.argv[1], sys.argv[2]
d = json.load(open(ledger_path, encoding="utf-8"))
claims = d.get("claims", []); PID = d.get("paper_id", "?")
src = d.get("source_files", []) or []
def sect(c): return ((c.get("location") or {}).get("section") or "").lower()

CONTRIB_TYPES = {"scope", "method", "comparison"}
CONTRIB_SECT  = {"abstract", "intro", "introduction"}
PRI = {"abstract", "intro", "introduction"}
CUE = re.compile(r"\b(we\s+(propose|present|introduce|develop|design|show|demonstrate)|"
                 r"our\s+(approach|method|framework|model|contribution|key\s+idea)|"
                 r"in\s+this\s+(paper|work)|the\s+first\s+to|novel|contributions?\s+(are|of))\b", re.I)

ranked = []
for c in claims:
    if not c.get("claim_id") or not c.get("text_span"):
        continue
    s = sect(c)
    if not (c.get("type") in CONTRIB_TYPES or s in CONTRIB_SECT):
        continue
    span = c["text_span"]
    score = (2 if s in PRI else 0) + (2 if CUE.search(span) else 0) + (1 if c.get("type") in CONTRIB_TYPES else 0)
    ranked.append((score, {"claim_id": c["claim_id"], "type": c.get("type", "?"),
                           "section": s or "?", "text_span": span}))
ranked.sort(key=lambda x: -x[0])
contrib = [c for _, c in ranked][:10]

# title SEARCH SEED (never an anchor): prefer a title-section ledger claim, else \title{...}
# from a latex source (brace-matched), else the first substantive line of the pdf text.
def strip_tex(s):
    s = re.sub(r"\\thanks\{[^}]*\}", " ", s)
    s = re.sub(r"\\[a-zA-Z]+\*?", " ", s)
    return " ".join(s.replace("{", " ").replace("}", " ").replace("\\", " ").split())
def title_from_latex(txt):
    m = re.search(r"\\title\s*(\[[^\]]*\])?\s*\{", txt)
    if not m: return None
    i, depth = m.end(), 1
    while i < len(txt) and depth:
        depth += (txt[i] == "{") - (txt[i] == "}"); i += 1
    t = strip_tex(txt[m.end():i-1]); return t if len(t) >= 6 else None

title, title_src = None, None
for c in claims:
    if sect(c) == "title" and c.get("text_span"):
        title, title_src = c["text_span"], "ledger:title-claim"; break
if not title:
    for s in src:
        if s.get("kind") == "latex" and os.path.isfile(s.get("path", "")):
            t = title_from_latex(open(s["path"], encoding="utf-8", errors="replace").read())
            if t: title, title_src = t[:240], "source:" + os.path.basename(s["path"]); break
if not title:
    for s in src:
        if s.get("kind") in ("text", "pdf") and os.path.isfile(s.get("path", "")):
            for ln in open(s["path"], encoding="utf-8", errors="replace"):
                ln = ln.strip()
                if len(ln) >= 12 and not ln.lower().startswith(("arxiv", "http", "doi")):
                    title, title_src = ln[:240], "source:" + os.path.basename(s["path"]); break
        if title: break
if not title and contrib:
    title, title_src = contrib[0]["text_span"][:240], "fallback:abstract-claim"

prof = {"paper_id": PID, "title_seed": title, "title_seed_source": title_src,
        "title_seed_is_anchor": False, "contribution_claims": contrib,
        "n_contribution_claims": len(contrib)}
out = os.path.join(paper_dir, "novelty-duplication-advisory.profile.json")
json.dump(prof, open(out, "w", encoding="utf-8"), indent=2, ensure_ascii=False)
print("PROFILE  =", out)
print("TITLE    =", (title or "(none — combination axis only)")[:120], "| source:", title_src)
print("CONTRIB  =", len(contrib))
for c in contrib[:10]:
    print(f"  [{c['claim_id']}] ({c['section']}/{c['type']}) {c['text_span'][:100]}")
if not contrib:
    print("NO_CONTRIB: ledger has no scope/method/comparison/abstract/intro claim — "
          "cannot build a retrieval query. Treat like CONTRIB_CLAIMS==0 (Step 4 honest-null).")
PY

Sanity gate (before searching). title_seed should look like a paper title and CONTRIB should be > 0 for any normal paper. If a span looks truncated or mis-sectioned, Read claims.json and spot-check that contribution claim_id's text_span before you build a query from it — a malformed seed wastes the external-search budget. Never fabricate a title or a contribution to fill a gap.

Building the queries (literal terms only). From profile.json:

  • Title (for the duplicate axis). Use title_seed verbatim. It is a query seed, not an anchor (title_seed_is_anchor: false); re-opening a source to quote it is permitted because it is a query input, not a finding. If it is None, run only the combination axis.
  • Constituent techniques (for the combination axis). Decompose the contribution into ≤4 named techniques using literal terms copied from the contribution spans (the method names, the architecture, the training objective). Do not introduce vocabulary the spans do not contain — that would manufacture overlap. If the contribution is one atomic method (no decomposition into ≥2 known components), the combination axis is N/A; say so and run only the duplicate axis.

Failure handling. NO_CONTRIB → write the honest-null memo + empty findings (Step 4 snippet) and stop. A thin ledger may yield a weak query — note that limitation in the memo; do not pad the query with guessed terms.

Step 2 — Retrieve candidate prior work (executor; real records only)

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
152
Forks
8
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
novelty-duplication-advisory
Source
github.com/wanshuiyin/anti-autoresearch