Skip to content

Commit 2628957

Browse files
committed
feat: complete eval checklist follow-ups and timeline tooling
1 parent d65eb31 commit 2628957

File tree

18 files changed

+832
-98
lines changed

18 files changed

+832
-98
lines changed

README.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ uspto search --reel-frame "012345/0001"
109109

110110
# Auto-paginate all results (up to 10,000)
111111
uspto search --examiner "RILEY" --all -f ndjson
112+
uspto search --assignee "Tesla" --granted --all -f csv > tesla_all.csv
112113

113114
# Count matches only (lightweight sizing call for agents)
114115
uspto search --assignee "Tesla" --granted-after 2023-01-01 --count-only -f json -q
@@ -127,6 +128,7 @@ uspto search --filter "applicationTypeLabelName=Utility" --facets applicationTyp
127128
- Uses `POST /search` when `--filter`, `--facets`, date ranges, or `--granted/--pending` are present.
128129
- Uses `GET /search` for simple query-only cases.
129130
- For `--download`, it uses `POST /search/download` when those advanced parameters are present; otherwise `GET /search/download`.
131+
- `--all -f csv` performs client-side page concatenation for CSV export UX (useful when you need paged search semantics instead of `--download csv`).
130132

131133
**All search flags:**
132134
`--title`, `--inventor`, `--assignee`, `--examiner`, `--applicant`, `--assignor`,
@@ -186,15 +188,29 @@ uspto app associated-docs <appNumber> # Associated XML document metadata
186188
uspto app download <appNumber> [index|documentIdentifier] # Download a specific document PDF
187189
uspto app download-all <appNumber> # Download all document PDFs
188190

189-
# Grant XML extraction (for granted patents)
191+
# Patent XML extraction (grant + pgpub fallback)
190192
uspto app abstract <appNumber> # Patent abstract
191193
uspto app claims <appNumber> # Structured claims text
192194
uspto app citations <appNumber> # Prior art citations
193195
uspto app description <appNumber> # Full specification text
194196
uspto app fulltext <appNumber> # Everything: meta + abstract + claims + citations + description
195197
```
196198

197-
The grant XML commands (`abstract`, `claims`, `citations`, `description`, `fulltext`) parse the official patent grant XML to extract structured data. `fulltext` is the most comprehensive single-command view of a granted patent.
199+
The XML commands (`abstract`, `claims`, `citations`, `description`, `fulltext`) parse official patent XML to extract structured data. They prefer grant XML and fall back to pgpub XML for pending applications when available. `fulltext` is the most comprehensive single-command view.
200+
For pending applications, these commands automatically fall back to pgpub XML when available.
201+
For older patents (especially pre-2010), citation completeness can vary depending on legacy XML structure and source data availability.
202+
203+
Document code filters (`app docs --codes`, `app dl --codes`, `app dl-all --codes`) support aliases:
204+
- `rejection` -> `CTNF,CTFR`
205+
- `allowance` -> `NOA`
206+
- `claims` -> `CLM`
207+
- `specification` / `spec` -> `SPEC`
208+
- `abstract` -> `ABST`
209+
- `drawings` -> `DRWR`
210+
- `ids` -> `IDS`
211+
212+
Assignment note:
213+
- `app assign` can legitimately return `[]` for direct-company filings where no post-filing assignment recordation exists in the assignment dataset.
198214

199215
### Compound Commands
200216

@@ -206,8 +222,14 @@ uspto summary 16123456
206222
# Recursive family tree (follows parent/child continuity chains)
207223
uspto family 16123456 --depth 3
208224
uspto family 16123456 --depth 3 --with-dates
225+
226+
# Prosecution timeline (metadata + transactions + key docs in one view)
227+
uspto prosecution-timeline 16123456
228+
uspto prosecution-timeline 16123456 --codes rejection,allowance,CLM -f json -q
209229
```
210230

231+
`family` JSON includes relationship-aware `allApplicationNumbers` entries so CON/DIV/CIP links are explicit in the flat member list.
232+
211233
### PTAB (Patent Trial and Appeal Board)
212234

213235
14 subcommands for trials, decisions, appeals, and interferences:
@@ -254,6 +276,8 @@ uspto petition search --facets decisionTypeCodeDescriptionText -f json -q
254276
uspto petition get <recordId> --include-documents
255277
```
256278

279+
Dataset note: decision search data is currently dominated by `DENIED` records; `--decision GRANTED` may return no results depending on dataset coverage.
280+
257281
### Bulk Data
258282

259283
```bash

cmd/app.go

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,58 @@ func filterEmpty(parts ...string) []string {
475475
return out
476476
}
477477

478+
var documentCodeAliases = map[string][]string{
479+
"rejection": {"CTNF", "CTFR"},
480+
"office-action": {"CTNF", "CTFR"},
481+
"non-final-rejection": {"CTNF"},
482+
"final-rejection": {"CTFR"},
483+
"allowance": {"NOA"},
484+
"notice-of-allowance": {"NOA"},
485+
"claims": {"CLM"},
486+
"specification": {"SPEC"},
487+
"spec": {"SPEC"},
488+
"abstract": {"ABST"},
489+
"drawings": {"DRWR"},
490+
"ids": {"IDS"},
491+
}
492+
493+
func normalizeDocumentCodes(raw string) string {
494+
raw = strings.TrimSpace(raw)
495+
if raw == "" {
496+
return ""
497+
}
498+
499+
var out []string
500+
seen := make(map[string]bool)
501+
502+
tokens := strings.Split(raw, ",")
503+
for _, tok := range tokens {
504+
t := strings.TrimSpace(tok)
505+
if t == "" {
506+
continue
507+
}
508+
key := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(t, "_", "-"), " ", "-"))
509+
if expanded, ok := documentCodeAliases[key]; ok {
510+
for _, code := range expanded {
511+
c := strings.ToUpper(strings.TrimSpace(code))
512+
if c != "" && !seen[c] {
513+
seen[c] = true
514+
out = append(out, c)
515+
}
516+
}
517+
continue
518+
}
519+
520+
c := strings.ToUpper(t)
521+
if !seen[c] {
522+
seen[c] = true
523+
out = append(out, c)
524+
}
525+
}
526+
527+
return strings.Join(out, ",")
528+
}
529+
478530
func sortDocumentsByDateExpr(docs []types.Document, expr string) ([]types.Document, error) {
479531
expr = strings.TrimSpace(expr)
480532
if expr == "" {
@@ -682,7 +734,7 @@ var appDocsCmd = &cobra.Command{
682734
}
683735

684736
opts := types.DocumentOptions{
685-
DocumentCodes: appDocsCodesFlag,
737+
DocumentCodes: normalizeDocumentCodes(appDocsCodesFlag),
686738
OfficialDateFrom: appDocsFromFlag,
687739
OfficialDateTo: appDocsToFlag,
688740
}
@@ -1068,7 +1120,7 @@ the output file path (defaults to a generated filename).`,
10681120

10691121
// List documents to find the target.
10701122
docOpts := types.DocumentOptions{
1071-
DocumentCodes: appDownloadCodesFlag,
1123+
DocumentCodes: normalizeDocumentCodes(appDownloadCodesFlag),
10721124
}
10731125
docResp, err := api.DefaultClient.GetDocuments(context.Background(), appNumber, docOpts)
10741126
if err != nil {
@@ -1167,7 +1219,7 @@ Progress is shown on stderr.`,
11671219

11681220
// List all documents.
11691221
docOpts := types.DocumentOptions{
1170-
DocumentCodes: appDownloadAllCodesFlag,
1222+
DocumentCodes: normalizeDocumentCodes(appDownloadAllCodesFlag),
11711223
OfficialDateFrom: appDownloadAllFromFlag,
11721224
OfficialDateTo: appDownloadAllToFlag,
11731225
}
@@ -1312,7 +1364,7 @@ func init() {
13121364
appCmd.AddCommand(appDownloadAllCmd)
13131365

13141366
// docs flags
1315-
appDocsCmd.Flags().StringVar(&appDocsCodesFlag, "codes", "", "Comma-separated document codes to filter by")
1367+
appDocsCmd.Flags().StringVar(&appDocsCodesFlag, "codes", "", "Comma-separated document codes/aliases to filter by (e.g., CTNF,NOA or rejection,allowance)")
13161368
appDocsCmd.Flags().StringVar(&appDocsFromFlag, "from", "", "Filter documents from this date (YYYY-MM-DD)")
13171369
appDocsCmd.Flags().StringVar(&appDocsToFlag, "to", "", "Filter documents to this date (YYYY-MM-DD)")
13181370
appDocsCmd.Flags().StringVar(&appDocsSortFlag, "sort", "", "Client-side sort for documents (date:asc or date:desc)")
@@ -1322,11 +1374,11 @@ func init() {
13221374

13231375
// download flags
13241376
appDownloadCmd.Flags().StringVarP(&appDownloadOutputFlag, "output", "o", "", "Output file path (default: auto-generated)")
1325-
appDownloadCmd.Flags().StringVar(&appDownloadCodesFlag, "codes", "", "Filter documents by codes before selecting")
1377+
appDownloadCmd.Flags().StringVar(&appDownloadCodesFlag, "codes", "", "Filter documents by codes/aliases before selecting")
13261378

13271379
// download-all flags
13281380
appDownloadAllCmd.Flags().StringVarP(&appDownloadAllOutputFlag, "output", "o", "", "Output directory (default: current directory)")
1329-
appDownloadAllCmd.Flags().StringVar(&appDownloadAllCodesFlag, "codes", "", "Comma-separated document codes to filter by")
1381+
appDownloadAllCmd.Flags().StringVar(&appDownloadAllCodesFlag, "codes", "", "Comma-separated document codes/aliases to filter by")
13301382
appDownloadAllCmd.Flags().StringVar(&appDownloadAllFromFlag, "from", "", "Filter documents from this date (YYYY-MM-DD)")
13311383
appDownloadAllCmd.Flags().StringVar(&appDownloadAllToFlag, "to", "", "Filter documents to this date (YYYY-MM-DD)")
13321384
}

cmd/app_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"strings"
45
"testing"
56

67
"github.com/smcronin/uspto-cli/internal/types"
@@ -84,3 +85,13 @@ func TestSelectPrimaryAttorney(t *testing.T) {
8485
t.Fatalf("primary name = %q, want Jane Doe", got["name"])
8586
}
8687
}
88+
89+
func TestNormalizeDocumentCodes(t *testing.T) {
90+
got := normalizeDocumentCodes("rejection,allowance,clm,Spec,office-action,CTFR")
91+
wantParts := []string{"CTNF", "CTFR", "NOA", "CLM", "SPEC"}
92+
for _, part := range wantParts {
93+
if !strings.Contains(got, part) {
94+
t.Fatalf("normalizeDocumentCodes() = %q, want to contain %q", got, part)
95+
}
96+
}
97+
}

cmd/family.go

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,18 @@ type FamilyNode struct {
2626
Children []FamilyNode `json:"children,omitempty"`
2727
}
2828

29+
// FamilyApplicationRef is a deduplicated family member with relationship label.
30+
type FamilyApplicationRef struct {
31+
ApplicationNumber string `json:"applicationNumber"`
32+
Relationship string `json:"relationship"`
33+
}
34+
2935
// FamilyResult is the top-level output for the family command.
3036
type FamilyResult struct {
31-
Root string `json:"root"`
32-
Tree FamilyNode `json:"tree"`
33-
AllApplicationNumbers []string `json:"allApplicationNumbers"`
34-
TotalMembers int `json:"totalMembers"`
37+
Root string `json:"root"`
38+
Tree FamilyNode `json:"tree"`
39+
AllApplicationNumbers []FamilyApplicationRef `json:"allApplicationNumbers"`
40+
TotalMembers int `json:"totalMembers"`
3541
}
3642

3743
// ---------------------------------------------------------------------------
@@ -98,18 +104,25 @@ func runFamily(cmd *cobra.Command, args []string) error {
98104

99105
ctx := context.Background()
100106
client := api.DefaultClient
101-
visited := make(map[string]bool)
107+
visited := make(map[string]string)
102108

103109
progress(fmt.Sprintf("Building family tree for %s (depth %d)...", appNumber, flagFamilyDepth))
104110

105111
tree := buildFamilyNode(ctx, client, appNumber, "", flagFamilyDepth, visited)
106112

107-
// Collect all unique application numbers.
108-
allApps := make([]string, 0, len(visited))
113+
// Collect all unique application numbers and their relationship labels.
114+
allAppNums := make([]string, 0, len(visited))
109115
for app := range visited {
110-
allApps = append(allApps, app)
116+
allAppNums = append(allAppNums, app)
117+
}
118+
sortStrings(allAppNums)
119+
allApps := make([]FamilyApplicationRef, 0, len(allAppNums))
120+
for _, app := range allAppNums {
121+
allApps = append(allApps, FamilyApplicationRef{
122+
ApplicationNumber: app,
123+
Relationship: visited[app],
124+
})
111125
}
112-
sortStrings(allApps)
113126

114127
result := FamilyResult{
115128
Root: appNumber,
@@ -138,14 +151,21 @@ func runFamily(cmd *cobra.Command, args []string) error {
138151
// buildFamilyNode recursively builds a FamilyNode by fetching continuity
139152
// and metadata for the given application number. It uses the visited set
140153
// to avoid cycles and redundant API calls.
141-
func buildFamilyNode(ctx context.Context, client *api.Client, appNumber, relationship string, depth int, visited map[string]bool) FamilyNode {
154+
func buildFamilyNode(ctx context.Context, client *api.Client, appNumber, relationship string, depth int, visited map[string]string) FamilyNode {
142155
node := FamilyNode{
143156
ApplicationNumber: appNumber,
144157
Relationship: relationship,
145158
}
146159

147-
// Mark as visited immediately to prevent cycles.
148-
visited[appNumber] = true
160+
// Mark as visited immediately to prevent cycles. Keep the first discovered
161+
// relationship label for deduplicated allApplicationNumbers output.
162+
if _, exists := visited[appNumber]; !exists {
163+
rel := strings.TrimSpace(strings.ToUpper(relationship))
164+
if rel == "" {
165+
rel = "ROOT"
166+
}
167+
visited[appNumber] = rel
168+
}
149169

150170
// Fetch metadata for this application.
151171
progress(fmt.Sprintf(" Fetching metadata for %s...", appNumber))
@@ -192,7 +212,7 @@ func buildFamilyNode(ctx context.Context, client *api.Client, appNumber, relatio
192212
var related []relatedApp
193213

194214
for _, p := range fw.ParentContinuityBag {
195-
if p.ParentApplicationNumberText != "" && !visited[p.ParentApplicationNumberText] {
215+
if p.ParentApplicationNumberText != "" && visited[p.ParentApplicationNumberText] == "" {
196216
related = append(related, relatedApp{
197217
appNumber: p.ParentApplicationNumberText,
198218
relationship: parentRelationship(p.ClaimParentageTypeCode),
@@ -201,7 +221,7 @@ func buildFamilyNode(ctx context.Context, client *api.Client, appNumber, relatio
201221
}
202222

203223
for _, c := range fw.ChildContinuityBag {
204-
if c.ChildApplicationNumberText != "" && !visited[c.ChildApplicationNumberText] {
224+
if c.ChildApplicationNumberText != "" && visited[c.ChildApplicationNumberText] == "" {
205225
related = append(related, relatedApp{
206226
appNumber: c.ChildApplicationNumberText,
207227
relationship: childRelationship(c.ClaimParentageTypeCode),
@@ -217,7 +237,7 @@ func buildFamilyNode(ctx context.Context, client *api.Client, appNumber, relatio
217237
// because an earlier sibling's subtree may have already visited an app
218238
// that was in our related list.
219239
for _, rel := range related {
220-
if visited[rel.appNumber] {
240+
if visited[rel.appNumber] != "" {
221241
continue
222242
}
223243
childNode := buildFamilyNode(ctx, client, rel.appNumber, rel.relationship, depth-1, visited)
@@ -267,7 +287,7 @@ func writeFamilyTree(result FamilyResult) {
267287
fmt.Fprintln(os.Stdout)
268288
fmt.Fprintln(os.Stdout, "All application numbers:")
269289
for _, app := range result.AllApplicationNumbers {
270-
fmt.Fprintf(os.Stdout, " %s\n", app)
290+
fmt.Fprintf(os.Stdout, " %s (%s)\n", app.ApplicationNumber, app.Relationship)
271291
}
272292
}
273293

@@ -359,6 +379,6 @@ func writeKeyValueFamily(result FamilyResult) {
359379
fmt.Fprintln(os.Stdout)
360380
fmt.Fprintln(os.Stdout, "Applications:")
361381
for _, app := range result.AllApplicationNumbers {
362-
fmt.Fprintf(os.Stdout, " %s\n", app)
382+
fmt.Fprintf(os.Stdout, " %s (%s)\n", app.ApplicationNumber, app.Relationship)
363383
}
364384
}

cmd/family_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package cmd
2+
3+
import "testing"
4+
5+
func TestRelationshipNormalization(t *testing.T) {
6+
tests := []struct {
7+
in string
8+
want string
9+
}{
10+
{in: "con", want: "CON"},
11+
{in: " div ", want: "DIV"},
12+
{in: "cip", want: "CIP"},
13+
{in: "pro", want: "PRO"},
14+
{in: "", want: "PARENT"},
15+
}
16+
for _, tc := range tests {
17+
if got := parentRelationship(tc.in); got != tc.want {
18+
t.Fatalf("parentRelationship(%q) = %q, want %q", tc.in, got, tc.want)
19+
}
20+
}
21+
if got := childRelationship(""); got != "CHILD" {
22+
t.Fatalf("childRelationship(\"\") = %q, want CHILD", got)
23+
}
24+
}

0 commit comments

Comments
 (0)