Skip to content

feat(cli): add featcopilot CLI for agentic usage#5

Merged
thinkall merged 30 commits into
mainfrom
feat/cli
May 6, 2026
Merged

feat(cli): add featcopilot CLI for agentic usage#5
thinkall merged 30 commits into
mainfrom
feat/cli

Conversation

@thinkall
Copy link
Copy Markdown
Owner

@thinkall thinkall commented May 3, 2026

Summary

Adds a stable, agent-friendly featcopilot command-line interface so FeatCopilot can be used from shells, CI pipelines, and agentic / LLM tool-use workflows without writing Python glue. All subcommands accept --json for machine-readable stdout; user-facing errors are written to stderr with a non-zero exit code so agents can parse failures deterministically.

Subcommands

Command Purpose
featcopilot info Print version, supported engines, selection methods, leakage guards, I/O formats, and a runtime parquet_available flag.
featcopilot transform Read CSV / Parquet / JSON, run AutoFeatureEngineer, write engineered features.
featcopilot explain Fit and print a JSON document with {name, explanation, code} per feature for LLM consumption. Selection is intentionally disabled.

Module form is also wired up: python -m featcopilot ....

Examples

# Discovery (machine-readable)
featcopilot info --json

# Feature engineering (CSV in/out, JSON status on stdout)
featcopilot transform \
    --input data.csv --target label --output features.csv \
    --engines tabular --max-features 50 --json

# JSON document for downstream LLM agents
featcopilot explain --input data.csv --target label

# Equivalent module invocation
python -m featcopilot info --json

Pass --config config.json to provide nested keys such as llm_config; explicit CLI flags override config values.

Parquet I/O. FeatCopilot's base install does not pin a parquet engine.
To use .parquet files (or pass parquet to --input-format /
--output-format), install pyarrow or fastparquet. info reports
"parquet_available": true only when an engine is importable in the
current environment.

Implementation notes

  • argparse only — no new dependencies.
  • Format detection from file extension (.csv / .parquet/.pq/.parq / .json) with explicit --input-format / --output-format override.
  • transform returns a structured JSON payload: status, input, output, input_format, output_format, n_rows, n_features, n_input_columns, n_generated_features, engines, selection_methods, max_features, target, selection_applied.
  • Exit codes: 0 success, 2 user/input/config error (with clean stderr message — no traceback), 1 unexpected error, 130 keyboard interrupt.
  • All user-facing failure modes (missing/unreadable file, directory passed where a file is expected, missing parquet engine, malformed JSON config, scalar type mismatches in config, missing --target when selection would actually run) are normalized into ValueError -> exit 2 instead of falling into the generic exit-1 backstop.

Engineer-config flags

Flag transform explain
--engines
--max-features ✓ (engine-level cap)
--selection-methods ✗ (selection disabled in explain)
--correlation-threshold ✗ (selection disabled in explain)
--leakage-guard
--gate-n-jobs
--config
--verbose / --no-verbose
--no-selection n/a
--include-target n/a

explain rejects --selection-methods and --correlation-threshold on the CLI and also skips reading those keys from --config (they are inert at runtime since explain disables selection).

Tests

  • tests/test_cli.py68 tests covering info (JSON/text + parquet engine probing), --version / --help, transform (CSV/Parquet/JSON round-trips, --no-selection, --include-target, format overrides), explain, --config precedence vs CLI flags, and all user-facing error paths (missing input/output, unknown target, unknown extension, malformed config / non-object root / directory / unreadable / non-dict llm_config, scalar-type mismatches, --no-verbose / --verbose overriding config bool, parquet engine missing on read/write, unwritable output, uncreatable parent dir, missing --target when selection would run, --target not required when selection is a no-op).
  • Full suite: 841 passed, 2 skipped, no regressions.
  • Coverage: featcopilot/cli.py at >90%; project total 89.22% (--cov-fail-under=85).

Files changed

  • featcopilot/cli.py (new) — argparse CLI.
  • featcopilot/__main__.py (new) — python -m featcopilot entry.
  • pyproject.toml[project.scripts] featcopilot = "featcopilot.cli:main".
  • tests/test_cli.py (new) — 68 tests.
  • README.md — new "Command-Line Interface" section.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Introduces a stable, agent-friendly CLI exposed via the `featcopilot`
console script (and `python -m featcopilot`). All subcommands accept
`--json` for machine-readable stdout; user-facing errors are written
to stderr with a non-zero exit code so agents (e.g. Copilot tool-use,
shell scripts, CI pipelines) can parse failures deterministically.

Subcommands:

* `info`      — print version, supported engines, selection methods,
                  leakage guards, and supported I/O formats.
* `transform` — read CSV / Parquet / JSON, run AutoFeatureEngineer,
                  write engineered features. Supports `--config` JSON,
                  `--engines`, `--max-features`, `--selection-methods`,
                  `--correlation-threshold`, `--leakage-guard`,
                  `--gate-n-jobs`, `--no-selection`, `--include-target`,
                  and explicit `--input-format` / `--output-format`
                  overrides. Emits a JSON status payload (rows, features,
                  engines, selection_applied, ...) when `--json` is set.
* `explain`   — fit AutoFeatureEngineer and print a JSON document with
                  `{name, explanation, code}` per feature so an LLM can
                  consume the result directly.

Files:

* `featcopilot/cli.py`      — argparse-based CLI (no new dependencies).
* `featcopilot/__main__.py` — enables `python -m featcopilot`.
* `pyproject.toml`          — `[project.scripts]` entry point.
* `tests/test_cli.py`       — 18 tests covering info/transform/explain,
                                CSV/Parquet/JSON round-trips, `--config`
                                handling and override precedence, and all
                                user-facing error paths.
* `README.md`                — new "Command-Line Interface" section.

Coverage: featcopilot/cli.py at 94 %; project total 88.95 %
(`--cov-fail-under=85`).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 3, 2026 06:06
@codecov
Copy link
Copy Markdown

codecov Bot commented May 3, 2026

Codecov Report

❌ Patch coverage is 92.51208% with 31 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.24%. Comparing base (37847ce) to head (99fe807).

Files with missing lines Patch % Lines
featcopilot/cli.py 92.73% 30 Missing ⚠️
featcopilot/__main__.py 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main       #5      +/-   ##
==========================================
+ Coverage   88.90%   89.24%   +0.34%     
==========================================
  Files          36       38       +2     
  Lines        3937     4351     +414     
==========================================
+ Hits         3500     3883     +383     
- Misses        437      468      +31     
Flag Coverage Δ
unittests 89.24% <92.51%> (+0.34%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

Copilot AI left a comment

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 introduces a new command-line interface for FeatCopilot so the library can be invoked from shells, CI, and agent/tool workflows without custom Python wrappers. It adds a package entry point plus tests and README guidance around the new info, transform, and explain commands.

Changes:

  • Add a new featcopilot argparse-based CLI with info, transform, and explain subcommands.
  • Wire up module/package entry points so the CLI can run via both featcopilot and python -m featcopilot.
  • Add CLI-focused tests and README documentation for command usage, formats, and config precedence.

Reviewed changes

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

Show a summary per file
File Description
featcopilot/cli.py Implements the new CLI, argument parsing, config loading, I/O handling, and subcommand execution.
featcopilot/__main__.py Adds python -m featcopilot module dispatch to the CLI entry point.
pyproject.toml Registers the new featcopilot console script.
tests/test_cli.py Adds coverage for CLI output modes, transform/explain flows, config precedence, and error paths.
README.md Documents the new CLI and example invocations.

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

Comment thread featcopilot/cli.py Outdated
Comment thread featcopilot/cli.py Outdated
Comment thread tests/test_cli.py
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f0b6616f4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread featcopilot/cli.py Outdated
Comment thread featcopilot/cli.py Outdated
Addresses all five review comments from Copilot and Codex on PR #5:

* explain now actually returns generated features
  (Copilot review #1, Codex P1).
  Built-in engines (e.g. tabular) populate `_feature_names` during
  `transform()`, not `fit()`. `_cmd_explain` now calls
  `fit_transform(..., apply_selection=False)` so the JSON payload
  contains the full `{name, explanation, code}` records the
  subcommand advertises. Test asserts `n_features > 0` for tabular.

* main(argv) -> int contract honored on parse errors
  (Copilot review #2).
  `argparse.parse_args` raises `SystemExit` for usage errors,
  `--help` and `--version`. `main` now traps those and returns
  the exit code so programmatic and agent callers always get an int.
  Tests cover `--version` (rc=0), `--help` (rc=0), no-subcommand
  (rc=2) and unknown-flag (rc=2).

* Real subprocess test for python -m featcopilot
  (Copilot review #3).
  `test_dunder_main_subprocess_invocation` and
  `test_dunder_main_subprocess_version_flag` spawn a real
  `python -m featcopilot ...` subprocess and assert stdout JSON,
  so a regression in `__main__.py` actually breaks the suite.

* Parquet `ImportError` -> clean exit 2 (Codex P2).
  `_read_table`/`_write_table` now wrap parquet calls and convert
  `ImportError` into a `ValueError` with a friendly install hint;
  the top-level handler routes that to the deterministic `exit 2`
  user-error path instead of the generic `exit 1` backstop.
  `test_transform_parquet_missing_engine_returns_exit_2` exercises
  this via `monkeypatch` of `DataFrame.to_parquet`.

* Pre-commit black: re-applied formatting from the pinned
  `black 24.1.1` hook (joined two long string raises) so the CI
  pre-commit job passes.

Tests: 23 (+5 new) in tests/test_cli.py, 796 passed full suite.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@thinkall
Copy link
Copy Markdown
Owner Author

thinkall commented May 3, 2026

@copilot please review the entire PR (not only the latest commit). Round-1 review comments from both Copilot and Codex have been addressed in commit b999555 and the diff against main reflects all CLI changes. Looking for any remaining concerns across eatcopilot/cli.py, eatcopilot/main.py, ests/test_cli.py, pyproject.toml, and the README CLI section.

@thinkall
Copy link
Copy Markdown
Owner Author

thinkall commented May 3, 2026

@codex review the whole PR (not only the latest commit). All round-1 comments from Copilot and Codex were addressed in b999555. Please re-evaluate the full diff against main across eatcopilot/cli.py, eatcopilot/main.py, ests/test_cli.py, pyproject.toml, and the README CLI section.

Copy link
Copy Markdown
Contributor

Copilot AI commented May 3, 2026

@copilot please review the entire PR (not only the latest commit). Round-1 review comments from both Copilot and Codex have been addressed in commit b999555 and the diff against main reflects all CLI ...

Full PR review complete. All three round-1 concerns were correctly addressed in b999555:

1. explain empty features (cli.py:303)
_cmd_explain now calls engineer.fit_transform(…, apply_selection=False) before reading get_feature_names() / explain_features() / get_feature_code(). The docstring on lines 283–293 explicitly documents why fit_transform (not fit) is required. test_explain_emits_json_payload asserts n_features > 0 and validates the {name, explanation, code} structure.

2. parse_args() before try/except (cli.py:467–481)
parser.parse_args(argv) is now inside the same try/except SystemExit block as the rest of main(). --help returns 0, --version returns 0, missing subcommand returns 2, unknown flag returns 2 — all confirmed by test_no_subcommand_exits_nonzero, test_unknown_flag_returns_exit_2, and test_help_flag_returns_zero.

3. __main__.py not actually exercised (test_cli.py:447–481)
Added two subprocess tests — test_dunder_main_subprocess_invocation (runs python -m featcopilot info --json in a real subprocess and validates the JSON payload) and test_dunder_main_subprocess_version_flag — so a broken import path in __main__.py would fail the suite.

No remaining concerns across the five changed files:

  • featcopilot/cli.py — implementation is clean; exit codes (0/1/2/130) and JSON/text output modes are correct; _detect_format handles input and output consistently; error messages go to stderr without tracebacks.
  • featcopilot/__main__.py — minimal and correct.
  • tests/test_cli.py — 22/23 tests pass (1 skipped: pyarrow not installed in this env). Covers all subcommands, config/flag precedence, format detection, error paths, and real subprocess invocation of python -m featcopilot.
  • pyproject.toml[project.scripts] entry is correct.
  • README.md — documentation section is accurate and matches the implemented flags.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@thinkall
Copy link
Copy Markdown
Owner Author

thinkall commented May 3, 2026

Round-1 review feedback from both Copilot and Codex (5 comments total) has been addressed in commit b999555 and all 5 review threads have been replied to and resolved. CI is green (pre-commit + tests on Python 3.10/3.11/3.12/3.13 + codecov).

Codex re-reviewed and explicitly confirmed Didn't find any major issues. (#5 (comment)).

@copilot-pull-request-reviewer please perform a fresh full-PR review against the current head (b999555), covering the entire diff against \main\ — not only the latest commit.

No code changes. Triggers another round of automated review from the
Copilot/Codex review bots so the full PR (not just the latest commit)
can be re-evaluated against the current head after round-1 fixes in
b999555 (which addressed all 5 review comments and resolved all 5
review threads).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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


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

Comment thread README.md Outdated
Comment thread featcopilot/cli.py Outdated
Comment thread pyproject.toml
Comment thread featcopilot/cli.py Outdated
Comment thread featcopilot/cli.py Outdated
Addresses all five new review comments from copilot-pull-request-reviewer
on PR #5:

* info now reflects parquet engine availability at runtime.
  `_parquet_engine_available()` probes `pyarrow` / `fastparquet`;
  parquet is included in `supported_input_formats` /
  `supported_output_formats` only when an engine is importable. Adds a
  new `"parquet_available"` boolean in the payload so agents can branch
  on it deterministically. Tests cover both the engine-present and
  engine-missing cases via `monkeypatch`.

* _build_engineer no longer mangles misconfigured configs.
  Removed the `list(...)` coercion of `engines` /
  `selection_methods`: a misconfigured `"engines": "tabular"` (string)
  used to be silently expanded into `['t','a','b','u','l','a','r']` and
  bubble up as a confusing "unknown engines" error. It now flows straight
  into `AutoFeatureEngineer.__init__`'s precise type validation
  ("engines must be a list or tuple of strings"), surfaced via the
  standard exit-2 user-error path.

* Empty config lists now produce the documented exit 2.
  Replaced `pick(...) or [default]` with a tri-state `pick` that
  honors explicit empty values from the config: `"engines": []` /
  `"selection_methods": []` now propagate into the transformer where
  `_validate_configuration` raises "must contain at least one ..." and
  the CLI returns exit 2 — instead of being silently rewritten into the
  defaults.

* README parquet caveat. Switched the example to CSV/CSV (no surprise
  dependency for base installs) and added an explicit note that parquet
  I/O requires installing `pyarrow` or `fastparquet`, with a pointer
  to `info`'s `parquet_available` flag for runtime detection.

* Console script test. New `test_console_script_subprocess_invocation`
  and `test_console_script_version_flag` use `shutil.which` to locate
  the installed `featcopilot` script and run it through `subprocess`;
  a typo or packaging regression in `[project.scripts]` now actually
  breaks the suite. Tests skip cleanly when the script is not on PATH.

Tests: 30 (+7 new) in tests/test_cli.py, 803 passed full suite.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@thinkall
Copy link
Copy Markdown
Owner Author

thinkall commented May 3, 2026

Round-2 review feedback (5 new comments from copilot-pull-request-reviewer at 07:27Z) has been addressed in 92fcd09 and all 5 round-2 threads have been replied to and resolved. CI is running.

@codex review the whole PR (not only the latest commit). Diff against \main\ covers \ eatcopilot/cli.py, \ eatcopilot/main.py, \ ests/test_cli.py, \pyproject.toml, and the README CLI section.

@copilot-pull-request-reviewer please re-review the entire PR against the current head (92fcd09) — not only the latest commit.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 92fcd090fe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread featcopilot/cli.py Outdated
Comment thread featcopilot/cli.py Outdated
Addresses both new comments from chatgpt-codex-connector on PR #5
(commit 92fcd09):

* Validate `llm_config` is a JSON object (Codex P2).
  `_build_engineer` now type-checks the `llm_config` value from
  `--config` before forwarding it. A non-mapping (e.g. a string)
  previously made it all the way into `AutoFeatureEngineer._create_engine`
  where `self.llm_config.get(...)` raised `AttributeError` — bypassing
  the structured exit-2 user-error path and surfacing as exit 1
  `unexpected error`. The new check raises a precise `ValueError` so
  the CLI returns exit 2 with a clean stderr message.
  Test `test_non_dict_llm_config_returns_exit_2` covers this.

* Normalize `--config` user-input mistakes to exit 2 (Codex P3).
  Pointing `--config` at a directory used to raise
  `IsADirectoryError` from `path.open(...)` and fall into the generic
  exit-1 backstop. `_load_config` now:
  - rejects directories explicitly,
  - converts `json.JSONDecodeError` into a clean
    "is not valid JSON" message,
  - converts other read errors (`OSError`) into a clean
    "could not be read" message.
  All paths return exit 2 so automation can handle config errors
  consistently. Tests `test_directory_as_config_returns_exit_2` and
  `test_malformed_json_config_returns_exit_2` cover the new branches.

Tests: 33 (+3 new) in tests/test_cli.py, 806 passed full suite.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 3, 2026 10:34
@thinkall
Copy link
Copy Markdown
Owner Author

thinkall commented May 3, 2026

Round-3 review feedback (2 P2/P3 comments from Codex on 92fcd09) has been addressed in 0072e5b and both threads have been replied to and resolved.

@codex review the whole PR (not only the latest commit). Diff against \main\ covers all CLI files.

@copilot-pull-request-reviewer please re-review the entire PR against the current head (0072e5b).

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.


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

Comment thread featcopilot/cli.py Outdated
Comment thread featcopilot/cli.py
Addresses all three new comments from round-19 reviewers on PR #5
(commit f710fbe):

* No false sampling warning at exact-boundary (Codex gZs1 P3,
  Copilot gmOw — same root cause). The previous `n_sampled >=
  sample_size` check fired the "metadata may differ" warning even
  when the input had EXACTLY `sample_size` rows (no truncation
  actually happened), causing agents to misinterpret an unsampled
  run as degraded metadata.

  `_cmd_explain` now reads with `nrows = sample_size + 1` so the
  returned length is a strict proof of the file size relative to the
  cap: `len(df) > sample_size` means at least one row was dropped.
  The warning fires only in that case; the post-read truncation to
  exactly `sample_size` happens at the same time. CSV memory bound
  is preserved (`pd.read_csv(nrows=N+1)` still caps at N+1, no full
  load). Three new tests cover the three boundary cases:
  - input exactly = cap -> no warning, n_rows_used == cap
  - input < cap         -> no warning, n_rows_used == input size
  - input = cap + 1     -> warning fires, n_rows_used == cap

* --explain-sample-size help text describes actual semantics
  (Copilot gmPC). The help previously said "deterministic seed",
  implying a seeded random sample, but the implementation is now a
  deterministic head slice (`read_csv(nrows=N)` / `df.iloc[:N]`).
  The help and the docstring both updated to say "deterministic head
  slice (the first N rows of the input)" and to call out explicitly
  that this is NOT a random sample. New
  `test_explain_sample_size_help_text_describes_head_slice_not_random_seed`
  asserts the new wording and a regression guard against the old.

Tests: 116 (+4 new) in tests/test_cli.py, 889 passed full suite.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@thinkall
Copy link
Copy Markdown
Owner Author

thinkall commented May 5, 2026

Round-19 review feedback (1 Codex P3 + 2 Copilot, 3 threads on commit f710fbe) has been addressed in bfb5da8 and all 3 threads have been replied to and resolved.

@copilot please review the entire PR (not only the latest commit) — full diff against main covers all CLI files.

@codex review the whole PR (not only the latest commit). Diff against main covers all CLI files.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bfb5da8373

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread featcopilot/cli.py Outdated
Addresses the Codex P2 comment on PR #5 (commit bfb5da8):

* Reject empty / header-only inputs with exit 2 (Codex gtYr).
  When a CSV has headers but zero data rows, `pd.read_csv` returns
  an empty DataFrame rather than raising `EmptyDataError`. Same for
  an empty JSON array (`[]`) or a parquet file with schema but zero
  rows. The CLI then passed that frame into `TabularEngine`, which
  divides by `len(X)` while fitting categorical encoding and the
  command exited via the generic exit-1 `unexpected error` path
  instead of a clean user-input error.

  `_read_table` now performs an explicit `df.empty` check after
  every supported format read and raises `ValueError("Input file ...
  is empty (zero data rows). Feature engineering requires at least
  one row of data.")` -> exit 2 with a precise stderr message.

  Four new tests cover the new branch:
  - header-only CSV (transform)
  - empty JSON array (transform)
  - parquet with schema but zero rows (transform)
  - header-only CSV (explain)

Tests: 120 (+4 new) in tests/test_cli.py, 893 passed full suite.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@thinkall
Copy link
Copy Markdown
Owner Author

thinkall commented May 5, 2026

Round-20 review feedback (Codex P2 on commit bfb5da8) has been addressed in 167b490 and the thread has been replied to and resolved.

@copilot please review the entire PR (not only the latest commit) — full diff against main covers all CLI files.

@codex review the whole PR (not only the latest commit). Diff against main covers all CLI files.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.


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

Comment thread featcopilot/cli.py Outdated
Comment thread featcopilot/cli.py
@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@thinkall
Copy link
Copy Markdown
Owner Author

thinkall commented May 5, 2026

All previous review comments and check failures have been addressed and resolved (60 review threads resolved, 0 unresolved). The PR is currently green: all 7 CI checks pass, 120 CLI tests in tests/test_cli.py, 893 passed full suite, 25 commits on feat/cli (head: 167b490).

@copilot please review the entire PR (not only the latest commit) — full diff against main covers featcopilot/cli.py, featcopilot/__main__.py, tests/test_cli.py, pyproject.toml, and the README CLI section.

@codex review the whole PR (not only the latest commit). Diff against main covers all CLI files.

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.


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

Comment thread featcopilot/cli.py Outdated
Addresses all three new comments from copilot-pull-request-reviewer
on PR #5 (commits 167b490 + 5cbb843):

* Drop the over-broad single-active-capture fallback (Copilot hFmA).
  Round-18's `_ThreadCaptureState.get` fallback routed records
  from ANY thread to the single in-flight CLI capture whenever
  exactly one capture was active. As Copilot pointed out, that was
  too broad: unrelated background work using `featcopilot` in the
  same process would have its log/warning output silently swallowed
  into the active CLI command's payload, with no ownership tie back
  to the command. `get` is now strictly per-thread; cross-thread
  records (e.g. ThreadPoolExecutor workers) bleed onto stderr like
  any other background log. The class docstring documents the
  trade-off and the existing options for callers who really need
  every worker record captured. The previous worker-routing test
  was inverted into `test_capture_does_not_route_unrelated_thread_records`,
  which guards against the regression.

* Structured single-line argparse error format (Copilot hFmP).
  `argparse`'s default `error()` writes the multi-line usage
  banner (`usage: featcopilot ...`) PLUS `prog: error: ...` to
  stderr before raising `SystemExit`. `main()` was only converting
  the exit code; the banner still appeared on stderr, breaking the
  documented "stderr carries one `featcopilot: error: ...` line per
  failure" contract. New `_StructuredArgumentParser` overrides
  `error()` to emit exactly one `featcopilot: error: <message>`
  line with no banner, and is wired into both the top-level parser
  and all subparsers via `parser_class=_StructuredArgumentParser`.
  Two new tests cover the contract: single-line stderr for an
  unknown flag and for a missing subcommand, plus a regression guard
  against the `usage:` substring appearing in stderr.

* Eliminate double truncation warning for parquet/JSON sample
  (Copilot iH8R). `_cmd_explain` was passing `nrows=sample_size+1`
  to `_read_table` so it could detect truncation. For parquet/JSON
  `_read_table` then emitted its own post-read warning saying
  "truncating to N+1" and trimmed to that, after which `_cmd_explain`
  trimmed again to N and emitted a second warning. Two near-duplicate
  warnings with off-by-one numbers — confusing to agents.

  The fix:
  - `_read_table` now accepts `suppress_truncation_warning=True`
    so callers that emit their own consolidated message can silence
    its post-read notice.
  - `_cmd_explain` only passes `nrows` to `_read_table` when
    pandas can natively bound the read (CSV with `nrows=N+1`).
    For parquet/JSON it reads with `nrows=None` (full file) and
    handles both detection and trimming itself.
  - The single `_cmd_explain` UserWarning now uses the user-facing
    `sample_size` value, AND for parquet/JSON includes the
    "memory wasn't bounded" caveat as a single sentence — same
    information, no duplication. Test
    `test_explain_sample_size_warns_post_read_for_parquet` asserts
    exactly one truncation notice and that it references `100 rows`
    (not the internal +1).

Tests: 122 (+2 net) in tests/test_cli.py, 895 passed full suite.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@thinkall
Copy link
Copy Markdown
Owner Author

thinkall commented May 5, 2026

Round-22 review feedback (3 comments from auto Copilot reviewer on commits 167b490 and 5cbb843, missed by my previous first:60 GraphQL pagination) has been addressed in 016fe34 and all 3 threads have been replied to and resolved.

@copilot please review the entire PR (not only the latest commit) — full diff against main covers all CLI files.

@codex review the whole PR (not only the latest commit). Diff against main covers all CLI files.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.


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

Comment thread featcopilot/cli.py Outdated
Addresses the new comment from copilot-pull-request-reviewer on
PR #5 (commit 016fe34):

* Restore narrow LLM-only cross-thread fallback (Copilot osJO).
  Round-22 removed the single-active-capture fallback in response to
  Copilot's complaint that it was too broad (would misattribute
  unrelated background `featcopilot` work). Round-23 Copilot now
  flags that the strict per-thread routing breaks the
  "stderr is reserved for failures" contract for LLM-backed runs in
  event-loop environments — the sync LLM clients in
  `featcopilot/llm/*_client.py` fall back to `ThreadPoolExecutor`
  there, and their mock-mode startup warnings emit from worker
  threads that `submit()` spawns.

  Resolution: a *narrow* fallback that satisfies both reviewers.
  `_ThreadCaptureState` now exposes `get_for_llm_record(tid,
  logger_name)`: it returns the calling thread's capture if any,
  otherwise — and only when the record originates from
  `featcopilot.llm.*` AND exactly one capture is active in the
  process — it routes to that single capture. Multiple concurrent
  captures keep strict isolation (no LLM cross-talk either). Records
  from non-LLM featcopilot loggers on cross-threads still flow to
  stderr as before, so unrelated background work is NOT
  misattributed.

  Both `_ThreadRoutingHandler.emit` and
  `_SuppressCapturingFilter.filter` go through
  `get_for_llm_record`, so the routing handler and the stderr-
  suppression filter stay in lockstep: anything captured is also
  suppressed from the original handlers.

  Three tests cover the policy:
  - `test_capture_does_not_route_unrelated_thread_records`: non-LLM
    record from a cross-thread is NOT captured (regression guard
    against round-22's "too broad" complaint).
  - `test_capture_routes_llm_client_worker_records_to_single_active_capture`:
    LLM record from ThreadPoolExecutor + raw threading.Thread workers
    IS captured.
  - `test_capture_does_not_apply_llm_fallback_with_multiple_captures`:
    even for LLM records, two concurrent captures stay strictly
    isolated.

Tests: 124 (+2 net) in tests/test_cli.py, 897 passed full suite.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@thinkall
Copy link
Copy Markdown
Owner Author

thinkall commented May 5, 2026

Round-23 review feedback (1 comment from auto Copilot reviewer on commit 016fe34) has been addressed in 298d4de. The strict per-thread isolation introduced in round-22 was conflicting with round-23''s observation about LLM sync clients in event-loop environments — both concerns are now satisfied by a narrow featcopilot.llm.*-only single-active-capture fallback.

@copilot please review the entire PR (not only the latest commit) — full diff against main covers all CLI files.

@codex review the whole PR (not only the latest commit). Diff against main covers all CLI files.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 298d4def60

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread featcopilot/cli.py Outdated
Round-23's `test_capture_does_not_apply_llm_fallback_with_multiple_captures`
passed locally but failed in CI on Python 3.10/3.12/3.13. Root cause: a
race between the two threads. Without explicit barriers, one thread's
`ThreadPoolExecutor` worker could fire BEFORE the other thread had
pushed its capture onto `_state._per_thread`. At that moment
`len(_per_thread) == 1`, so the narrow LLM fallback (correctly)
routed the worker's record to the only active capture — which is what
the test was checking the multi-capture scenario does NOT do.

Replaced the single `Barrier(2)` with a three-phase protocol:
  - `enter_barrier`: both threads start at the same time
  - `inside_barrier`: BOTH have pushed their capture (so any
    subsequent emit sees `len == 2`)
  - `done_barrier`: BOTH workers have completed before EITHER
    exits its capture (pins `len == 2` for the entire worker
    window)

The contract under test — multi-capture isolation — is unchanged; the
test now reliably exercises the multi-capture branch of
`_ThreadCaptureState.get_for_llm_record`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 5 changed files in this pull request and generated 2 comments.


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

Comment thread featcopilot/cli.py Outdated
Comment thread tests/test_cli.py
Round-24 reviewer feedback addressed:

1. **Codex P1 — atomic routing/suppression decision.**
   `_SuppressCapturingFilter.filter()` and `_ThreadRoutingHandler.emit()`
   each independently called `state.get_for_llm_record(...)`. Under
   concurrent CLI calls, another thread could push or pop a capture
   between filter and emit so the two phases saw different state — the
   same record could end up both captured AND on stderr (or suppressed
   without being captured). Replaced both call sites with
   `state.resolve_for_record(record)`, which computes the decision
   once and caches it on the record itself; the two phases now always
   see the same answer.

2. **Copilot — narrow LLM whitelist was too broad.**
   `_LLM_FALLBACK_LOGGER_PREFIXES = ("featcopilot.llm.",)` matched
   every `featcopilot.llm.*` logger, including modules that never
   hop onto worker threads (`semantic_engine`, `code_generator`,
   `transform_rule_generator`, `explainer`). Replaced with an
   *exact* set `_LLM_FALLBACK_LOGGER_NAMES` containing only the
   three sync-client modules that actually fall back to
   `ThreadPoolExecutor` (`copilot_client`, `litellm_client`,
   `openai_client`).

3. **Copilot — `_run` did not actually capture handler output.**
   `featcopilot/utils/logger.py` installs `StreamHandler(sys.stderr)`
   at import time, so `redirect_stderr` only swapped the module
   attribute while the handler kept writing to the *original* stream.
   That made the file's stderr-cleanliness assertions vacuous. The
   `_run` helper now also temporarily re-points every
   `StreamHandler` on the `featcopilot` logger at the captured
   `err` buffer for the duration of the call, restoring the original
   `stream` attribute in `finally`.

New tests:
- `test_capture_does_not_apply_llm_fallback_for_non_whitelisted_llm_loggers`
  pins the exact-set whitelist (semantic_engine et al. stay strictly
  isolated).
- `test_capture_decision_is_cached_per_record_for_atomic_filter_emit`
  proves the resolver computes the decision exactly once per record.
- `test_capture_decision_stable_under_concurrent_pop_between_filter_and_emit`
  proves the cached decision survives a concurrent pop.
- `test_run_helper_redirects_featcopilot_stream_handlers` proves the
  test helper captures handler-stream writes (so leaks would be
  detectable, not silently passing).

Existing test `test_capture_routes_llm_client_worker_records_to_single_active_capture`
updated to use a whitelisted logger name (`openai_client`) since
arbitrary `featcopilot.llm.*` names no longer trigger the fallback.

901 tests pass locally (full suite); 128 in tests/test_cli.py.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@thinkall
Copy link
Copy Markdown
Owner Author

thinkall commented May 5, 2026

Round 24 push at 1005009 addresses all three previously-unresolved threads:

  • Codex P1 (atomic filter/emit): _ThreadCaptureState.resolve_for_record caches the decision on the record itself; filter and emit now always agree even under concurrent push/pop.
  • Copilot (whitelist too broad): replaced prefix match with an exact frozenset of the three sync-client module names; semantic_engine/code_generator/transform_rule_generator/explainer stay strictly isolated.
  • Copilot (_run doesn't capture): the test helper now also redirects every featcopilot StreamHandler's stream to the captured err for the duration of the call.

Local: 901 tests pass (full suite), 128 in tests/test_cli.py.

@copilot @codex please re-review the WHOLE PR (not just the latest commit) — the round-23/24 capture changes touch featcopilot/cli.py and tests/test_cli.py. Thanks!

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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


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

Comment thread featcopilot/cli.py
Comment thread featcopilot/cli.py Outdated
Comment thread featcopilot/cli.py Outdated
…ut cases

Round-25 reviewer feedback addressed:

1. **Copilot — empty-input branch conflated zero-row and zero-column.**
   `_read_table` raised the same "zero data rows" error for both
   `len(df) == 0` and `len(df.columns) == 0` (both make
   `DataFrame.empty` return `True`). A JSON `[{}, {}]` array
   produces a frame WITH rows but NO columns ù very different user
   error than a header-only CSV. Split into two distinct, accurately
   worded error messages: "no columns" vs "zero data rows".

2. **Copilot — read-time warnings leaked to stderr.**
   `_cmd_transform` only opened the capture context around
   `engineer.fit_transform`. `pd.read_csv` can legitimately emit
   `DtypeWarning` on mixed-type CSVs and parquet/JSON readers can
   emit pyarrow / pandas warnings on a successful read; those were
   bypassing the capture and bleeding to stderr, breaking the
   "stderr reserved for failures" contract.

3. **Copilot — write-time warnings also leaked to stderr.**
   `_write_table` was likewise outside the capture, so pandas /
   pyarrow `FutureWarning` / `UserWarning` from a successful
   write could leak. Same contract violation as #2.

The fix wraps the entire `_cmd_transform` pipeline (read +
build_engineer + fit_transform + write) in a single
`_capture_featcopilot_messages` block so warnings from ANY phase end
up in the JSON `warnings` field instead of stderr. `_cmd_explain`
also widened: `explain_features` / `get_feature_code` are now
inside the same capture as the read+fit_transform.

The dead helper `_fit_transform_capturing_warnings` is no longer used
internally; kept as a thin convenience wrapper for external test code
with an updated docstring noting that the CLI now wraps a wider
region.

New tests:
- `test_transform_zero_columns_input_distinguishes_from_zero_rows` ù
  pins the column-vs-row error-message distinction.
- `test_transform_zero_rows_input_still_uses_zero_rows_message` ù
  guards the existing zero-rows wording for the header-only case.
- `test_transform_read_warning_captured_not_on_stderr` ù patches
  `pd.read_csv` to emit a warning, asserts `err == ""` and the
  warning lands in JSON `warnings`.
- `test_transform_write_warning_captured_not_on_stderr` ù patches
  `DataFrame.to_csv` to emit a warning, same contract.
- `test_explain_features_warnings_captured_not_on_stderr` ù patches
  `AutoFeatureEngineer.explain_features` to emit a warning, same
  contract for `_cmd_explain`.

906 tests pass locally (full suite); 133 in tests/test_cli.py.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@thinkall
Copy link
Copy Markdown
Owner Author

thinkall commented May 5, 2026

Round 25 push at 99fe807 addresses all three previously-unresolved threads:

  • _read_table empty-input check: split into "no columns" vs "zero data rows" with distinct, accurate error messages.
  • _cmd_transform warning capture widened to span _read_table ΓåÆ _write_table (was previously only around fit_transform), so read-time and write-time pandas/pyarrow warnings are now in the JSON warnings field instead of stderr.
  • _cmd_explain widened so explain_features / get_feature_code are also inside the capture.

Local: 906 tests pass (full suite); 133 in tests/test_cli.py.

@copilot @codex please re-review the WHOLE PR (not just the latest commit). Thanks!

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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


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

Comment thread featcopilot/cli.py
Comment on lines +211 to +213
except ValueError as exc:
raise ValueError(f"Failed to read JSON from {str(path)!r}: {exc}") from exc
except OSError as exc:
Comment thread featcopilot/cli.py
Comment on lines +340 to +349
try:
with path.open("r", encoding="utf-8") as fh:
data = json.load(fh)
except json.JSONDecodeError as exc:
raise ValueError(f"Config file {config_path!r} is not valid JSON: {exc}") from exc
except OSError as exc:
# Catch-all for unreadable files (permission denied, broken symlink,
# etc.). Surface as a user-facing error rather than the generic
# exit-1 backstop.
raise ValueError(f"Config file {config_path!r} could not be read: {exc}") from exc
Comment thread featcopilot/cli.py
Comment on lines +865 to +878
with _install_lock:
# Logger handler/filter install (truly once — these can't be
# silently undone by external code in the way ``warnings.showwarning``
# can).
if not _install_done:
fc_root = logging.getLogger("featcopilot")
if _routing_handler not in fc_root.handlers:
fc_root.addHandler(_routing_handler)
for handler in list(fc_root.handlers):
if handler is _routing_handler:
continue
if _suppress_filter not in handler.filters:
handler.addFilter(_suppress_filter)
_install_done = True
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.

4 participants