chitragupta-cli 6.7.0__tar.gz
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.
- chitragupta_cli-6.7.0/.claude/agents/deep-research-interviewer.md +131 -0
- chitragupta_cli-6.7.0/.claude/agents/deep-research-writer.md +138 -0
- chitragupta_cli-6.7.0/.claude/agents/peer-reviewer.md +115 -0
- chitragupta_cli-6.7.0/.claude/hooks/citation_gate_hook.py +73 -0
- chitragupta_cli-6.7.0/.claude/hooks/draft_target.py +109 -0
- chitragupta_cli-6.7.0/.claude/hooks/session_start_hook.py +166 -0
- chitragupta_cli-6.7.0/.claude/hooks/style_check_hook.py +137 -0
- chitragupta_cli-6.7.0/.claude/settings.json +50 -0
- chitragupta_cli-6.7.0/.claude/skills/book-assembler/SKILL.md +315 -0
- chitragupta_cli-6.7.0/.claude/skills/corpus-reviser/SKILL.md +207 -0
- chitragupta_cli-6.7.0/.claude/skills/deep-research/SKILL.md +638 -0
- chitragupta_cli-6.7.0/.claude/skills/deep-research/reference.md +218 -0
- chitragupta_cli-6.7.0/.claude/skills/draft-reviser/SKILL.md +663 -0
- chitragupta_cli-6.7.0/.claude/skills/overlap-reviser/SKILL.md +311 -0
- chitragupta_cli-6.7.0/.claude/skills/survey-writer/SKILL.md +489 -0
- chitragupta_cli-6.7.0/.claude/skills/textbook-chapter-writer/SKILL.md +493 -0
- chitragupta_cli-6.7.0/.claude/skills/thesis-chapter-writer/SKILL.md +451 -0
- chitragupta_cli-6.7.0/.claude/skills/tutorial-writer/SKILL.md +543 -0
- chitragupta_cli-6.7.0/AGENTS.md +204 -0
- chitragupta_cli-6.7.0/CLAUDE.md +60 -0
- chitragupta_cli-6.7.0/LICENSE +21 -0
- chitragupta_cli-6.7.0/PKG-INFO +14 -0
- chitragupta_cli-6.7.0/README.md +346 -0
- chitragupta_cli-6.7.0/SOUL.md +91 -0
- chitragupta_cli-6.7.0/assets/csl/README.md +34 -0
- chitragupta_cli-6.7.0/assets/csl/ieee.csl +519 -0
- chitragupta_cli-6.7.0/assets/style/README.md +51 -0
- chitragupta_cli-6.7.0/assets/style/acronyms.toml +10 -0
- chitragupta_cli-6.7.0/assets/style/acronyms.toml.example +8 -0
- chitragupta_cli-6.7.0/assets/vale/README.md +136 -0
- chitragupta_cli-6.7.0/assets/vale/styles/chitragupta/Acronyms.yml +133 -0
- chitragupta_cli-6.7.0/assets/vale/styles/chitragupta/DefectMarkers.yml +11 -0
- chitragupta_cli-6.7.0/assets/vale/styles/chitragupta/DialectGB.yml +55 -0
- chitragupta_cli-6.7.0/assets/vale/styles/chitragupta/DialectIN.yml +32 -0
- chitragupta_cli-6.7.0/assets/vale/styles/chitragupta/DialectUS.yml +69 -0
- chitragupta_cli-6.7.0/assets/vale/styles/chitragupta/Just.yml +7 -0
- chitragupta_cli-6.7.0/assets/vale/vale.ini +58 -0
- chitragupta_cli-6.7.0/chitragupta/__init__.py +0 -0
- chitragupta_cli-6.7.0/chitragupta/__main__.py +141 -0
- chitragupta_cli-6.7.0/chitragupta/acronyms.py +191 -0
- chitragupta_cli-6.7.0/chitragupta/bib_collections.py +141 -0
- chitragupta_cli-6.7.0/chitragupta/bib_names.py +61 -0
- chitragupta_cli-6.7.0/chitragupta/bib_reader.py +317 -0
- chitragupta_cli-6.7.0/chitragupta/citation_gate.py +278 -0
- chitragupta_cli-6.7.0/chitragupta/config.py +652 -0
- chitragupta_cli-6.7.0/chitragupta/corpus.py +122 -0
- chitragupta_cli-6.7.0/chitragupta/dedup.py +70 -0
- chitragupta_cli-6.7.0/chitragupta/dossier/__init__.py +303 -0
- chitragupta_cli-6.7.0/chitragupta/dossier/_acronyms.py +150 -0
- chitragupta_cli-6.7.0/chitragupta/dossier/_archive.py +243 -0
- chitragupta_cli-6.7.0/chitragupta/dossier/_brief.py +183 -0
- chitragupta_cli-6.7.0/chitragupta/dossier/_citekeys.py +213 -0
- chitragupta_cli-6.7.0/chitragupta/dossier/_cli.py +134 -0
- chitragupta_cli-6.7.0/chitragupta/dossier/_create.py +210 -0
- chitragupta_cli-6.7.0/chitragupta/dossier/_drift.py +283 -0
- chitragupta_cli-6.7.0/chitragupta/dossier/_drift_report.py +126 -0
- chitragupta_cli-6.7.0/chitragupta/dossier/_language.py +67 -0
- chitragupta_cli-6.7.0/chitragupta/dossier/_retrieval.py +302 -0
- chitragupta_cli-6.7.0/chitragupta/dossier/_sections.py +314 -0
- chitragupta_cli-6.7.0/chitragupta/dossier/_status.py +234 -0
- chitragupta_cli-6.7.0/chitragupta/draft.py +134 -0
- chitragupta_cli-6.7.0/chitragupta/enrich/__init__.py +21 -0
- chitragupta_cli-6.7.0/chitragupta/enrich/__main__.py +398 -0
- chitragupta_cli-6.7.0/chitragupta/enrich/corpus.py +62 -0
- chitragupta_cli-6.7.0/chitragupta/enrich/docling_parse.py +723 -0
- chitragupta_cli-6.7.0/chitragupta/enrich/embed_index.py +277 -0
- chitragupta_cli-6.7.0/chitragupta/enrich/topic_model.py +136 -0
- chitragupta_cli-6.7.0/chitragupta/hook_launchers.py +211 -0
- chitragupta_cli-6.7.0/chitragupta/init.py +204 -0
- chitragupta_cli-6.7.0/chitragupta/ledger.py +628 -0
- chitragupta_cli-6.7.0/chitragupta/logging_setup.py +291 -0
- chitragupta_cli-6.7.0/chitragupta/overlap_align.py +236 -0
- chitragupta_cli-6.7.0/chitragupta/overlap_chroma.py +184 -0
- chitragupta_cli-6.7.0/chitragupta/overlap_embed.py +317 -0
- chitragupta_cli-6.7.0/chitragupta/overlap_index.py +626 -0
- chitragupta_cli-6.7.0/chitragupta/overlap_segments.py +363 -0
- chitragupta_cli-6.7.0/chitragupta/overlap_skipgram.py +442 -0
- chitragupta_cli-6.7.0/chitragupta/passages.py +290 -0
- chitragupta_cli-6.7.0/chitragupta/pdf_text.py +1419 -0
- chitragupta_cli-6.7.0/chitragupta/porter_stemmer.py +236 -0
- chitragupta_cli-6.7.0/chitragupta/progname.py +44 -0
- chitragupta_cli-6.7.0/chitragupta/references.py +530 -0
- chitragupta_cli-6.7.0/chitragupta/registry/__init__.py +244 -0
- chitragupta_cli-6.7.0/chitragupta/registry/_cli.py +154 -0
- chitragupta_cli-6.7.0/chitragupta/render_output/__init__.py +346 -0
- chitragupta_cli-6.7.0/chitragupta/render_output/_assets.py +88 -0
- chitragupta_cli-6.7.0/chitragupta/render_output/_citeproc.py +128 -0
- chitragupta_cli-6.7.0/chitragupta/render_output/_cli.py +145 -0
- chitragupta_cli-6.7.0/chitragupta/render_output/_csl.py +89 -0
- chitragupta_cli-6.7.0/chitragupta/render_output/_errors.py +33 -0
- chitragupta_cli-6.7.0/chitragupta/render_output/_figures.py +338 -0
- chitragupta_cli-6.7.0/chitragupta/render_output/_paths.py +101 -0
- chitragupta_cli-6.7.0/chitragupta/retrieval.py +555 -0
- chitragupta_cli-6.7.0/chitragupta/review/__init__.py +305 -0
- chitragupta_cli-6.7.0/chitragupta/review/__main__.py +95 -0
- chitragupta_cli-6.7.0/chitragupta/review/citation_coverage.py +239 -0
- chitragupta_cli-6.7.0/chitragupta/review/citation_provenance.py +502 -0
- chitragupta_cli-6.7.0/chitragupta/review/verbatim_check.py +2357 -0
- chitragupta_cli-6.7.0/chitragupta/runlock.py +210 -0
- chitragupta_cli-6.7.0/chitragupta/sentences.py +106 -0
- chitragupta_cli-6.7.0/chitragupta/spec/__init__.py +250 -0
- chitragupta_cli-6.7.0/chitragupta/spec/_cli.py +208 -0
- chitragupta_cli-6.7.0/chitragupta/style_acronym_drift.py +66 -0
- chitragupta_cli-6.7.0/chitragupta/style_check.py +282 -0
- chitragupta_cli-6.7.0/chitragupta/style_report.py +60 -0
- chitragupta_cli-6.7.0/chitragupta/style_rules.py +26 -0
- chitragupta_cli-6.7.0/chitragupta/sync.py +791 -0
- chitragupta_cli-6.7.0/chitragupta/unit/__init__.py +229 -0
- chitragupta_cli-6.7.0/chitragupta/unit/_cli.py +123 -0
- chitragupta_cli-6.7.0/config.toml.example +384 -0
- chitragupta_cli-6.7.0/docs/ARCHITECTURE.md +738 -0
- chitragupta_cli-6.7.0/docs/AUTO-IMPROVEMENT-RATIONALE.md +495 -0
- chitragupta_cli-6.7.0/docs/AUTO-IMPROVEMENT.md +323 -0
- chitragupta_cli-6.7.0/docs/BOOKS.md +613 -0
- chitragupta_cli-6.7.0/docs/CITATION-PROVENANCE.md +555 -0
- chitragupta_cli-6.7.0/docs/CLI.md +1510 -0
- chitragupta_cli-6.7.0/docs/CODE-STANDARDS.md +543 -0
- chitragupta_cli-6.7.0/docs/CONFIG.md +689 -0
- chitragupta_cli-6.7.0/docs/DESIGN.md +305 -0
- chitragupta_cli-6.7.0/docs/DIAGRAMS.md +1000 -0
- chitragupta_cli-6.7.0/docs/DRAFT-ITERATION.md +725 -0
- chitragupta_cli-6.7.0/docs/EXPORT-ZOTERO-GROUPS.md +167 -0
- chitragupta_cli-6.7.0/docs/GENRE.md +487 -0
- chitragupta_cli-6.7.0/docs/GROBID-CITATION-GRAPH.md +331 -0
- chitragupta_cli-6.7.0/docs/HOOKS.md +711 -0
- chitragupta_cli-6.7.0/docs/HOUSE-STYLE.md +244 -0
- chitragupta_cli-6.7.0/docs/INSPIRATION.md +127 -0
- chitragupta_cli-6.7.0/docs/LADDERS.md +629 -0
- chitragupta_cli-6.7.0/docs/NAME.md +66 -0
- chitragupta_cli-6.7.0/docs/PACKAGING.md +231 -0
- chitragupta_cli-6.7.0/docs/PARALLELISM.md +439 -0
- chitragupta_cli-6.7.0/docs/PDF-PARSER.md +426 -0
- chitragupta_cli-6.7.0/docs/PERFORMANCE.md +529 -0
- chitragupta_cli-6.7.0/docs/PLAGIARISM-DESIGN.md +445 -0
- chitragupta_cli-6.7.0/docs/PLAGIARISM.md +476 -0
- chitragupta_cli-6.7.0/docs/PROMPTS.md +226 -0
- chitragupta_cli-6.7.0/docs/REJECTION.md +444 -0
- chitragupta_cli-6.7.0/docs/REQUIREMENTS.md +605 -0
- chitragupta_cli-6.7.0/docs/RETRIEVAL.md +224 -0
- chitragupta_cli-6.7.0/docs/TECHNICAL-DEBT.md +1195 -0
- chitragupta_cli-6.7.0/docs/TOKENS.md +838 -0
- chitragupta_cli-6.7.0/docs/WRITING-STANDARDS.md +431 -0
- chitragupta_cli-6.7.0/docs/ZOTERO.md +163 -0
- chitragupta_cli-6.7.0/docs/diagrams/00-main-workflow.mmd +113 -0
- chitragupta_cli-6.7.0/docs/diagrams/extra-ledger-state.mmd +41 -0
- chitragupta_cli-6.7.0/docs/diagrams/extra-sequence.mmd +72 -0
- chitragupta_cli-6.7.0/docs/diagrams/g1-corpus-led.mmd +48 -0
- chitragupta_cli-6.7.0/docs/diagrams/g2-teaching.mmd +46 -0
- chitragupta_cli-6.7.0/docs/diagrams/g3-thesis.mmd +46 -0
- chitragupta_cli-6.7.0/docs/diagrams/svg/00-main-workflow.svg +1 -0
- chitragupta_cli-6.7.0/docs/diagrams/svg/extra-ledger-state.svg +1 -0
- chitragupta_cli-6.7.0/docs/diagrams/svg/extra-sequence.svg +1 -0
- chitragupta_cli-6.7.0/docs/diagrams/svg/g1-corpus-led.svg +1 -0
- chitragupta_cli-6.7.0/docs/diagrams/svg/g2-teaching.svg +1 -0
- chitragupta_cli-6.7.0/docs/diagrams/svg/g3-thesis.svg +1 -0
- chitragupta_cli-6.7.0/docs/diagrams/svg/v1-overview.svg +1 -0
- chitragupta_cli-6.7.0/docs/diagrams/svg/v2-first-run.svg +1 -0
- chitragupta_cli-6.7.0/docs/diagrams/svg/v3-artifacts.svg +1 -0
- chitragupta_cli-6.7.0/docs/diagrams/svg/v4-gates-and-failure.svg +1 -0
- chitragupta_cli-6.7.0/docs/diagrams/svg/v5-parallelism.svg +1 -0
- chitragupta_cli-6.7.0/docs/diagrams/v1-overview.mmd +36 -0
- chitragupta_cli-6.7.0/docs/diagrams/v2-first-run.mmd +52 -0
- chitragupta_cli-6.7.0/docs/diagrams/v3-artifacts.mmd +83 -0
- chitragupta_cli-6.7.0/docs/diagrams/v4-gates-and-failure.mmd +65 -0
- chitragupta_cli-6.7.0/docs/diagrams/v5-parallelism.mmd +78 -0
- chitragupta_cli-6.7.0/docs/logo-dark.svg +11 -0
- chitragupta_cli-6.7.0/docs/logo.svg +11 -0
- chitragupta_cli-6.7.0/pyproject.toml +206 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deep-research-interviewer
|
|
3
|
+
description: Perspective-driven interviewer for the deep-research skill's Phase 2. Grounds every claim in a real citekey from this project's synced corpus (never a URL, never invented) instead of live web sources. Dispatched in parallel, one per persona, by .claude/skills/deep-research/SKILL.md -- not meant to be invoked directly by a user request.
|
|
4
|
+
tools: Bash, Read, Grep, Glob
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# deep-research-interviewer
|
|
8
|
+
|
|
9
|
+
Adapted from [hadufer/claude-storm](https://github.com/hadufer/claude-storm)'s
|
|
10
|
+
`agents/storm-researcher.md` (MIT License) -- a perspective-driven
|
|
11
|
+
interviewer, retooled here to ground claims in this project's closed corpus
|
|
12
|
+
(`content/ledger.sqlite` + `papers/bibliography.bib`) instead of
|
|
13
|
+
live web search. Read `.claude/skills/deep-research/reference.md` §3 for
|
|
14
|
+
the full protocol; this file is the packet schema and grounding discipline.
|
|
15
|
+
|
|
16
|
+
## Core function
|
|
17
|
+
|
|
18
|
+
A perspective-driven interviewer that grounds every claim in this project's
|
|
19
|
+
synced corpus, simulating one editorial angle on the topic.
|
|
20
|
+
|
|
21
|
+
## Input parameters (given by the orchestrating skill)
|
|
22
|
+
|
|
23
|
+
- **TOPIC**: research subject
|
|
24
|
+
- **PERSPECTIVE**: assigned persona name + focus
|
|
25
|
+
- **ROUNDS**: interview cycles (default 3, per depth preset)
|
|
26
|
+
|
|
27
|
+
## Interview process (per round)
|
|
28
|
+
|
|
29
|
+
1. Generate one persona-specific question -- never repeat a question asked
|
|
30
|
+
earlier in this interview; go deeper each round.
|
|
31
|
+
2. Formulate up to 3 search-query reformulations of that question.
|
|
32
|
+
3. Run each against this project's corpus:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
python -m chitragupta.draft retrieve search "<query>" --k 15 --log <the draft path you were given>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Pass `--log` on every call. The dispatching skill hands you the draft
|
|
39
|
+
path; it records your query in the shared dossier, which is what lets a
|
|
40
|
+
later `dossier status` tell this report which newly synced papers it has
|
|
41
|
+
never seen. Appending is concurrency-safe, so every interviewer logs.
|
|
42
|
+
or, if `content/chroma/` exists (the embedding stack has been built for
|
|
43
|
+
this corpus):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python -c "from chitragupta.enrich import embed_index; [print(r) for r in embed_index.search('<query>', k=15)]"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Where a 500-character snippet is not enough to decide on a source you
|
|
50
|
+
are minded to cite, read more of that one document:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
python -m chitragupta.draft retrieve evidence "<query>" --citekey <key> \
|
|
54
|
+
--log <draft path>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
4. **Filter before using anything as evidence.** A hit is a candidate, not
|
|
58
|
+
evidence: a high score means the query's words are in the document, not
|
|
59
|
+
that it supports a claim. Judge each snippet yourself and discard what
|
|
60
|
+
doesn't genuinely support one.
|
|
61
|
+
|
|
62
|
+
**Do not economise here.** Your job is breadth -- finding what your
|
|
63
|
+
perspective sees that others don't, including sources that *disagree*
|
|
64
|
+
with each other, which is what Phase 3's contradiction map is built
|
|
65
|
+
from. Disagreement is usually stated in a discussion or limitations
|
|
66
|
+
section rather than near a keyword hit, so a source ruled out cheaply
|
|
67
|
+
is exactly the one the map needed. `docs/REJECTION.md` has the
|
|
68
|
+
reasoning; the short version is that this skill already pays for its
|
|
69
|
+
token efficiency by running you in a subagent, and should not buy more
|
|
70
|
+
of it with coverage.
|
|
71
|
+
5. Answer using only what survived filtering, every sentence cited by its
|
|
72
|
+
real citekey. If nothing relevant survives after reformulating, say so:
|
|
73
|
+
"no appropriate answer can be formulated from this corpus" is a valid,
|
|
74
|
+
honest output for this question.
|
|
75
|
+
|
|
76
|
+
## The corpus is read-only, and you don't own any file
|
|
77
|
+
|
|
78
|
+
Never run `python -m chitragupta.corpus sync`, `python -m chitragupta.enrich`,
|
|
79
|
+
or any `chitragupta/enrich/*`
|
|
80
|
+
build stage. Both take the pipeline's write lock and can run for tens of
|
|
81
|
+
minutes, and several of you run in parallel. Use `content/chroma/` only if
|
|
82
|
+
it already exists; if it doesn't, fall back to `chitragupta.retrieval.search()`
|
|
83
|
+
and
|
|
84
|
+
say so in your packet -- do not build one.
|
|
85
|
+
|
|
86
|
+
You write no files at all. In particular you never write into
|
|
87
|
+
`content/dossiers/` -- the orchestrating run owns the dossier and
|
|
88
|
+
transcribes your packet into it. Anything you don't put in your returned
|
|
89
|
+
packet is lost when you exit.
|
|
90
|
+
|
|
91
|
+
## Mandatory grounding discipline
|
|
92
|
+
|
|
93
|
+
- Every claim requires a real citekey pulled from a `search()` result --
|
|
94
|
+
never fabricate one, per AGENTS.md's invariant.
|
|
95
|
+
- Document genuine disagreement between sources rather than picking one.
|
|
96
|
+
- No fabricated citekeys, quotes, statistics, or attributions, ever.
|
|
97
|
+
|
|
98
|
+
## Required output (return this to the orchestrator, don't write a file)
|
|
99
|
+
|
|
100
|
+
Markdown containing:
|
|
101
|
+
|
|
102
|
+
- **Perspective name** and core position (2 sentences)
|
|
103
|
+
- **Key claims**, each with its citekey(s)
|
|
104
|
+
- **Unique insight** only this perspective's questions surfaced
|
|
105
|
+
- **Strongest evidence**, with its citekey
|
|
106
|
+
- **Open questions** this interview didn't resolve
|
|
107
|
+
- **Sources consulted**: the list of citekeys used, plus any citekeys that
|
|
108
|
+
came up in searches but were discarded during filtering. For each
|
|
109
|
+
discarded one, give **the query that surfaced it and one clause on why it
|
|
110
|
+
did not hold up**. The orchestrator copies these straight into the
|
|
111
|
+
dossier's `rejected.md`, and a row missing either field cannot do that
|
|
112
|
+
file's job -- stopping the next revision re-retrieving and re-judging the
|
|
113
|
+
same paper
|
|
114
|
+
|
|
115
|
+
Your output is an **internal packet for the orchestrator**, not
|
|
116
|
+
reader-facing prose. Optimize it for the orchestrator's later use --
|
|
117
|
+
specific, complete, every claim attached to its citekey -- rather than for
|
|
118
|
+
polish. Don't spend effort on flow or transitions; `docs/WRITING-STANDARDS.md`
|
|
119
|
+
(and its "Sources and attribution") governs the assembled report in Phase 6,
|
|
120
|
+
not this packet.
|
|
121
|
+
|
|
122
|
+
One standard does apply here, because it can't be repaired downstream: **be
|
|
123
|
+
specific about what you didn't find.** "No appropriate answer can be
|
|
124
|
+
formulated from this corpus" is a valid output, but "searched X, Y and Z
|
|
125
|
+
wordings; the corpus covers A but nothing on B" is far more useful to the
|
|
126
|
+
orchestrator, which has to decide whether the gap is real or a retrieval
|
|
127
|
+
artifact.
|
|
128
|
+
|
|
129
|
+
No local-to-global citation renumbering is needed (unlike the original
|
|
130
|
+
claude-storm protocol) -- citekeys are already the project-wide stable
|
|
131
|
+
identifier; see `.claude/skills/deep-research/reference.md` §4.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deep-research-writer
|
|
3
|
+
description: Section writer for the deep-research skill's Phase 5. Writes one self-contained, cited section from pre-vetted citekeys, never inventing a source. Dispatched in parallel, one per outline section, by .claude/skills/deep-research/SKILL.md -- not meant to be invoked directly by a user request.
|
|
4
|
+
tools: Bash, Read, Grep, Glob
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# deep-research-writer
|
|
8
|
+
|
|
9
|
+
Adapted from [hadufer/claude-storm](https://github.com/hadufer/claude-storm)'s
|
|
10
|
+
`agents/storm-writer.md` (MIT License) -- a section writer, retooled here to
|
|
11
|
+
cite only real citekeys from this project's corpus instead of URLs.
|
|
12
|
+
|
|
13
|
+
## System role
|
|
14
|
+
|
|
15
|
+
A specialized section writer for the `deep-research` skill. Produces one
|
|
16
|
+
self-contained section of the final report from pre-vetted source material.
|
|
17
|
+
|
|
18
|
+
## Input (given by the orchestrating skill)
|
|
19
|
+
|
|
20
|
+
- `TOPIC`
|
|
21
|
+
- `READER` -- one sentence naming who this report is for
|
|
22
|
+
- `GLOSSARY` -- recurring terms with the definitions every section must use;
|
|
23
|
+
these are fixed, not suggestions. If you need a term that isn't in it, use
|
|
24
|
+
it consistently and report it in your `### Sources added` block so the
|
|
25
|
+
orchestrator can reconcile.
|
|
26
|
+
- `SECTION` -- the outline fragment (heading + subheadings) this section
|
|
27
|
+
must cover
|
|
28
|
+
- **A command that prints your evidence**, rather than the evidence
|
|
29
|
+
itself:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python -m chitragupta.draft dossier brief <draft path> --section "<your heading>"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Run it first, before writing anything. It prints one block per citekey
|
|
36
|
+
the orchestrator assigned to your section -- the supporting facts and
|
|
37
|
+
quotes already extracted during Phase 2, so you can cite without
|
|
38
|
+
re-deriving relevance from scratch. Reading them here rather than being
|
|
39
|
+
handed them pasted into this prompt is deliberate: the orchestrator
|
|
40
|
+
would pay five times as much to re-emit them, once per writer (see
|
|
41
|
+
`docs/TOKENS.md`).
|
|
42
|
+
|
|
43
|
+
If it exits non-zero, or warns that a citekey has no block, **say so in
|
|
44
|
+
your response and write only what the blocks you did get will support.**
|
|
45
|
+
A missing block means the run never transcribed that packet; the
|
|
46
|
+
material is gone, and no amount of confident prose recovers it. Do not
|
|
47
|
+
fill the gap from general knowledge -- an ungrounded paragraph is
|
|
48
|
+
indistinguishable from a grounded one to everything downstream of you.
|
|
49
|
+
You may re-search instead, as below, and report what you found.
|
|
50
|
+
|
|
51
|
+
## Writing standards
|
|
52
|
+
|
|
53
|
+
- Cover every subheading in logical sequence.
|
|
54
|
+
- Support every sentence with an inline `[@citekey]` citation using the
|
|
55
|
+
citekeys your brief printed.
|
|
56
|
+
- Neutral, encyclopedic tone -- no personal voice, no unsupported
|
|
57
|
+
conclusions.
|
|
58
|
+
- Prefer specific facts, figures, and named entities from the source
|
|
59
|
+
material over vague summary.
|
|
60
|
+
- Short sentences, one idea each. Active voice with a named actor ("the
|
|
61
|
+
scheduler discards the packet", not "the packet is discarded").
|
|
62
|
+
- Lead each paragraph with its point -- a reader skimming first sentences
|
|
63
|
+
should still get the section's argument.
|
|
64
|
+
- Use `GLOSSARY` terms exactly as defined; expand an acronym at first use in
|
|
65
|
+
your section, then use the acronym.
|
|
66
|
+
- Never write "obviously", "simply", "just", "clearly", or "of course". In an
|
|
67
|
+
encyclopedic register these words add nothing and usually mark a claim
|
|
68
|
+
that's carrying less evidence than it sounds like.
|
|
69
|
+
- State a limitation plainly rather than hedging around it. "The corpus
|
|
70
|
+
covers X only for single-node deployments" beats "it may perhaps be the
|
|
71
|
+
case that coverage is somewhat limited".
|
|
72
|
+
|
|
73
|
+
See `docs/WRITING-STANDARDS.md` for the full set, and its "Sources and
|
|
74
|
+
attribution" section for the works these rules derive from (Diátaxis; Last,
|
|
75
|
+
*Technical Writing Essentials*; Google's Technical Writing courses). The
|
|
76
|
+
above is what matters most for a section written in parallel with others.
|
|
77
|
+
|
|
78
|
+
## Citation protocol
|
|
79
|
+
|
|
80
|
+
- Use only the citekeys your brief printed, or a new one you find yourself
|
|
81
|
+
(see below) -- **never invent a citekey**.
|
|
82
|
+
- No separate references list in your output -- the orchestrator assembles
|
|
83
|
+
the final References section from every citekey used across all sections.
|
|
84
|
+
|
|
85
|
+
## If a subpoint is thin
|
|
86
|
+
|
|
87
|
+
You may re-search this project's corpus for a subpoint that needs more than
|
|
88
|
+
what you were given:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
python -m chitragupta.draft retrieve search "<query>" --k 15 --log <the draft path you were given>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
(or `chitragupta.enrich.embed_index.search()` if `content/chroma/` exists). Filter
|
|
95
|
+
what comes back the same way the interviewers do -- read the snippet and
|
|
96
|
+
judge relevance yourself, don't just take the top hit. Where a snippet is
|
|
97
|
+
not enough to decide on a source you mean to cite, read more of that one
|
|
98
|
+
document with:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
python -m chitragupta.draft retrieve evidence "<query>" --citekey <key> \
|
|
102
|
+
--log <draft path>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Report any citekey you used this
|
|
106
|
+
way in a trailing `### Sources added` block so the orchestrator can include
|
|
107
|
+
it in the final references.
|
|
108
|
+
|
|
109
|
+
Report what you turned down too, in a `### Candidates discarded` block --
|
|
110
|
+
citekey, the query that surfaced it, and one clause on why it didn't hold
|
|
111
|
+
up. A candidate you rejected is the most expensive thing in your context
|
|
112
|
+
to reconstruct later, and the orchestrator cannot see it unless you say
|
|
113
|
+
so. If you didn't re-search, omit both blocks.
|
|
114
|
+
|
|
115
|
+
## The corpus is read-only, and you don't own any file
|
|
116
|
+
|
|
117
|
+
Never run `python -m chitragupta.corpus sync`, `python -m chitragupta.enrich`,
|
|
118
|
+
or any `chitragupta/enrich/*`
|
|
119
|
+
build stage. Both take the pipeline's write lock and can run for tens of
|
|
120
|
+
minutes, and several of you run in parallel. Use `content/chroma/` only if
|
|
121
|
+
it already exists; if it doesn't, fall back to `chitragupta.retrieval.search()`
|
|
122
|
+
and
|
|
123
|
+
say so in your packet -- do not build one.
|
|
124
|
+
|
|
125
|
+
You write no files at all. In particular you never write into
|
|
126
|
+
`content/dossiers/` -- the orchestrating run owns the dossier and
|
|
127
|
+
transcribes your packet into it. *Reading* it, which is what your brief
|
|
128
|
+
does, is the point; writing it would mean the dossier had several authors
|
|
129
|
+
and no single trustworthy record. Anything you don't put in your returned
|
|
130
|
+
packet is lost when you exit.
|
|
131
|
+
|
|
132
|
+
## Output format
|
|
133
|
+
|
|
134
|
+
Markdown section starting with the heading (`##`), subsections as `###`,
|
|
135
|
+
inline `[@citekey]` citations, optionally ending with `### Sources added`
|
|
136
|
+
and `### Candidates discarded` blocks if you re-searched. Return this as
|
|
137
|
+
your response -- don't write it to a file yourself; the orchestrator
|
|
138
|
+
assembles the full document.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: peer-reviewer
|
|
3
|
+
description: One independent voice in a multi-reviewer critique panel (domain-accuracy, methodology-rigor, clarity-completeness, or devils-advocate). Dispatched in parallel, one per role, by .claude/skills/deep-research/SKILL.md's Phase 7 -- not meant to be invoked directly by a user request.
|
|
4
|
+
tools: Bash, Read, Grep, Glob
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# peer-reviewer
|
|
8
|
+
|
|
9
|
+
One independent voice in a multi-reviewer critique panel. **Idea credited
|
|
10
|
+
to [Imbad0202/academic-research-skills](https://github.com/Imbad0202/academic-research-skills)'s
|
|
11
|
+
Stage-3 peer-review design** (an Editor-in-Chief plus several independent
|
|
12
|
+
reviewers and a Devil's Advocate) -- **the design below is written from
|
|
13
|
+
scratch in this project's own words; no text from that repository
|
|
14
|
+
(CC-BY-NC 4.0) has been copied.** See the README's Acknowledgements section
|
|
15
|
+
and `.claude/skills/deep-research/reference.md` §7 for the full protocol
|
|
16
|
+
this agent is one piece of.
|
|
17
|
+
|
|
18
|
+
## Why independent, not sequential
|
|
19
|
+
|
|
20
|
+
You critique the draft **without seeing any other reviewer's critique**.
|
|
21
|
+
This is deliberate: if reviewers see each other's notes first, disagreement
|
|
22
|
+
gets smoothed over before the orchestrating skill (acting as
|
|
23
|
+
Editor-in-Chief) ever sees it. Independence is what makes a panel worth
|
|
24
|
+
more than one self-critique pass.
|
|
25
|
+
|
|
26
|
+
## Input (given by the orchestrating skill)
|
|
27
|
+
|
|
28
|
+
- `DRAFT` -- the full text under review (or a section, if reviewing
|
|
29
|
+
incrementally)
|
|
30
|
+
- `DRAFT PATH` -- `content/drafts/<slug>.md`, needed only to pass to
|
|
31
|
+
`--log` below; you never read or write it
|
|
32
|
+
- `ROLE` -- exactly one of:
|
|
33
|
+
|
|
34
|
+
- **domain-accuracy** -- for every claim that carries a citation, re-read
|
|
35
|
+
the actual cited source (`Read` on `content/parsed/<citekey>.txt` /
|
|
36
|
+
`content/docling/<citekey>.md` if it exists, or a fresh
|
|
37
|
+
`python -m chitragupta.draft retrieve search "<query>" --k 15 --log <DRAFT
|
|
38
|
+
PATH>` /
|
|
39
|
+
`chitragupta.enrich.embed_index.search()` call -- pass `--log` on every
|
|
40
|
+
retrieval call, same as every other dispatch site, so this role's
|
|
41
|
+
reads are measured too) and check whether the source actually supports
|
|
42
|
+
what's claimed. **This is
|
|
43
|
+
the one check neither this project's `citation_gate.py` nor
|
|
44
|
+
academic-research-skills' external-database citation triangulation
|
|
45
|
+
performs** -- both verify a citekey *exists*, not that the claim
|
|
46
|
+
attributed to it is *accurate*. Flag every mismatch: the citekey, the
|
|
47
|
+
claim, and what the source actually says instead.
|
|
48
|
+
- **methodology-rigor** -- does the argument structure hold together? Do
|
|
49
|
+
conclusions overreach what the cited evidence actually supports? Is the
|
|
50
|
+
contradiction-map/synthesis logic internally consistent?
|
|
51
|
+
- **clarity-completeness** -- is the writing clear and well organized?
|
|
52
|
+
Are there unaddressed gaps -- thin-coverage areas that went unflagged,
|
|
53
|
+
or claims that clearly need a citation but don't have one? Check
|
|
54
|
+
against `docs/WRITING-STANDARDS.md` -- whose rules derive from
|
|
55
|
+
Diátaxis, Last's *Technical Writing Essentials* and Google's Technical
|
|
56
|
+
Writing courses, credited in that file's "Sources and attribution" --
|
|
57
|
+
specifically for:
|
|
58
|
+
- a term used before it's defined, or defined twice differently
|
|
59
|
+
- one concept under two names, or one name covering two concepts
|
|
60
|
+
- an acronym expanded more than once, or dropped back to long form
|
|
61
|
+
- notation or terminology that shifts between sections (the
|
|
62
|
+
characteristic seam of parallel section writers)
|
|
63
|
+
- "obviously" / "simply" / "just" / "clearly" -- each one flags a
|
|
64
|
+
sentence to re-examine, not just a word to delete
|
|
65
|
+
- a paragraph whose first sentence doesn't carry its point
|
|
66
|
+
- a missing scope statement: does the reader learn what the document
|
|
67
|
+
does *not* cover?
|
|
68
|
+
- hedged prose that conveys no actual uncertainty
|
|
69
|
+
Report these as `severity: low` individually, but if several cluster
|
|
70
|
+
in one section, raise that section as `severity: medium` -- the
|
|
71
|
+
pattern matters more than any single instance.
|
|
72
|
+
- **devils-advocate** -- argue against the draft's central claims and
|
|
73
|
+
conclusions as strongly as the evidence allows. Find the single
|
|
74
|
+
strongest case that the main conclusion is wrong, overstated, or
|
|
75
|
+
resting on weak support. Not a style nitpick pass -- attack the
|
|
76
|
+
substance.
|
|
77
|
+
|
|
78
|
+
## The corpus is read-only, and you don't own any file
|
|
79
|
+
|
|
80
|
+
Never run `python -m chitragupta.corpus sync`, `python -m chitragupta.enrich`,
|
|
81
|
+
or any `chitragupta/enrich/*`
|
|
82
|
+
build stage. Both take the pipeline's write lock and can run for tens of
|
|
83
|
+
minutes, and several of you run in parallel. Use `content/chroma/` only if
|
|
84
|
+
it already exists; if it doesn't, fall back to
|
|
85
|
+
`python -m chitragupta.draft retrieve search "<query>" --k 15 --log <DRAFT
|
|
86
|
+
PATH>` and
|
|
87
|
+
say so in your packet -- do not build one.
|
|
88
|
+
|
|
89
|
+
You write no files at all. In particular you never write into
|
|
90
|
+
`content/dossiers/` -- the orchestrating run owns the dossier and
|
|
91
|
+
transcribes your packet into it. Anything you don't put in your returned
|
|
92
|
+
packet is lost when you exit.
|
|
93
|
+
|
|
94
|
+
## Output (return this as your response, don't write a file)
|
|
95
|
+
|
|
96
|
+
For your assigned `ROLE`:
|
|
97
|
+
|
|
98
|
+
- **Concerns**, each tagged `severity: high | medium | low`, quoting the
|
|
99
|
+
specific text/claim/citekey it applies to
|
|
100
|
+
- **What's solid**, briefly -- so the orchestrator can tell "reviewed and
|
|
101
|
+
fine" apart from "not looked at"
|
|
102
|
+
- A **verdict**: `ready` / `needs revision` / `reject`, from your role's
|
|
103
|
+
perspective alone. Don't hedge toward consensus -- the orchestrator
|
|
104
|
+
reconciles all four verdicts against the concession-threshold rule in
|
|
105
|
+
`reference.md` §7.
|
|
106
|
+
|
|
107
|
+
## Ground rules
|
|
108
|
+
|
|
109
|
+
- Show your work for `domain-accuracy` specifically -- quote the source
|
|
110
|
+
snippet you checked the claim against, don't just assert a mismatch.
|
|
111
|
+
- A citation existing in the ledger doesn't mean the claim is accurate;
|
|
112
|
+
don't let `citation_gate.py` having passed substitute for actually
|
|
113
|
+
reading the source.
|
|
114
|
+
- Stay inside your assigned role. Four narrow, independent reviews surface
|
|
115
|
+
more than one reviewer trying to cover everything at once.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PostToolUse hook: enforce the citekey gate on genre-skill drafts.
|
|
3
|
+
|
|
4
|
+
AGENTS.md calls python -m chitragupta.draft gate "a gate, not a lint
|
|
5
|
+
suggestion" and every genre skill's prose instructs the agent to run it
|
|
6
|
+
before presenting a draft -- but until this hook existed, nothing
|
|
7
|
+
mechanically enforced that instruction; an agent could just skip the
|
|
8
|
+
step. This makes it enforced by the harness: every Write/Edit under
|
|
9
|
+
content/drafts/ (.md from survey-writer/textbook-chapter-writer/
|
|
10
|
+
tutorial-writer/deep-research,
|
|
11
|
+
.tex from thesis-chapter-writer -- see each SKILL.md's "Save the
|
|
12
|
+
draft/fragment as content/drafts/<slug>.{md,tex}" step) is gated
|
|
13
|
+
automatically, and a failure is surfaced back to Claude as blocking
|
|
14
|
+
feedback (not a silent/advisory warning).
|
|
15
|
+
|
|
16
|
+
**The gate class, and the only member of it.** This is the one hook in
|
|
17
|
+
this repository permitted to block, because it is the only one measured
|
|
18
|
+
against ground truth: a citekey is in the ledger or it is not, and no
|
|
19
|
+
state of the world makes an absent one legitimately present.
|
|
20
|
+
docs/HOOKS.md has the axis that decides which checks may block, and
|
|
21
|
+
SOUL.md has why there is exactly one.
|
|
22
|
+
|
|
23
|
+
Deciding whether a write was a draft at all now lives in
|
|
24
|
+
`draft_target.py`, shared with the advisory hook beside it. The
|
|
25
|
+
subtleties it holds -- a relative `file_path`, the repo root taken from
|
|
26
|
+
the hook's own location, `is_relative_to` containment rather than a
|
|
27
|
+
substring match, the two suffixes this pipeline writes -- are recorded
|
|
28
|
+
there, each learned from a real near-miss. What is left in this file is
|
|
29
|
+
the gate and nothing else.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import subprocess
|
|
34
|
+
import sys
|
|
35
|
+
|
|
36
|
+
import draft_target
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main() -> int:
|
|
40
|
+
file_path = draft_target.from_stdin(sys.stdin)
|
|
41
|
+
if file_path is None:
|
|
42
|
+
return 0 # not a genre-skill draft -- nothing to gate
|
|
43
|
+
|
|
44
|
+
# sys.executable, not a bare "python"/"python3". This hook is
|
|
45
|
+
# the gate's only automatic enforcement point, and an interpreter name
|
|
46
|
+
# that does not resolve raises FileNotFoundError here -- which exits
|
|
47
|
+
# non-zero *without* the block, so the draft lands ungated. A hard
|
|
48
|
+
# gate that degrades to advisory depending on whether a host has
|
|
49
|
+
# `python` as well as `python3` is the worst of the available failure
|
|
50
|
+
# modes. The interpreter already running this hook is known to exist
|
|
51
|
+
# and is the one settings.json chose.
|
|
52
|
+
result = subprocess.run(
|
|
53
|
+
[sys.executable, "-m", "chitragupta.draft", "gate", str(file_path)],
|
|
54
|
+
check=False, cwd=draft_target.REPO_ROOT,
|
|
55
|
+
capture_output=True,
|
|
56
|
+
text=True,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if result.returncode != 0:
|
|
60
|
+
reason = (
|
|
61
|
+
"Citation gate FAILED for this draft (AGENTS.md: a hard gate, not "
|
|
62
|
+
"advisory). Fix the offending citekey(s) -- correct the key or "
|
|
63
|
+
"remove the claim -- then this file will be re-checked "
|
|
64
|
+
"automatically on your next write to it.\n\n"
|
|
65
|
+
f"{result.stdout}{result.stderr}"
|
|
66
|
+
)
|
|
67
|
+
print(json.dumps({"decision": "block", "reason": reason}))
|
|
68
|
+
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Was this write a draft? -- the one decision both PostToolUse hooks share.
|
|
2
|
+
|
|
3
|
+
`citation_gate_hook.py` gates a draft and `style_check_hook.py` checks its
|
|
4
|
+
prose, and they differ only in what they do once they know a draft was
|
|
5
|
+
written. This module is that "once they know", factored out rather than
|
|
6
|
+
copied, because two hooks disagreeing about which writes they cover is a
|
|
7
|
+
worse bug than either could have alone -- and it is the bug a copied forty
|
|
8
|
+
lines produces the first time one copy is fixed. docs/HOOKS.md argues the
|
|
9
|
+
fault-isolation objection to sharing anything between a gate and a
|
|
10
|
+
non-gate: what must not be shared is the *failure* of a check, which
|
|
11
|
+
separate processes guarantee, and what must be shared is the *definition
|
|
12
|
+
of a draft*.
|
|
13
|
+
|
|
14
|
+
Every part below was learned from a real near-miss, and is recorded here
|
|
15
|
+
because none of it is guessable from the payload:
|
|
16
|
+
|
|
17
|
+
- **`file_path` may be relative.** Claude Code's Write/Edit tools document
|
|
18
|
+
it as absolute and it has always been so in practice, but a substring
|
|
19
|
+
match on "/content/drafts/" would silently skip a relative
|
|
20
|
+
"content/drafts/<slug>.md" -- no leading slash to match, no error, just
|
|
21
|
+
an ungated draft. So a relative path is resolved rather than ignored.
|
|
22
|
+
- **The repo root comes from this file's own location**, never from the
|
|
23
|
+
target path and never from the working directory. A hook is run from
|
|
24
|
+
wherever the harness happens to be.
|
|
25
|
+
- **Containment is `is_relative_to` on resolved paths**, not a string
|
|
26
|
+
test, so `content/drafts/../../etc/passwd` cannot pass for a draft.
|
|
27
|
+
- **The suffix must be one this pipeline writes.** `.md` from the four
|
|
28
|
+
Markdown genres and the revisers, `.tex` from thesis-chapter-writer.
|
|
29
|
+
|
|
30
|
+
Malformed stdin fails open in all three shapes that have been hit --
|
|
31
|
+
invalid JSON, valid JSON that is not an object, and a `tool_input` that is
|
|
32
|
+
not a dict. Each means "no file path was given", and a hook that raises
|
|
33
|
+
there is a hook that stops reporting.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import json
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
DRAFT_EXTENSIONS = (".md", ".tex")
|
|
42
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def from_stdin(stream, repo_root: Path | None = None) -> Path | None:
|
|
46
|
+
"""The draft this PostToolUse payload wrote, or None for anything else.
|
|
47
|
+
|
|
48
|
+
None covers every "not our business" case -- unparseable stdin, a
|
|
49
|
+
payload of the wrong shape, no file path, a write outside
|
|
50
|
+
`content/drafts/`, and a suffix this pipeline does not produce. A hook
|
|
51
|
+
that gets None returns 0 and says nothing.
|
|
52
|
+
|
|
53
|
+
`repo_root` exists for the tests, which relocate a copy of a hook into
|
|
54
|
+
a temporary tree; production callers pass nothing and get this file's
|
|
55
|
+
own location, which is the point.
|
|
56
|
+
"""
|
|
57
|
+
return target(_file_path(stream), repo_root)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _file_path(stream) -> str:
|
|
61
|
+
"""The `tool_input.file_path` in this payload, or "" if there isn't one."""
|
|
62
|
+
try:
|
|
63
|
+
payload = json.load(stream)
|
|
64
|
+
except (json.JSONDecodeError, ValueError, UnicodeDecodeError):
|
|
65
|
+
return "" # can't identify a target file from this -- fail open, not loud
|
|
66
|
+
if not isinstance(payload, dict):
|
|
67
|
+
return "" # valid JSON (a bare array, string, number) but not the shape
|
|
68
|
+
tool_input = payload.get("tool_input")
|
|
69
|
+
if not isinstance(tool_input, dict):
|
|
70
|
+
return "" # missing, null, or the wrong shape -- same as "no file_path"
|
|
71
|
+
raw = tool_input.get("file_path", "")
|
|
72
|
+
return raw if isinstance(raw, str) else ""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def target(raw_path: str, repo_root: Path | None = None) -> Path | None:
|
|
76
|
+
"""The resolved draft `raw_path` names, or None if it is not one.
|
|
77
|
+
|
|
78
|
+
Split from `from_stdin` so a caller that already has a path -- a test,
|
|
79
|
+
or a hook reading the payload for something else too -- can ask the
|
|
80
|
+
same question without building a JSON document to ask it with.
|
|
81
|
+
"""
|
|
82
|
+
if not raw_path:
|
|
83
|
+
return None
|
|
84
|
+
try:
|
|
85
|
+
root = (repo_root or REPO_ROOT).resolve()
|
|
86
|
+
path = Path(raw_path)
|
|
87
|
+
if not path.is_absolute():
|
|
88
|
+
path = root / path
|
|
89
|
+
path = path.resolve()
|
|
90
|
+
drafts = (root / "content" / "drafts").resolve()
|
|
91
|
+
inside = path.is_relative_to(drafts)
|
|
92
|
+
except (OSError, ValueError):
|
|
93
|
+
# A path this platform will not construct. The observed case is an
|
|
94
|
+
# embedded null byte, which raises ValueError from resolve();
|
|
95
|
+
# OSError is guarded for the platforms where resolution touches the
|
|
96
|
+
# filesystem, though on Linux it does not -- `resolve()` does not
|
|
97
|
+
# stat, so even a path past the length limit gets this far. Neither
|
|
98
|
+
# can be a draft this pipeline wrote.
|
|
99
|
+
#
|
|
100
|
+
# This is caught rather than left to propagate because the caller
|
|
101
|
+
# that matters has no catch-all: citation_gate_hook's `main` runs
|
|
102
|
+
# straight off `raise SystemExit(main())`, so an exception here
|
|
103
|
+
# would exit non-zero *without* the blocking decision, and the
|
|
104
|
+
# write would land ungated. A hook that crashes on a malformed
|
|
105
|
+
# payload is a gate that stops being one.
|
|
106
|
+
return None
|
|
107
|
+
if not inside or path.suffix not in DRAFT_EXTENSIONS:
|
|
108
|
+
return None
|
|
109
|
+
return path
|