Skip to content

Commit c179ae1

Browse files
authored
Merge pull request #2 from jet52/claude/repackage-skill-upload-EYBkb
Claude/repackage skill for checking appellate briefs against ND rules
2 parents 7bb151c + 88b4e75 commit c179ae1

6 files changed

Lines changed: 617 additions & 17 deletions

File tree

README.md

Lines changed: 156 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,161 @@
22

33
Checks appellate brief PDFs for compliance with the North Dakota Rules of Appellate Procedure and produces an HTML compliance report with a recommended action: **Accept**, **Correction Letter**, or **Reject**.
44

5-
## Quick Start
5+
---
6+
7+
## Installing the Skill in Claude (Browser)
8+
9+
This section walks you through adding the Brief Compliance Checker to your Claude account so you can use it directly in a browser chat session. No programming knowledge is required.
10+
11+
### What You Need Before You Start
12+
13+
- A Claude account at [claude.ai](https://claude.ai) with a Pro, Team, or Enterprise plan (the skill requires the ability to upload files and use projects).
14+
- The **`brief-compliance.zip`** file from this repository. You can download it from the repository's file list by clicking on `brief-compliance.zip` and then clicking the download button.
15+
16+
### Step-by-Step Installation
17+
18+
#### 1. Open Claude and Create a New Project
19+
20+
1. Go to [claude.ai](https://claude.ai) and sign in.
21+
2. In the left sidebar, click **Projects**.
22+
3. Click **Create Project** (or the **+** button).
23+
4. Give your project a name, such as "Brief Compliance Checker".
24+
5. Click **Create**.
25+
26+
#### 2. Upload the Skill Files to Project Knowledge
27+
28+
> **Important — upload to the *project*, not to a chat.** Files uploaded to Project Knowledge are available in every chat you open inside that project. If you instead drag a file into a regular chat window, it only exists in that single conversation and disappears when you start a new one. Make sure you are adding the file through the project settings, not the chat input box.
29+
30+
1. Inside your new project, look for the **Project Knowledge** section (sometimes labeled "Project files" or accessible via a paperclip/attachment icon in the project settings).
31+
2. Click **Upload** or **Add files**.
32+
3. Select the **`brief-compliance.zip`** file you downloaded earlier.
33+
4. Wait for the upload to finish. Claude will unpack and index the contents automatically.
34+
35+
The ZIP file contains everything the skill needs: the analysis scripts, the bundled North Dakota appellate rules, check definitions, and the skill instructions.
36+
37+
#### 3. Set the Project Instructions
38+
39+
1. In your project settings, find the **Custom Instructions** field (also called "System prompt" or "Project instructions").
40+
2. Open the file `SKILL.md` from this repository (or extract it from the ZIP). Copy its entire contents.
41+
3. Paste the contents into the Custom Instructions field.
42+
4. Save the project settings.
43+
44+
These instructions tell Claude how to run the compliance analysis step by step whenever you upload a brief.
45+
46+
#### 4. Verify the Installation
47+
48+
1. Open a new chat inside the project.
49+
2. Type: **"Are you ready to check a brief for compliance?"**
50+
3. Claude should respond confirming it can analyze appellate briefs against the North Dakota Rules.
51+
52+
If Claude does not seem to recognize the skill, double-check that:
53+
- The ZIP file was uploaded to the project (not just to a regular chat).
54+
- The SKILL.md contents were pasted into the project's Custom Instructions.
55+
56+
### A Note on Scope
57+
58+
The skill lives inside the project you created — it is not available in other projects or in regular (non-project) chats. Claude does not currently offer a way to install a skill globally across your entire account.
59+
60+
In practice this isn't a limitation: just **open all your brief-checking chats inside this one project**. You can create as many chats as you like within a project, and every one of them will have access to the skill files and instructions automatically. Think of the project as a dedicated "Brief Compliance Checker" app that's always ready when you need it.
61+
62+
---
63+
64+
## Using the Skill
65+
66+
Once installed, using the skill is straightforward. You upload a PDF of an appellate brief, and Claude produces a detailed compliance report.
67+
68+
### Checking a Brief
69+
70+
1. **Open a chat** inside your Brief Compliance Checker project.
71+
2. **Drag and drop** your brief PDF into the chat window (or click the attachment/paperclip icon and select the file).
72+
3. **Tell Claude what to do.** You can simply say:
73+
74+
> "Check this brief for compliance."
75+
76+
Or be more specific if you know the brief type:
77+
78+
> "Check this appellant brief for compliance."
79+
>
80+
> "This is an appellee brief. Please run a compliance check."
81+
>
82+
> "Check this reply brief."
83+
84+
4. **Wait for the analysis.** Claude will work through several phases automatically:
85+
- **Extraction** — reads the PDF and measures formatting (paper size, margins, fonts, spacing, page count).
86+
- **Mechanical checks** — compares measurements against Rule 32 requirements.
87+
- **Semantic checks** — reads the brief text and evaluates whether required sections are present and adequate (Table of Contents, Statement of Issues, Argument, etc.).
88+
- **Report generation** — combines all results into an HTML compliance report.
89+
90+
5. **Review the results.** Claude will:
91+
- State the **recommended action**: Accept, Correction Letter, or Reject.
92+
- Summarize any **failed checks**, grouped by severity.
93+
- Provide a **downloadable HTML report** with full details.
94+
95+
### Understanding the Report
96+
97+
The HTML report has several sections:
98+
99+
- **Recommended Action** — a color-coded banner at the top:
100+
- **Green (Accept)** — the brief appears to comply with all rules.
101+
- **Yellow (Correction Letter)** — there are formatting issues that should be corrected, but the brief is not rejected outright.
102+
- **Red (Reject)** — there are serious compliance failures that warrant rejection.
103+
104+
- **Failed Checks** — grouped into three severity levels:
105+
- **Critical (Reject)** — violations that alone justify rejection (e.g., wrong paper size, font too small, over the page limit).
106+
- **Correction Required** — problems that should be fixed but don't rise to rejection level (e.g., margin too narrow, missing paragraph numbering).
107+
- **Advisory Notes** — minor issues or observations (e.g., oral argument notation not found, font style question).
108+
109+
- **Passed Checks** — an expandable section listing everything that passed. Click to expand.
110+
111+
- **Not Applicable** — checks that don't apply to this brief type (e.g., amicus-specific checks on an appellant brief).
112+
113+
Each failed check shows:
114+
- A **check ID** (e.g., FMT-006) for reference.
115+
- The **rule citation** (e.g., Rule 32(a)(5)), linked to the official rule text on ndcourts.gov.
116+
- A **message** explaining what was found.
117+
- **Details** with specifics — for font size issues, this includes a per-page breakdown showing which pages have undersized text and how many characters are affected.
118+
119+
### Tips for Best Results
120+
121+
- **Specify the brief type** if you know it. Auto-detection works for most appellant briefs but sometimes misidentifies appellee and reply briefs. Telling Claude the type up front avoids this.
122+
123+
- **Known false positives to watch for:**
124+
- **Font size (FMT-006)**: Page numbers, footnote markers, and superscripts are often smaller than 12pt. The report now categorizes these separately (body text vs. header/footer vs. superscript) so you can see whether the issue is real body text or just incidental small characters. If the only noncompliant characters on a page are in the header/footer or superscript categories, the severity is downgraded to a note rather than a rejection.
125+
- **Line spacing (FMT-009)**: The spacing detector can misread certain PDF encodings. If the brief was prepared in a standard word processor with double spacing selected, a spacing failure is likely a false positive.
126+
- **Bottom margin (FMT-005)**: Page numbers at the bottom of the page are measured as content, which makes the bottom margin appear smaller than it really is.
127+
128+
- **You can ask follow-up questions.** After the report is generated, you can ask Claude things like:
129+
- "Which pages have the font size issue?"
130+
- "Is the Table of Contents adequate?"
131+
- "What would need to be fixed for this brief to be accepted?"
132+
- "Can you re-check this as an appellee brief instead?"
133+
134+
- **You can check multiple briefs** in the same chat session. Just upload another PDF and ask Claude to check it.
135+
136+
### Brief Types Supported
137+
138+
| Brief Type | Description | Page Limit |
139+
|------------|-------------|------------|
140+
| Appellant | Opening brief filed by the appealing party | 38 pages |
141+
| Appellee | Response brief filed by the opposing party | 38 pages |
142+
| Reply | Reply to the appellee's brief | 12 pages |
143+
| Cross-Appeal | Brief when both parties appeal | 38 pages |
144+
| Amicus Curiae | "Friend of the court" brief | 19 pages |
145+
146+
### What Rules Are Checked
147+
148+
The checker evaluates compliance against these North Dakota rules:
149+
150+
- **Rule 28** — Required contents of briefs (sections, formatting of arguments, etc.)
151+
- **Rule 29** — Requirements for amicus curiae briefs
152+
- **Rule 30** — How to cite the record
153+
- **Rule 32** — Physical formatting (paper size, margins, fonts, spacing, page limits, cover requirements)
154+
- **Rule 34** — Oral argument notation on the cover
155+
- **Rule 3.4** — Privacy protection for personal identifiers in filings
156+
157+
---
158+
159+
## Developer Quick Start
6160

7161
```bash
8162
# Set up the virtual environment
@@ -27,7 +181,7 @@ python app.py
27181
- **`SKILL.md`** — Claude Code skill definition (deployed via symlink)
28182
- **`deploy_skill.py`** — Cross-platform script to deploy the skill to `~/.claude/skills/`
29183

30-
## Skill Deployment
184+
## Skill Deployment (Claude Code CLI)
31185

32186
The Claude Code skill (`/brief-compliance`) reads its files from `~/.claude/skills/brief-compliance/`. This repo is the single source of truth — `deploy_skill.py` copies the needed files into the skill directory.
33187

core/checks_mechanical.py

Lines changed: 124 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from collections import Counter
1212

1313
from core.constants import (
14+
FONT_NONCOMPLIANT_THRESHOLD,
1415
MARGIN_TOLERANCE,
1516
MAX_CHARS_PER_INCH,
1617
MIN_BOTTOM_MARGIN,
@@ -124,20 +125,7 @@ def _check_fonts(metadata: BriefMetadata) -> list[CheckResult]:
124125
results = []
125126

126127
# FMT-006: Font size >= 12pt — Rule 32(a)(5): "The typeface must be 12 point or larger"
127-
if metadata.min_font_size is not None and metadata.min_font_size < MIN_FONT_SIZE_PT - FONT_SIZE_TOLERANCE:
128-
results.append(CheckResult(
129-
check_id="FMT-006", name="Minimum Font Size", rule="32(a)(5)",
130-
passed=False, severity=Severity.REJECT,
131-
message=f"Font size {metadata.min_font_size:.1f}pt found; minimum is {MIN_FONT_SIZE_PT}pt.",
132-
details=f"Predominant font size: {metadata.predominant_font_size}pt. "
133-
f"Smallest detected: {metadata.min_font_size:.1f}pt.",
134-
))
135-
else:
136-
results.append(CheckResult(
137-
check_id="FMT-006", name="Minimum Font Size", rule="32(a)(5)",
138-
passed=True, severity=Severity.REJECT,
139-
message=f"Font size meets the {MIN_FONT_SIZE_PT}pt minimum.",
140-
))
128+
results.append(_check_font_size_per_page(metadata))
141129

142130
# FMT-007: Max 16 characters per inch — Rule 32(a)(5): "no more than 16 characters per inch"
143131
cpi_issues = _check_chars_per_inch(metadata)
@@ -174,6 +162,128 @@ def _check_fonts(metadata: BriefMetadata) -> list[CheckResult]:
174162
return results
175163

176164

165+
def _classify_font_span(font: dict, page_height_pts: float) -> str:
166+
"""Classify a font span as 'header_footer', 'superscript', or 'body'.
167+
168+
- header_footer: origin_y in top or bottom 10% of the page
169+
- superscript: PyMuPDF superscript flag (bit 0) set, or <=4 chars of
170+
digit-only text at small size (catches footnote markers / ordinal suffixes)
171+
- body: everything else
172+
"""
173+
origin_y = font.get("origin_y", page_height_pts / 2)
174+
top_zone = page_height_pts * 0.10
175+
bottom_zone = page_height_pts * 0.90
176+
177+
if origin_y <= top_zone or origin_y >= bottom_zone:
178+
return "header_footer"
179+
180+
flags = font.get("flags", 0)
181+
is_superscript = bool(flags & 1) # bit 0
182+
if is_superscript:
183+
return "superscript"
184+
185+
# Short digit-only text at small size — likely footnote marker or ordinal
186+
chars = font.get("chars", 0)
187+
if chars <= 4 and font["size"] < MIN_FONT_SIZE_PT - FONT_SIZE_TOLERANCE:
188+
return "superscript"
189+
190+
return "body"
191+
192+
193+
def _check_font_size_per_page(metadata: BriefMetadata) -> CheckResult:
194+
"""FMT-006: Font size >= 12pt with per-page detail and categorisation."""
195+
threshold = MIN_FONT_SIZE_PT - FONT_SIZE_TOLERANCE
196+
page_issues: list[dict] = [] # one entry per page that has noncompliant chars
197+
198+
for p in metadata.pages:
199+
if not p.fonts:
200+
continue
201+
202+
page_height_pts = p.height_inches * 72.0
203+
total_chars = 0
204+
nc_body = 0
205+
nc_hf = 0
206+
nc_super = 0
207+
min_size_on_page: float | None = None
208+
209+
for f in p.fonts:
210+
char_count = f.get("chars", 1)
211+
total_chars += char_count
212+
213+
if f["size"] < threshold:
214+
category = _classify_font_span(f, page_height_pts)
215+
if category == "header_footer":
216+
nc_hf += char_count
217+
elif category == "superscript":
218+
nc_super += char_count
219+
else:
220+
nc_body += char_count
221+
222+
if min_size_on_page is None or f["size"] < min_size_on_page:
223+
min_size_on_page = f["size"]
224+
225+
nc_total = nc_body + nc_hf + nc_super
226+
if nc_total > 0:
227+
page_issues.append({
228+
"page": p.page_number + 1,
229+
"total_chars": total_chars,
230+
"nc_total": nc_total,
231+
"nc_body": nc_body,
232+
"nc_hf": nc_hf,
233+
"nc_super": nc_super,
234+
"min_size": min_size_on_page,
235+
})
236+
237+
if not page_issues:
238+
return CheckResult(
239+
check_id="FMT-006", name="Minimum Font Size", rule="32(a)(5)",
240+
passed=True, severity=Severity.REJECT,
241+
message=f"Font size meets the {MIN_FONT_SIZE_PT}pt minimum.",
242+
)
243+
244+
# Determine severity: REJECT if any page has >= threshold count, else NOTE
245+
any_serious = any(pi["nc_total"] >= FONT_NONCOMPLIANT_THRESHOLD for pi in page_issues)
246+
severity = Severity.REJECT if any_serious else Severity.NOTE
247+
248+
global_min = min(pi["min_size"] for pi in page_issues)
249+
bad_page_nums = [pi["page"] for pi in page_issues]
250+
251+
page_label = "page" if len(bad_page_nums) == 1 else "pages"
252+
message = (
253+
f"Font size {global_min:.1f}pt found on {page_label} "
254+
f"{_page_list(bad_page_nums)}; minimum is {MIN_FONT_SIZE_PT}pt."
255+
)
256+
257+
# Build per-page breakdown
258+
lines = [
259+
f"Predominant font size: {metadata.predominant_font_size}pt. "
260+
f"Smallest detected: {global_min:.1f}pt.",
261+
"",
262+
"Per-page breakdown:",
263+
]
264+
for pi in page_issues:
265+
pct = pi["nc_total"] / pi["total_chars"] * 100 if pi["total_chars"] else 0
266+
parts = []
267+
if pi["nc_body"]:
268+
parts.append(f"{pi['nc_body']} body")
269+
if pi["nc_hf"]:
270+
parts.append(f"{pi['nc_hf']} header/footer")
271+
if pi["nc_super"]:
272+
parts.append(f"{pi['nc_super']} superscript")
273+
breakdown = ", ".join(parts)
274+
lines.append(
275+
f" Page {pi['page']}: {pi['nc_total']} of {pi['total_chars']:,} chars "
276+
f"({pct:.1f}%) noncompliant — {breakdown}"
277+
)
278+
279+
return CheckResult(
280+
check_id="FMT-006", name="Minimum Font Size", rule="32(a)(5)",
281+
passed=False, severity=severity,
282+
message=message,
283+
details="\n".join(lines),
284+
)
285+
286+
177287
def _check_chars_per_inch(metadata: BriefMetadata) -> str:
178288
"""Estimate characters per inch from page content."""
179289
# Use body pages only, skip cover

core/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
MIN_FONT_SIZE_PT = 12.0
1919
MAX_CHARS_PER_INCH = 16
2020
FONT_SIZE_TOLERANCE = 0.3 # pt tolerance for font size detection
21+
FONT_NONCOMPLIANT_THRESHOLD = 10 # chars per page: >= this count is REJECT, below is NOTE
2122

2223
# --- Spacing ---
2324
# Double spacing is ~24pt between baselines for 12pt text.

core/pdf_extract.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ def _extract_page(page: fitz.Page, page_idx: int) -> PageInfo:
105105
"name": span["font"],
106106
"size": span["size"],
107107
"flags": span["flags"], # bold/italic flags
108+
"chars": len(span["text"].strip()),
109+
"origin_y": span["origin"][1],
108110
})
109111

110112
# Margins: find bounding box of all content

core/report_builder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ def _css() -> str:
218218
.severity-correction { background: #fff8c5; color: #735c0f; }
219219
.severity-note { background: #f1f1f1; color: #586069; }
220220
.check-msg { margin-top: 0.4rem; font-size: 0.93rem; }
221-
.detail { margin-top: 0.3rem; font-size: 0.85rem; color: #586069; }
221+
.detail { margin-top: 0.3rem; font-size: 0.85rem; color: #586069; white-space: pre-line; }
222222
.checks-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
223223
.checks-table th, .checks-table td { padding: 0.4rem 0.6rem; border-bottom: 1px solid #e1e4e8; text-align: left; }
224224
.checks-table th { background: #f6f8fa; font-weight: 600; }

0 commit comments

Comments
 (0)