Skip to content

feat(xbrl): ISO date transforms + CIK/dimension query coverage (#154)#195

Merged
sroussey merged 2 commits into
harnessfrom
claude/sec-issue-154-nqphku
Jul 15, 2026
Merged

feat(xbrl): ISO date transforms + CIK/dimension query coverage (#154)#195
sroussey merged 2 commits into
harnessfrom
claude/sec-issue-154-nqphku

Conversation

@sroussey

Copy link
Copy Markdown
Contributor

Implements #154 (iXBRL ingestion hardening and query coverage). Based on harness.

The issue is broad — "harden edge cases, grow fixture coverage, and expand sec query xbrl". This PR delivers a coherent, tested slice across all three prongs.

Hardening — ixt date transforms

Date-formatted non-numeric facts (e.g. dei:DocumentPeriodEndDate tagged ixt:date-monthname-day-year-en = "March 31, 2026") were left as locale strings because unregistered transforms fall through to raw text. Registered the SEC date-transform family so they normalize to ISO-8601 (2026-03-31):

  • Both TR1 concatenated (datemonthdayyearen, dateslashus/dateslasheu) and TR3/TR4 hyphenated (date-monthname-day-year-en, date-day-monthname-year-en, date-month-day-year, date-day-month-year, date-year-month-day) spellings.
  • A registered date transform that can't parse its text falls back to the trimmed raw text rather than blanking the fact.

Query coverage

  • Exposed sec query xbrl --cik <cik> to read a concept's series across all of an issuer's filings (e.g. trust balance over time). The query function already accepted a CIK; only the CLI was accession-only. Cross-issuer results carry an Accession column and order by (accession, fact_index).
  • Surfaced each fact's dimensional qualifiers as a compact Axis=Member column (e.g. StatementClassOfStock=CommonClassA), previously invisible in the table.
  • Clear validation for the accession-vs---cik choice (neither / both / non-numeric CIK all error cleanly).

Tests

  • Date transforms: every registered spelling, unparseable-date fallback, out-of-range month/day rejection.
  • A dei date fact normalized to ISO end to end through parseInlineXbrl.
  • formatXbrlDimensions: Axis=Member rendering (prefix + Axis/Member/Domain suffix stripping) and graceful "" degradation on null / malformed JSON.
  • CIK-path ordering for both the unfiltered and concept-filtered code paths.

38 tests pass (was 28).

Verification

  • bun test on all XBRL + query tests: 38 pass, 0 fail
  • tsc --noEmit: clean
  • prettier --check: clean
  • End-to-end: seeded a real SQLite DB and drove the actual CLI — confirmed the Dimensions column renders on accession queries, the Accession column + cross-filing ordering render on --cik queries, and the validation errors surface cleanly.

Docs

Updated CLAUDE.md's iXBRL section to document the ISO date normalization and the new --cik / dimensions query surface.

🤖 Generated with Claude Code

https://claude.ai/code/session_0112rEbCs1VKZQ4DJ6CfQojT


Generated by Claude Code

Hardening: register the ixt/ixt-sec date transforms so non-numeric date
facts (e.g. dei:DocumentPeriodEndDate tagged ixt:date-monthname-day-year-en)
normalize to ISO-8601 instead of being left as locale strings. Both the TR1
concatenated (datemonthdayyearen, dateslashus/dateslasheu) and TR3/TR4
hyphenated spellings are handled; an unparseable date keeps its trimmed raw
text rather than blanking the fact.

Query coverage: expose `sec query xbrl --cik <cik>` to read a concept's
series across all of an issuer's filings (the query function already
supported CIK; the CLI only took an accession), ordered by
(accession, fact_index) with an Accession column. Surface each fact's
dimensional qualifiers as a compact Axis=Member column.

Tests: date-transform cases (all spellings + unparseable fallback), a
dei date fact normalized end to end, formatXbrlDimensions rendering and
malformed-JSON degradation, and CIK-path ordering (unfiltered and
concept-filtered).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112rEbCs1VKZQ4DJ6CfQojT

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens iXBRL ingestion by registering SEC date ixt transforms that normalize common locale date formats into ISO-8601 strings, and expands sec query xbrl to support cross-filing queries by CIK plus display of dimensional qualifiers.

Changes:

  • Add ixt date transform registrations and parsing logic to normalize supported date spellings to ISO-8601, with “unparseable -> trimmed raw text” fallback.
  • Extend XBRL querying/CLI output: sec query xbrl --cik <cik> support, deterministic cross-filing ordering, and a Dimensions (Axis=Member) column.
  • Add/expand tests for date normalization, CIK query ordering, and dimension formatting.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/sec/xbrl/parseInlineXbrl.test.ts Adds end-to-end test asserting a DEI date fact is normalized to ISO-8601 via ixt transforms.
src/sec/xbrl/ixtTransforms.ts Implements and registers SEC date transforms (month-name, slash, hyphen variants) to produce ISO dates.
src/cli/queries/XbrlQuery.ts Updates query ordering logic and adds formatXbrlDimensions() for compact Axis=Member display.
src/cli/queries/XbrlQuery.test.ts Adds tests for CIK-path ordering and dimension formatting behavior/fallbacks.
src/cli/groups/query.ts Extends sec query xbrl command to accept --cik, validates accession vs CIK usage, and renders new columns.
CLAUDE.md Documents ISO date normalization behavior and the new query surface (--cik, dimensions).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +61 to +67
function toIsoDate(year: number, month: number, day: number): string | null {
if (!Number.isInteger(month) || month < 1 || month > 12) return null;
if (!Number.isInteger(day) || day < 1 || day > 31) return null;
if (!Number.isInteger(year) || year < 1) return null;
const pad = (n: number, width: number): string => String(n).padStart(width, "0");
return `${pad(year, 4)}-${pad(month, 2)}-${pad(day, 2)}`;
}
Comment thread src/cli/queries/XbrlQuery.ts Outdated
Comment on lines +45 to +47
const total = await repo.count(criteria);
const rows = (await repo.query(criteria, { limit, offset })) ?? [];
return { rows: rows.sort((a, b) => a.fact_index - b.fact_index), total };
return { rows: rows.sort(byFilingOrder), total };
Comment thread src/cli/groups/query.ts
Comment on lines +414 to +420
const cikRaw = options.cik as string | undefined;
let cik: number | undefined;
if (cikRaw !== undefined) {
cik = parseInt(cikRaw, 10);
if (!Number.isFinite(cik))
throw new Error(`Invalid --cik "${cikRaw}". Must be a number.`);
}
…R BY pushdown

- toIsoDate: reject impossible calendar dates (Feb 30, Apr 31) via a UTC
  round-trip check instead of emitting an invalid ISO string like
  "2024-02-30"; unparseable dates keep their raw text.
- query xbrl --cik: require an all-digit string (matches parseCikArg in the
  fetch group) so "123abc" errors instead of being silently read as 123.
- queryXbrlFacts unfiltered path: push ORDER BY (accession_number, fact_index)
  down to storage so offset/limit pagination is consistent across pages,
  rather than sorting only the returned page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112rEbCs1VKZQ4DJ6CfQojT
@sroussey
sroussey merged commit 80e8b5c into harness Jul 15, 2026
1 check passed
@sroussey sroussey linked an issue Jul 15, 2026 that may be closed by this pull request
sroussey added a commit that referenced this pull request Jul 16, 2026
…#195)

* feat(xbrl): ISO date transforms + CIK/dimension query coverage (#154)

Hardening: register the ixt/ixt-sec date transforms so non-numeric date
facts (e.g. dei:DocumentPeriodEndDate tagged ixt:date-monthname-day-year-en)
normalize to ISO-8601 instead of being left as locale strings. Both the TR1
concatenated (datemonthdayyearen, dateslashus/dateslasheu) and TR3/TR4
hyphenated spellings are handled; an unparseable date keeps its trimmed raw
text rather than blanking the fact.

Query coverage: expose `sec query xbrl --cik <cik>` to read a concept's
series across all of an issuer's filings (the query function already
supported CIK; the CLI only took an accession), ordered by
(accession, fact_index) with an Accession column. Surface each fact's
dimensional qualifiers as a compact Axis=Member column.

Tests: date-transform cases (all spellings + unparseable fallback), a
dei date fact normalized end to end, formatXbrlDimensions rendering and
malformed-JSON degradation, and CIK-path ordering (unfiltered and
concept-filtered).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112rEbCs1VKZQ4DJ6CfQojT

* fix(xbrl): address review — reject impossible dates, strict CIK, ORDER BY pushdown

- toIsoDate: reject impossible calendar dates (Feb 30, Apr 31) via a UTC
  round-trip check instead of emitting an invalid ISO string like
  "2024-02-30"; unparseable dates keep their raw text.
- query xbrl --cik: require an all-digit string (matches parseCikArg in the
  fetch group) so "123abc" errors instead of being silently read as 123.
- queryXbrlFacts unfiltered path: push ORDER BY (accession_number, fact_index)
  down to storage so offset/limit pagination is consistent across pages,
  rather than sorting only the returned page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112rEbCs1VKZQ4DJ6CfQojT

---------

Co-authored-by: Claude <noreply@anthropic.com>
@sroussey
sroussey deleted the claude/sec-issue-154-nqphku branch July 17, 2026 00:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[iXBRL] XBRL ingestion hardening and query coverage

3 participants