Skip to content

Add pipeline ability - #1

Merged
mattbriggs merged 3 commits into
mainfrom
mdb_2026-06-27-addpipelineandtriage.md
Jun 28, 2026
Merged

mattbriggs merged 3 commits into
mainfrom
mdb_2026-06-27-addpipelineandtriage.md

Conversation

@mattbriggs

@mattbriggs mattbriggs commented Jun 27, 2026 •

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added structure-parser pipe to process Markdown folders/files into per-file JSON outputs plus a CSV inventory report (custom report path supported).
    • Added run_pipeline() API for the same repository pipeline flow.
    • Supports --dry-run, include/exclude filters, optional file logging (format selectable), and --strict exit behavior.
  • Bug Fixes

    • Improved fail-fast handling for unsafe output/input overlap and duplicate targets, with more reliable exit codes and run diagnostics.
    • Enhanced resilience so failures in individual files don’t stop the full run.
  • Documentation / Tests

    • Updated repository pipeline documentation and added extensive integration/unit test coverage.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds a complete folder-based pipe pipeline to structure-parser, plus new article-triage design and documentation updates, classifier/schema validation changes, fixture content, and broad unit/integration coverage.

Changes

Repository Pipeline

Layer / File(s) Summary
Contracts and discovery
src/structure_parser/contracts/pipeline.py, src/structure_parser/pipeline/discovery.py, tests/unit/test_pipeline_contracts.py, tests/unit/test_pipeline_discovery.py
Pipeline config/result/status contracts and PIPE diagnostics are defined, and Markdown discovery recursively scans inputs with deterministic ordering and include/exclude filtering.
Output and inventory reporting
src/structure_parser/pipeline/output.py, src/structure_parser/pipeline/reporting.py, tests/unit/test_pipeline_output.py, tests/unit/test_pipeline_reporting.py
JSON target computation/writing, overlap and duplicate-target checks, and CSV inventory generation are implemented and tested.
Orchestration and CLI
src/structure_parser/pipeline/__init__.py, src/structure_parser/pipeline/orchestrator.py, src/structure_parser/application/commands.py, src/structure_parser/cli.py, tests/unit/test_pipeline_orchestrator.py, tests/unit/test_cli_pipeline_command.py
The pipeline orchestrator coordinates discovery, parsing, output, logging, and exit codes, and the pipe Typer command wires CLI options into PipelineConfig.
Docs, fixtures, and integration tests
README.md, docs/*, docs_src/*, mkdocs.yml, tests/fixtures/content_repo/*, tests/integration/*
Pipeline and triage documentation is updated across the site and source docs, new Markdown fixtures are added, and integration tests cover the CLI and orchestrator end to end.

Article Triage, Classifier, and Docs

Layer / File(s) Summary
Triage design note
design/2026-06-27-tech-note-article-triage-note.md
The triage note defines metadata and construction signals, weighted unit/article scoring, reconciliation behavior, diagnostics, and a triage.py implementation plan.
Classifier and validation
src/structure_parser/structured_markdown/classifier.py, src/structure_parser/validation/model_validator.py, tests/contract/test_schema_round_trip.py, tests/unit/test_structured_markdown_classifier.py
Structured-markdown classification now uses metadata plus unit-population scoring, declared-schema validation uses schema_name, and tests cover schema round-trips and inference precedence.
Architecture and concept docs
docs_src/architecture/*, docs_src/concept/*, docs_src/model/*, docs_src/user-guide/*
Parser-flow, triage, fallback, and diagnostic guidance is rewritten across the model, architecture, concept, and user-guide documentation pages.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I hop through docs and code all day,
New pipes and triage found their way.
JSON, CSV, and clues in sight,
The parser hums by rabbit light.
With warnings, scores, and paths in tow,
This PR makes the whole burrow glow ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is related to the PR but too vague to convey the main change clearly. Rename it to describe the specific pipeline feature, such as adding the repository pipeline CLI and CSV inventory reporting.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 mdb_2026-06-27-addpipelineandtriage.md

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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: 12

🧹 Nitpick comments (2)
design/2026-06-27-tech-note-article-triage-note.md (2)

516-533: 📐 Maintainability & Code Quality | 🔵 Trivial

MetadataEvidence missing schema_name for future evidence contract.

Section 5.9's evidence JSON includes "schema": "artHowto.schema.json", but MetadataEvidence (section 12.2) only has schema_exists: bool. When the evidence contract is later implemented (section 12.9), the schema filename will be needed. Add schema_name: str | None to MetadataEvidence now to avoid a breaking change later.

Also applies to: 292-316

🤖 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 `@design/2026-06-27-tech-note-article-triage-note.md` around lines 516 - 533,
MetadataEvidence is missing the future schema filename field needed by the
evidence contract, so update the MetadataEvidence dataclass to include
schema_name: str | None alongside schema_exists. Keep the new field aligned with
the existing evidence shape and use the existing symbols MetadataEvidence and
TriageDecision as the place to make the change so later JSON evidence can carry
the schema name without a breaking contract change.

762-791: 📐 Maintainability & Code Quality | 🔵 Trivial

Test plan missing explicit coverage for reconciliation cases 2 and 6.

The fixture table covers cases 1 (implicitly), 3 (metadata_construction_conflict), 4/5/7 (unknown_fallback, construction_only_*), and 6/7 (metadata_invalid_value). Missing:

  • Case 2: Metadata valid, construction weak — e.g., articleType: howto with minimal structure where construction scores don't clear threshold.
  • Case 6 explicit: Invalid metadata with strong construction — metadata_invalid_value.md may cover this if structure is strong, but the expected outcome says "construction inference or unknown" which is ambiguous.

Add explicit fixtures for cases 2 and 6, and ensure the reconciliation unit tests parametrize all 7 cases with exact threshold boundaries (e.g., metadata confidence 0.80, construction winner 0.59, gap 0.19).

🤖 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 `@design/2026-06-27-tech-note-article-triage-note.md` around lines 762 - 791,
The test plan is missing explicit coverage for reconciliation cases 2 and 6, so
add dedicated fixtures and assertions to make those paths unambiguous. Update
the fixture table in the test plan with a case-2 example where valid metadata
like articleType=howto has weak construction and should keep metadata-driven
triage, and a case-6 example where invalid metadata is overridden by strong
construction with an exact expected outcome. Also expand tests such as
test_triage_reconciliation and test_metadata_triage to parametrize all seven
reconciliation cases with clear threshold-boundary expectations, using the
existing diagnostic symbols SP-043, SP-044, SP-045, and SP-046 rather than
relying on vague fallback behavior.
🤖 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 `@design/2026-06-27-tech-note-article-triage-note.md`:
- Around line 265-273: The conflict-resolution rules are inconsistent between
the summary table and the detailed case in the article, especially around the
metadata-vs-construction decision logic. Update the section that defines the
“metadata valid, construction strongly disagrees” path so it matches the policy
in the table by explicitly stating how to choose the stronger evidence, and
align the detailed case logic in the relevant section with that rule instead of
always preferring construction when the gap threshold is met. Use the section
headings and the conflict-resolution case descriptions as the primary symbols to
locate and reconcile the mismatched guidance.

In `@src/structure_parser/application/commands.py`:
- Around line 221-223: The CSV export path in the command flow is treating
CsvInventoryReporter.write() as if it cannot fail, so report success is logged
even when the write returns PIPE_003. Update the logic around report_path,
CsvInventoryReporter.write(), and _log_report_written() to check the write
result first, and only print/log success when the write succeeds; if it fails,
propagate or return the error instead of exiting 0.

In `@src/structure_parser/cli.py`:
- Around line 156-158: The CLI option for log formatting currently accepts any
string, which lets invalid values silently fall back to text in
_add_log_file_handler(). Update the Typer option on the log_format parameter in
the CLI entrypoint to validate only the supported values (text and jsonl), so
unsupported inputs are rejected at the command boundary instead of being treated
as text.

In `@src/structure_parser/contracts/pipeline.py`:
- Around line 81-90: PipelineConfig currently allows an empty inputs list, which
lets invalid Python callers proceed into discovery instead of failing at
validation time. Update the PipelineConfig model in the pipeline contract to
enforce at least one input on the inputs field, using the existing field
definition so empty lists are rejected before any run begins. Keep the change
localized to the PipelineConfig inputs declaration and align it with the CLI
contract.

In `@src/structure_parser/pipeline/discovery.py`:
- Around line 79-98: Explicit file inputs are being accepted without verifying
they are Markdown, so _add_file() can emit non-Markdown paths like notes.txt as
SourceFormat.markdown. Update discovery._add_file to apply the same
_MARKDOWN_SUFFIXES gate used by folder discovery before creating
DiscoveredSource, and only append when the path matches a Markdown suffix in
addition to the existing include/exclude checks.

In `@src/structure_parser/pipeline/orchestrator.py`:
- Around line 75-85: The PIPE-007 early return in the orchestrator is dropping
discovery and run-diagnostic totals because _make_run_result() only derives
aggregates from files. Update the early-abort path and _make_run_result() so
discovered_count still reflects the parsed sources and run-level diagnostics
still contribute to error_count/warning_count, then ensure PipelineCommand.run()
summary uses those preserved counts for duplicate/overlap/missing-input aborts.

In `@src/structure_parser/pipeline/output.py`:
- Around line 108-123: Duplicate-target detection in the output pipeline is
using the raw Path as the seen key, so case-only differences can bypass PIPE-007
on case-insensitive filesystems. Update the duplicate check in
ParsedDocumentWriter/its target collection logic to normalize target keys using
the output filesystem’s case rules before storing or comparing in seen, and keep
the existing error logging and duplicates list behavior. Add a test around the
duplicate-target detection path that uses case-variant source names to verify
the collision is caught.

In `@src/structure_parser/pipeline/reporting.py`:
- Around line 30-70: The report write failure path in write() is currently easy
to miss because it returns PIPE_003, but the caller in the command flow ignores
that status and still reports success. Update write() and the surrounding
reporting flow so a failed report write is surfaced as a real failure: either
raise from InventoryReportWriter.write or have the caller check the PIPE_003
return, add a run diagnostic, and fail the command with a non-zero exit. Make
sure the fix is applied consistently in InventoryReportWriter.write and the
command handler that invokes it.

In `@tests/integration/test_pipeline_error_inventory.py`:
- Around line 61-62: The integration test assertion in
test_pipeline_error_inventory should verify parser_codes directly instead of
allowing a fallback on status == "parsed". Update the check in the test around
rows[0]["parser_codes"] so it explicitly fails when parser diagnostics are
missing, keeping the assertion tied to the parser_codes field and the test's
intended regression coverage.

In `@tests/unit/test_cli_pipeline_command.py`:
- Around line 63-71: The strict-mode test in test_exit_1_strict_with_warnings is
currently non-deterministic because it accepts either exit code, so it does not
verify PipelineCommand.run()’s strict branch. Make the warning input
deterministic by stubbing the orchestrator result or equivalent in the test
setup, then assert that the strict path returns code 1 when warnings are
present. Use the existing _run helper and PipelineCommand.run as the key symbols
to update the test.

In `@tests/unit/test_pipeline_orchestrator.py`:
- Around line 135-145: The test currently only covers a fully valid discovery
path, so it never proves failure isolation in PipelineOrchestrator.run. Update
test_one_failure_does_not_stop_others to force one parse/write failure by
monkeypatching structure_parser.pipeline.orchestrator.parse_one (or the writer)
while keeping a second valid file in the input set, then assert the good file
still gets processed and stats.discovered_count reflects both discovery and
continued execution.
- Around line 61-62: The assertion in the pipeline orchestrator test is too weak
because it only checks one parsed file, so update the test around result.stats
in test_pipeline_orchestrator to explicitly verify both discovered files are
accounted for. Replace the duplicated parsed_count check with a condition that
reflects two successful parses or otherwise compares parsed_count against
discovered_count so the test fails if either file is not parsed.

---

Nitpick comments:
In `@design/2026-06-27-tech-note-article-triage-note.md`:
- Around line 516-533: MetadataEvidence is missing the future schema filename
field needed by the evidence contract, so update the MetadataEvidence dataclass
to include schema_name: str | None alongside schema_exists. Keep the new field
aligned with the existing evidence shape and use the existing symbols
MetadataEvidence and TriageDecision as the place to make the change so later
JSON evidence can carry the schema name without a breaking contract change.
- Around line 762-791: The test plan is missing explicit coverage for
reconciliation cases 2 and 6, so add dedicated fixtures and assertions to make
those paths unambiguous. Update the fixture table in the test plan with a case-2
example where valid metadata like articleType=howto has weak construction and
should keep metadata-driven triage, and a case-6 example where invalid metadata
is overridden by strong construction with an exact expected outcome. Also expand
tests such as test_triage_reconciliation and test_metadata_triage to parametrize
all seven reconciliation cases with clear threshold-boundary expectations, using
the existing diagnostic symbols SP-043, SP-044, SP-045, and SP-046 rather than
relying on vague fallback behavior.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: ebcbf97b-700a-4775-9ec8-45685789d20a

📥 Commits

Reviewing files that changed from the base of the PR and between 8cd8b97 and bb454a2.

📒 Files selected for processing (24)
  • design/2026-06-27-pipe-the-pipeline-SRS.md
  • design/2026-06-27-tech-note-article-triage-note.md
  • src/structure_parser/application/commands.py
  • src/structure_parser/cli.py
  • src/structure_parser/contracts/pipeline.py
  • src/structure_parser/pipeline/__init__.py
  • src/structure_parser/pipeline/discovery.py
  • src/structure_parser/pipeline/orchestrator.py
  • src/structure_parser/pipeline/output.py
  • src/structure_parser/pipeline/reporting.py
  • tests/fixtures/content_repo/guide/configure.md
  • tests/fixtures/content_repo/guide/install.md
  • tests/fixtures/content_repo/index.md
  • tests/fixtures/content_repo/reference/api.md
  • tests/integration/test_pipeline_cli.py
  • tests/integration/test_pipeline_dry_run.py
  • tests/integration/test_pipeline_end_to_end.py
  • tests/integration/test_pipeline_error_inventory.py
  • tests/unit/test_cli_pipeline_command.py
  • tests/unit/test_pipeline_contracts.py
  • tests/unit/test_pipeline_discovery.py
  • tests/unit/test_pipeline_orchestrator.py
  • tests/unit/test_pipeline_output.py
  • tests/unit/test_pipeline_reporting.py

Comment on lines +265 to +273
| Case | Behavior |
|---|---|
| Metadata valid, construction agrees | Select metadata type with high confidence. |
| Metadata valid, construction weak | Select metadata type with medium confidence and note weak construction evidence. |
| Metadata valid, construction strongly disagrees | Select the stronger evidence according to policy and emit a conflict diagnostic. |
| Metadata missing, construction strong | Select construction-inferred type. |
| Metadata missing, construction weak | Fall back to `topic` or `unknown` based on project policy. |
| Metadata invalid, construction strong | Select construction-inferred type and emit invalid metadata diagnostic. |
| Metadata invalid, construction weak | Fall back and emit both metadata and unknown/low-confidence diagnostics. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major

Inconsistent conflict resolution between section 5.8 and 12.6.

Section 5.8 states: "Metadata valid, construction strongly disagrees → Select the stronger evidence according to policy and emit a conflict diagnostic."

Section 12.6 case 3 hardcodes: always select construction type when gap > threshold, regardless of whether metadata confidence is higher.

If metadata confidence is 1.0 (exact match, schema exists) and construction score is 0.65 with gap 0.25, the design says pick construction (0.65) over metadata (1.0). This contradicts "select the stronger evidence according to policy." Clarify whether the gap threshold should override metadata confidence, or whether case 3 should compare absolute scores and pick the stronger evidence.

Also applies to: 676-684

🤖 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 `@design/2026-06-27-tech-note-article-triage-note.md` around lines 265 - 273,
The conflict-resolution rules are inconsistent between the summary table and the
detailed case in the article, especially around the metadata-vs-construction
decision logic. Update the section that defines the “metadata valid,
construction strongly disagrees” path so it matches the policy in the table by
explicitly stating how to choose the stronger evidence, and align the detailed
case logic in the relevant section with that rule instead of always preferring
construction when the gap threshold is met. Use the section headings and the
conflict-resolution case descriptions as the primary symbols to locate and
reconcile the mismatched guidance.

Comment on lines +221 to +223
report_path = config.effective_report_path()
CsvInventoryReporter().write(result, report_path)
_log_report_written(report_path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Handle CSV write failures before reporting success.

CsvInventoryReporter.write() returns PIPE_003 on OSError, but this path ignores that result and still logs pipeline.report.written, prints the report location, and can exit 0. An unwritable report path would therefore look like a successful pipeline run.

Suggested fix
-            CsvInventoryReporter().write(result, report_path)
-            _log_report_written(report_path)
+            report_error = CsvInventoryReporter().write(result, report_path)
+            if report_error is not None:
+                return f"Failed to write pipeline report: {report_path} ({report_error})", 1
🤖 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/structure_parser/application/commands.py` around lines 221 - 223, The CSV
export path in the command flow is treating CsvInventoryReporter.write() as if
it cannot fail, so report success is logged even when the write returns
PIPE_003. Update the logic around report_path, CsvInventoryReporter.write(), and
_log_report_written() to check the write result first, and only print/log
success when the write succeeds; if it fails, propagate or return the error
instead of exiting 0.

Comment on lines +156 to +158
log_format: Annotated[
str, typer.Option("--log-format", help="Log format: text or jsonl.")
] = "text",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

ast-grep outline src/structure_parser/cli.py --view expanded

Repository: mattbriggs/structured-markdown

Length of output: 863


🏁 Script executed:

sed -n '136,170p' src/structure_parser/cli.py

Repository: mattbriggs/structured-markdown

Length of output: 1555


🏁 Script executed:

sed -n '248,288p' src/structure_parser/application/commands.py

Repository: mattbriggs/structured-markdown

Length of output: 1859


🏁 Script executed:

rg -n "log_format|jsonl|typer\.Option|Choice|click\.Choice" src/structure_parser -S

Repository: mattbriggs/structured-markdown

Length of output: 2424


Reject unsupported --log-format values at the CLI boundary.

--log-format accepts any string, but _add_log_file_handler() only treats jsonl specially and falls back to text for everything else, so typos silently produce the wrong log format.

🤖 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/structure_parser/cli.py` around lines 156 - 158, The CLI option for log
formatting currently accepts any string, which lets invalid values silently fall
back to text in _add_log_file_handler(). Update the Typer option on the
log_format parameter in the CLI entrypoint to validate only the supported values
(text and jsonl), so unsupported inputs are rejected at the command boundary
instead of being treated as text.

Comment on lines +81 to +90
inputs: list[Path]
output_dir: Path
report_path: Path | None = None
include_patterns: list[str] = Field(default_factory=lambda: ["*.md", "*.markdown"])
exclude_patterns: list[str] = Field(default_factory=list)
log_file: Path | None = None
log_format: str = "text"
strict: bool = False
dry_run: bool = False
parser_config: ParserConfig = Field(default_factory=ParserConfig)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject empty inputs at model-validation time.

PipelineConfig(inputs=[], ...) currently validates, so Python callers can reach discovery with an invalid configuration and get a no-op run plus PIPE-004 instead of failing fast. Enforcing the minimum length here keeps the Python API aligned with the CLI contract.

Proposed fix
-class PipelineConfig(BaseModel):
+class PipelineConfig(BaseModel):
@@
-    inputs: list[Path]
+    inputs: list[Path] = Field(min_length=1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
inputs: list[Path]
output_dir: Path
report_path: Path | None = None
include_patterns: list[str] = Field(default_factory=lambda: ["*.md", "*.markdown"])
exclude_patterns: list[str] = Field(default_factory=list)
log_file: Path | None = None
log_format: str = "text"
strict: bool = False
dry_run: bool = False
parser_config: ParserConfig = Field(default_factory=ParserConfig)
inputs: list[Path] = Field(min_length=1)
output_dir: Path
report_path: Path | None = None
include_patterns: list[str] = Field(default_factory=lambda: ["*.md", "*.markdown"])
exclude_patterns: list[str] = Field(default_factory=list)
log_file: Path | None = None
log_format: str = "text"
strict: bool = False
dry_run: bool = False
parser_config: ParserConfig = Field(default_factory=ParserConfig)
🤖 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/structure_parser/contracts/pipeline.py` around lines 81 - 90,
PipelineConfig currently allows an empty inputs list, which lets invalid Python
callers proceed into discovery instead of failing at validation time. Update the
PipelineConfig model in the pipeline contract to enforce at least one input on
the inputs field, using the existing field definition so empty lists are
rejected before any run begins. Keep the change localized to the PipelineConfig
inputs declaration and align it with the CLI contract.

Comment on lines +79 to +98
def _add_file(
self,
path: Path,
source_root: Path,
config: PipelineConfig,
sources: list[DiscoveredSource],
) -> None:
if not self._matches_include(path.name, config.include_patterns):
return
if self._matches_exclude(path.name, config.exclude_patterns):
return
relative = path.relative_to(source_root)
sources.append(
DiscoveredSource(
source_root=source_root.resolve(),
source_path=path.resolve(),
relative_path=relative,
source_format=SourceFormat.markdown,
)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Explicit file inputs can admit non-Markdown files.

_add_file() never checks _MARKDOWN_SUFFIXES, so a caller can pass something like notes.txt with include_patterns=["*"] and it will still be emitted as SourceFormat.markdown. Folder discovery guards against this, but direct-file discovery does not.

Proposed fix
     def _add_file(
         self,
         path: Path,
         source_root: Path,
         config: PipelineConfig,
         sources: list[DiscoveredSource],
     ) -> None:
+        if path.suffix.lower() not in _MARKDOWN_SUFFIXES:
+            return
         if not self._matches_include(path.name, config.include_patterns):
             return
         if self._matches_exclude(path.name, config.exclude_patterns):
             return
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _add_file(
self,
path: Path,
source_root: Path,
config: PipelineConfig,
sources: list[DiscoveredSource],
) -> None:
if not self._matches_include(path.name, config.include_patterns):
return
if self._matches_exclude(path.name, config.exclude_patterns):
return
relative = path.relative_to(source_root)
sources.append(
DiscoveredSource(
source_root=source_root.resolve(),
source_path=path.resolve(),
relative_path=relative,
source_format=SourceFormat.markdown,
)
)
def _add_file(
self,
path: Path,
source_root: Path,
config: PipelineConfig,
sources: list[DiscoveredSource],
) -> None:
if path.suffix.lower() not in _MARKDOWN_SUFFIXES:
return
if not self._matches_include(path.name, config.include_patterns):
return
if self._matches_exclude(path.name, config.exclude_patterns):
return
relative = path.relative_to(source_root)
sources.append(
DiscoveredSource(
source_root=source_root.resolve(),
source_path=path.resolve(),
relative_path=relative,
source_format=SourceFormat.markdown,
)
)
🤖 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/structure_parser/pipeline/discovery.py` around lines 79 - 98, Explicit
file inputs are being accepted without verifying they are Markdown, so
_add_file() can emit non-Markdown paths like notes.txt as SourceFormat.markdown.
Update discovery._add_file to apply the same _MARKDOWN_SUFFIXES gate used by
folder discovery before creating DiscoveredSource, and only append when the path
matches a Markdown suffix in addition to the existing include/exclude checks.

Comment on lines +30 to +70
def write(self, result: PipelineRunResult, report_path: Path) -> str | None:
"""Write the CSV inventory report for a completed pipeline run.

:param result: Completed pipeline run result.
:param report_path: Output CSV path.
:returns: ``PIPE-003`` error code string on write failure, ``None`` on success.
:side effects: Creates parent directories and writes a UTF-8 CSV file.
"""
try:
report_path.parent.mkdir(parents=True, exist_ok=True)
with report_path.open("w", newline="", encoding="utf-8") as fh:
writer = csv.DictWriter(fh, fieldnames=CSV_FIELDS)
writer.writeheader()
for file_result in result.files:
writer.writerow(
{
"run_id": result.run_id,
"source_root": file_result.source.source_root.as_posix(),
"source_path": file_result.source.source_path.as_posix(),
"relative_path": file_result.source.relative_path.as_posix(),
"target_path": (
file_result.target_path.as_posix()
if file_result.target_path
else ""
),
"status": file_result.status,
"parser_codes": ";".join(file_result.parser_codes),
"pipeline_code": file_result.pipeline_code or "",
"error_count": file_result.error_count,
"warning_count": file_result.warning_count,
"duration_ms": f"{file_result.duration_ms:.1f}",
}
)
_log.info(
"pipeline.report.written",
extra={"report_path": str(report_path), "rows": len(result.files)},
)
return None
except OSError as exc:
_log.error("Failed to write report %s: %s", report_path, exc)
return PIPE_003

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make report-write failures impossible to ignore.

write() collapses failures to PIPE-003, but the provided src/structure_parser/application/commands.py:203-251 caller does not inspect that return and logs the report as written anyway. A bad report_path therefore loses the inventory artifact while the CLI still looks successful. Either raise here or require callers to turn PIPE-003 into a run diagnostic / non-zero exit.

🤖 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/structure_parser/pipeline/reporting.py` around lines 30 - 70, The report
write failure path in write() is currently easy to miss because it returns
PIPE_003, but the caller in the command flow ignores that status and still
reports success. Update write() and the surrounding reporting flow so a failed
report write is surfaced as a real failure: either raise from
InventoryReportWriter.write or have the caller check the PIPE_003 return, add a
run diagnostic, and fail the command with a non-zero exit. Make sure the fix is
applied consistently in InventoryReportWriter.write and the command handler that
invokes it.

Comment on lines +61 to +62
# Parser codes should be present (e.g. SP-011, SP-020)
assert rows[0]["parser_codes"] != "" or rows[0]["status"] == "parsed"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert parser_codes directly here.

This currently passes whenever the row status is "parsed", even if the CSV stops exporting parser diagnostics entirely. That makes the test miss the regression it is named for.

Suggested fix
-        assert rows[0]["parser_codes"] != "" or rows[0]["status"] == "parsed"
+        assert rows[0]["parser_codes"] != ""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Parser codes should be present (e.g. SP-011, SP-020)
assert rows[0]["parser_codes"] != "" or rows[0]["status"] == "parsed"
# Parser codes should be present (e.g. SP-011, SP-020)
assert rows[0]["parser_codes"] != ""
🤖 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 `@tests/integration/test_pipeline_error_inventory.py` around lines 61 - 62, The
integration test assertion in test_pipeline_error_inventory should verify
parser_codes directly instead of allowing a fallback on status == "parsed".
Update the check in the test around rows[0]["parser_codes"] so it explicitly
fails when parser diagnostics are missing, keeping the assertion tied to the
parser_codes field and the test's intended regression coverage.

Comment on lines +63 to +71
def test_exit_1_strict_with_warnings(self, tmp_path):
(tmp_path / "docs").mkdir()
# A file without front matter produces SP-011 (info) and possibly SP-020
# (warning). SP-020 is a warning so strict mode should raise exit 1.
(tmp_path / "docs/nowarn.md").write_text("No title here.\n\nJust text.\n")
_, code = _run(tmp_path, tmp_path / "docs", strict=True)
# strict=True: exit 1 if any warnings present
# (result depends on parser; we just check the code is valid)
assert code in (0, 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

This strict-mode test currently proves nothing.

assert code in (0, 1) will pass no matter how PipelineCommand.run() behaves. For a unit test of the strict exit-code branch, make the warning source deterministic (for example by stubbing the orchestrator result) and assert code == 1.

🤖 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 `@tests/unit/test_cli_pipeline_command.py` around lines 63 - 71, The
strict-mode test in test_exit_1_strict_with_warnings is currently
non-deterministic because it accepts either exit code, so it does not verify
PipelineCommand.run()’s strict branch. Make the warning input deterministic by
stubbing the orchestrator result or equivalent in the test setup, then assert
that the strict path returns code 1 when warnings are present. Use the existing
_run helper and PipelineCommand.run as the key symbols to update the test.

Comment on lines +61 to +62
assert result.stats.discovered_count == 2
assert result.stats.parsed_count + result.stats.parsed_count >= 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Tighten this assertion so it actually covers both files.

parsed_count + parsed_count >= 1 collapses to parsed_count >= 1, so this still passes if one of the two discovered files fails.

Suggested fix
         assert result.stats.discovered_count == 2
-        assert result.stats.parsed_count + result.stats.parsed_count >= 1
+        assert len(result.files) == 2
+        assert all(f.status != PipelineFileStatus.failed for f in result.files)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert result.stats.discovered_count == 2
assert result.stats.parsed_count + result.stats.parsed_count >= 1
assert result.stats.discovered_count == 2
assert len(result.files) == 2
assert all(f.status != PipelineFileStatus.failed for f in result.files)
🤖 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 `@tests/unit/test_pipeline_orchestrator.py` around lines 61 - 62, The assertion
in the pipeline orchestrator test is too weak because it only checks one parsed
file, so update the test around result.stats in test_pipeline_orchestrator to
explicitly verify both discovered files are accounted for. Replace the
duplicated parsed_count check with a condition that reflects two successful
parses or otherwise compares parsed_count against discovered_count so the test
fails if either file is not parsed.

Comment on lines +135 to +145
def test_one_failure_does_not_stop_others(self, tmp_path):
(tmp_path / "docs").mkdir()
(tmp_path / "docs/good.md").write_text(_MINIMAL_MD)
# A file that will be deleted before parsing simulates a race condition.
# Since the parser returns SP-001 for missing files (not an exception),
# we test with a missing explicit input path instead.
config = _make_config(tmp_path, tmp_path / "docs")
result = PipelineOrchestrator().run(config)
# Good file should still be discovered and attempted
assert result.stats.discovered_count >= 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

This test never exercises the failure path.

The setup only gives the orchestrator a valid directory with one valid file, so the assertion passes without proving that a failed file is isolated and the next file still runs. A small monkeypatch around structure_parser.pipeline.orchestrator.parse_one or the writer would make this test meaningful.

🤖 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 `@tests/unit/test_pipeline_orchestrator.py` around lines 135 - 145, The test
currently only covers a fully valid discovery path, so it never proves failure
isolation in PipelineOrchestrator.run. Update
test_one_failure_does_not_stop_others to force one parse/write failure by
monkeypatching structure_parser.pipeline.orchestrator.parse_one (or the writer)
while keeping a second valid file in the input set, then assert the good file
still gets processed and stats.discovered_count reflects both discovery and
continued execution.

@mattbriggs
mattbriggs merged commit 7b6e161 into main Jun 28, 2026
3 of 6 checks passed
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.

1 participant