Novelty & Duplication Advisory — the overlap a reviewer should weigh
SkillAI & modelsMEMO-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.
No other account needed.
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, orCronCreate. It retrieves external prior work once and synthesizes it into one advisory memo. Even though it is memo-only (the adjudicator caps it atinfo, 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·/scheduleare fire-control, not a judge.)
Adapted from ARIS
novelty-check, with one deliberate reframing and one deliberate downgrade. The reframing: ARISnovelty-checkasks "is MY idea novel — should I PROCEED / ABANDON?" and hands the author aScore: 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.pylistsnovelty-duplication-advisoryinZERO_WEIGHT_SKILLSand caps anything it emits atinfo. 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:
- 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.mdrule 1). The paper's title may be read from the source as a search seed only — it is never used as an anchor. - 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 andcitation-discipline). - 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.pyowns the verdict and caps this skill atinfo(references/reviewer-independence.mdLayer 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/comparisonclaim, OR (because the deterministic extractor types most abstract/intro contribution sentences asnumber/citation/scope, so the three contribution types alone are too thin to anchor to on a real ledger) any claim located in theabstract/introsection. The candidate prior work — its title, arXiv id / DOI / DBLP url, overlap kind — lives in the memo table and the finding'sdescription, never as the anchor span: there is no ledger claim for an external paper (same shape ascitation-forensics, where the DBLP facts go indescriptionand the anchor is the citing sentence).
How this differs from the other auditors (route correctly)
| Auditor | Question it answers | External lookup? | Verdict weight |
|---|---|---|---|
consistency-audit | Does the paper contradict ITSELF / described method = evaluated method? | no | yes (via adjudicator) |
experiment-forensics | Are reported numbers what the code computes? (fake GT, self-norm, phantom) | no | yes (L2) |
baseline-comparison-audit | Right baselines present, tuned, "SOTA" earned? | profile only | yes |
citation-forensics | Do the cited papers EXIST and support the claim they are used for? | yes (existence/context of cited works) | yes |
proof-derivation-forensics | Does the written proof / derivation hold? | no | yes |
presentation-signals | Surface "AI-flavor" hints (auxiliary) | no | surface-class label |
adversarial-case-builder | Strongest anchored rejection memo + defense | no | none (memo-only) |
novelty-duplication-advisory (this) | What prior-work OVERLAP should a reviewer weigh for trivial-combination / duplicate? | YES — retrieves uncited prior work | none (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-check | novelty-duplication-advisory (forensics) | |
|---|---|---|
| Frame | "is MY idea novel — PROCEED / ABANDON?" | "here is the overlap a REVIEWER should weigh" |
| Subject | the author's own prospective idea | a third party's submitted paper |
| Paper side | a free-text method description | ledger claims (claim_id + verbatim span) |
| Output | a 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 anchoring | verify_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.jsonof 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.jsonfrom itscwd, 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 onlycandidate_ids that exist incandidates.json. - Two fresh threads, serial, no
codex-reply. The duplicate axis and the combination axis are independentmcp__codex__codexcalls; never carry one axis's conclusion into the other (the per-dimension bias guard).codex-replyis intentionally absent fromallowed-tools. - Detect-only. No
Editinallowed-tools; the reviewer sandbox isread-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-ledgerfirst. 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-ledgeron 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 atinforegardless.
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_seedverbatim. 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 isNone, 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