Skip to content

feat(generator): SpecsIntact .SEC output renderer#207

Merged
thewrz merged 2 commits into
mainfrom
feat/issue-108
Jun 17, 2026
Merged

feat(generator): SpecsIntact .SEC output renderer#207
thewrz merged 2 commits into
mainfrom
feat/issue-108

Conversation

@thewrz

@thewrz thewrz commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Why

SpecR parses SpecsIntact .SEC but could not generate it. Firms delivering into SpecsIntact-mandated workflows need the inverse path. Kept on backlog per ADR-019 (explicitly not declined). This adds the AST → .SEC renderer.

What

New generateSec(tree, refs?) in src/generator/sec/ — the inverse of src/parser/sec/, exported from src/generator/index.ts.

  • tree<SEC><SCN>SECTION …</SCN><STL>…</STL>{parts}</SEC>
  • part<PRT><TTL>PART N …</TTL>…</PRT> (re-adds the PART N prefix the parser strips)
  • article/pr*<SPT><TTL>…</TTL>{refs}{children}</SPT>
  • note<NTE><NPR>…</NPR></NTE>, continuation<TXT>…</TXT>
  • leaf at parent tier +1 → <LST>, +2 → <ITM>; any node with children or a standard ref → nested <SPT>
  • standard refs → <REF><RID>code</RID><RTL>tail</RTL></REF> anchored to their source node
  • encodeXmlEntities (in sec/entities.ts) is the exact inverse of the parser's decodeXmlEntities — the five XML metacharacters, & first so introduced entities aren't double-escaped.

Faithfulness = AST round-trip, not byte-identity. The parser is deliberately lossy (drops MTA/HDR/BRK chrome, URL/SCP inline wrappers, and SRF inline section numbers), so the renderer emits a clean canonical .SEC that re-parses to the same tree.

Scope is rendering only — no scheduling/CLI wiring (out of scope per the issue, on backlog).

Design decisions

  • Round-trip is asserted on the tree shape(section, title, and each node's type/text/vanish) — since UUIDs are regenerated on every parse. The decisive tests run on the real UFGS fixtures 27_41_00.SEC and 27_10_00.SEC.
  • Tier offset drives the leaf element choice. The parser types nodes by SEC nesting depth: a nested SPT is always parent_tier+1, while LST is +1 and ITM is +2 as leaves. The renderer inverts this by computing each leaf child's tier offset from its parent SPT.
  • A node with children — or with an associated standard ref — must be a nested SPT. Only an SPT can hold child content or a <REF>; a <LST>/<ITM> leaf cannot. A node whose only "child" is a ref (e.g. an SPT containing just a <REF>) parses to a childless AST node, so the ref index is consulted to keep it an SPT.
  • KNOWN AMBIGUITY (documented in a test): a child that both declares a tier > parent+1 and carries children has no faithful single-element inverse — a nested SPT collapses the gap to +1. This shape never occurs in the UFGS corpus (every tier-gap node is a childless ITM), so it's rendered best-effort as a nested SPT and documented rather than silently picked.

Testing

  • Unit tests pass (pnpm test — 953 passed, incl. 14 new in src/generator/sec/)
  • Lint + typecheck + prettier pass (pnpm lint)
  • Round-trip verified on real UFGS fixtures (27_41_00.SEC, 27_10_00.SEC): parse → generate → re-parse yields an identical tree
  • Standard-reference round-trip (synthetic + real fixture) by (code, text)
  • CI green

🤖 Co-authored by Claude Opus 4.8. Closes #108.

Summary by CodeRabbit

  • New Features
    • Added generateSec to produce canonical SpecsIntact .SEC XML with nested sections, parts, articles, and list/item structures.
    • Introduced XML entity encoding to safely escape XML metacharacters while preserving correct decoding behavior.
    • Exposed generateSec from the generator module’s public API.
  • Tests
    • Added Vitest coverage for XML entity encoding/decoding behavior.
    • Added end-to-end and regression tests for SEC XML generation, including deterministic structural round-trips and known edge cases.

Add `generateSec(tree, refs?)` — the inverse of the SEC parser. Renders the
canonical CSI AST to SpecsIntact .SEC XML so firms can deliver into
SpecsIntact-mandated workflows. Faithfulness is defined as AST round-trip:
parse → generate → re-parse yields the same tree.

The parser is deliberately lossy (it drops MTA/HDR/BRK chrome and SRF inline
section numbers), so the renderer emits a clean canonical .SEC rather than
reproducing original bytes. Each node type inverts its parser mapping; the
tier offset from a parent SPT chooses LST (+1) vs ITM (+2) for leaves, while
any node carrying children or a standard ref becomes a nested SPT.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c8ea9d32-5713-4c9e-a94e-059d14669b8b

📥 Commits

Reviewing files that changed from the base of the PR and between 14d7d2b and 3d58724.

📒 Files selected for processing (2)
  • src/generator/sec/index.ts
  • src/generator/sec/tier.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/generator/sec/tier.ts
  • src/generator/sec/index.ts

📝 Walkthrough

Walkthrough

Adds generateSec(tree: SpecTree, refs: readonly SecRef[]): string, an AST-to-SpecsIntact .SEC XML renderer. Supporting utilities provide XML entity encoding (encodeXmlEntities) and structural tier mapping (tierOf). The function is re-exported from the generator's public entry point and covered by round-trip tests against real UFGS fixtures.

Changes

SEC XML Generator

Layer / File(s) Summary
XML entity encoder and tier mapping
src/generator/sec/tier.ts, src/generator/sec/entities.ts, src/generator/sec/entities.test.ts
tierOf maps structural node types (article → 0 through pr5 → 5) to numeric nesting tiers. encodeXmlEntities escapes & < > " ' in deterministic order (& first) to prevent double-escaping, and tests verify single-escape, no-double-escape, round-trip with decodeXmlEntities, and plain-text passthrough.
Core generateSec renderer and tests
src/generator/sec/index.ts, src/generator/sec/index.test.ts
Implements reference indexing by source node, note rendering as vanish-flagged content, standard reference reconstruction from referenceText, leaf vs. nested <SPT> selection via tierOf, continuation <TXT> and <PRT> emission with PART N prefixing, and GeneratorError wrapping. Tests verify section/title round-trips, entity handling, note vanish flags, standard-ref anchoring, a tier-gap inversion regression, and parse → generate → re-parse structural equivalence on two real UFGS fixtures.
Public API re-export
src/generator/index.ts
generateSec is re-exported from the generator module's public entry point via export { generateSec } from './sec/index.js'.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • wrzonance/SpecR#180: The main PR's SEC generator adds tier/type mapping helpers (tierOf) and uses them in generateSec output logic, which directly aligns with the retrieved PR's parser change that maps nested <SPT> depth to canonical pr1pr5 node types for correct rendering.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(generator): SpecsIntact .SEC output renderer' clearly and specifically describes the main change—implementing a renderer for SpecsIntact XML output, which is the primary objective of this pull request.
Linked Issues check ✅ Passed The PR fully implements the primary objectives from issue #108: creates an AST-to-SpecsIntact XML renderer that renders parts, articles, and other structural elements correctly, and validates round-trip equivalence (parse → generate → re-parse) on real UFGS fixtures.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the SEC renderer implementation (encodeXmlEntities, generateSec, tier mapping, and comprehensive tests). No unrelated modifications or features outside the renderer scope are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-108

Comment @coderabbitai help to get the list of available commands and usage tips.

@thewrz

thewrz commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/generator/sec/tier.ts (1)

6-13: 💤 Low value

Consider a more precise type for TIER to improve type safety.

Readonly<Record<string, number>> allows indexing with any string and types the result as number, but at runtime TIER[unknownType] returns undefined. Since tierOf correctly handles this with ?? null, the code is safe, but a more precise type like Readonly<Partial<Record<NodeType, number>>> would make the optional nature explicit and catch potential misuse at compile time.

♻️ Suggested type refinement
-const TIER: Readonly<Record<string, number>> = {
+const TIER: Readonly<Partial<Record<NodeType, number>>> = {
   article: 0,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/generator/sec/tier.ts` around lines 6 - 13, The TIER constant uses a
too-permissive type annotation that accepts any string key but returns undefined
at runtime for unknown keys, creating a type safety gap. Change the type of the
TIER constant from Readonly<Record<string, number>> to
Readonly<Partial<Record<NodeType, number>>> to explicitly express that it only
contains specific NodeType keys and that accessing unknown keys may return
undefined, which will catch potential misuse at compile time rather than
runtime.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/generator/sec/index.ts`:
- Line 1: The import statement for SpecNode, SpecTree, and SecRef is importing
directly from the internal types.js file instead of using the barrel export.
Update the import path from ../../ast/types.js to ../../ast/index.js. The three
types are already re-exported from the barrel, so only the import path needs to
change while keeping the imported type names unchanged. This follows the coding
guideline of importing from a sibling module's barrel (index.ts) rather than its
internal files.

In `@src/generator/sec/tier.ts`:
- Line 1: Update the import statement for NodeType to use the barrel export from
the ast module instead of the internal types file. In src/generator/sec/tier.ts,
change the import path from '../../ast/types.js' to '../../ast/index.js' to
follow the module boundary contract. Additionally, apply the same fix to
src/generator/sec/index.ts where SpecNode, SpecTree, and SecRef should also be
imported from their respective barrel files instead of internal implementation
files.

---

Nitpick comments:
In `@src/generator/sec/tier.ts`:
- Around line 6-13: The TIER constant uses a too-permissive type annotation that
accepts any string key but returns undefined at runtime for unknown keys,
creating a type safety gap. Change the type of the TIER constant from
Readonly<Record<string, number>> to Readonly<Partial<Record<NodeType, number>>>
to explicitly express that it only contains specific NodeType keys and that
accessing unknown keys may return undefined, which will catch potential misuse
at compile time rather than runtime.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f74ca854-8918-4166-b9e7-d2cb08252f6b

📥 Commits

Reviewing files that changed from the base of the PR and between c761bc6 and 14d7d2b.

📒 Files selected for processing (6)
  • src/generator/index.ts
  • src/generator/sec/entities.test.ts
  • src/generator/sec/entities.ts
  • src/generator/sec/index.test.ts
  • src/generator/sec/index.ts
  • src/generator/sec/tier.ts

Comment thread src/generator/sec/index.ts Outdated
Comment thread src/generator/sec/tier.ts Outdated
The SEC renderer imported SpecNode/SpecTree/SecRef (sec/index.ts) and NodeType
(sec/tier.ts) directly from ../../ast/types.js, crossing the hard module
boundary. Both are re-exported from the ast barrel — switch to ../../ast/index.js
per the module-boundary contract.

Also tighten TIER from Readonly<Record<string, number>> to
Readonly<Partial<Record<NodeType, number>>> so unknown-key access is typed
undefined at compile time (tierOf already guards with ?? null).

Addresses CodeRabbit review on #207.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@thewrz

thewrz commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

CodeRabbit nitpick addressed (sec/tier.ts TIER type): fixed in 3d58724TIER is now Readonly<Partial<Record<NodeType, number>>>, making unknown-key access compile-time undefined (tierOf already guards with ?? null).

@thewrz thewrz merged commit 68db6c5 into main Jun 17, 2026
13 checks passed
@thewrz thewrz deleted the feat/issue-108 branch June 17, 2026 18:59
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.

feat(generator): SpecsIntact .SEC output renderer

1 participant