# Changelog of Form D Funding Leads: Private Placement Filings Feed (`fecklesspleb/form-d-funding-leads`) Actor

- **URL**: https://apify.com/fecklesspleb/form-d-funding-leads/changelog.md
- **Full Actor documentation**: https://apify.com/fecklesspleb/form-d-funding-leads.md

## Changelog

### Unreleased

- **Invalid input no longer FAILS the run** (2026-11-04, local, not built or pushed; `actor.json` version stays `0.1`). Input that `parse_input` rejects (e.g. a placeholder or malformed `contactEmail`, `mode: "weekly"`, a bad date, a reversed or too-long date range, `ciks: ["abc"]`, `by_company` without CIKs) used to call `Actor.fail` (run FAILED, exit 1, status message = the reason). The run now ends SUCCEEDED (exit 0) with one free status row `{"recordType": "status", "type": "status", "status": "error", "statusRow": true, "message": "Invalid input, nothing was fetched or charged: <reason>", "error": "<reason>", "charged": false, "scrapedAt": ...}` (pushed with no charged event) and the same message as the terminal status message. No SEC request is made, no filing is charged and no `RUN_SUMMARY` is saved. The prefix is "nothing was fetched or charged" (not "...and no results were charged") because this Actor's pricing has no start event; the reason texts are unchanged.
  - Wrong-type crashes (uncaught `TypeError`/`AttributeError`/`OverflowError`, exit 91) now give the same row with the reason `a field has the wrong type (<python error>).`
  - A whole input that is not a JSON object (a string, a number, a list of objects) used to crash (exit 91); it now gives the same row with the reason `the input must be a JSON object, got <type>.` An empty list or null input still means `{}` (sample mode).
  - Unchanged: sample mode without `contactEmail`, SEC blocking, temporary download failures and the existing "Not charged" status rows.
  - `requirements.txt`: `apify >= 4.0.0, < 5.0.0` pinned to `apify >= 4.1, < 4.2`.
  - Memory unchanged (default 256, min 128, max 1024, no defaultRunOptions). Pricing, default input, prefill and `exampleRunInput` unchanged.
  - Files: `my_actor/main.py` (new `end_invalid_input`, wrong-type wrap around `parse_input`, non-object check, one comment), `README.md` (contactEmail paragraph and a new "Invalid input" FAQ line), `requirements.txt`, `CHANGELOG.md`, `tests/test_form_d.py` (`test_invalid_email_still_fails_clearly` becomes `test_invalid_email_gives_free_invalid_input_row`), `tests/test_invalid_input.py` (new).

- Fixed: a live run whose window listed only amendments (D/A) with `includeAmendments` off reported `reason: "noMatches"` in its free status row (the message was already right). It now reports `reason: "amendments"`. The README now lists exactly the 7 reasons the code can emit (`noMatches`, `noNewFilings`, `amendments`, `filteredOut`, `fetchErrors`, `stoppedBySec`, `costLimit`); a test checks the README against the code.

- Console: the dataset overview view shows `recordType`, `status` and `message`, so free status rows are readable there.

- Fixed: a live run (contactEmail set) that saved nothing ended with an empty dataset. It now pushes one free status row (`recordType: "status"`, `statusRow: true`, `status` "warning"/"error", `reason`, counts, `message` starting "Not charged: ...") naming the case: no filings in the window (`noMatches`, with the index days tried or unknown CIKs), nothing new since the last run (`noNewFilings`), all filtered out (`filteredOut`, with checked/filtered/unreadable counts), temporary SEC fetch errors (`fetchErrors`, "error" when every download failed; retried next run), SEC block (`stoppedBySec`, "error") or a maximum cost below one filing (`costLimit`). The status message starts with the same text. Runs that save at least one filing or free parseError row are unchanged.

- Fixed: a filing whose download failed for a temporary reason (network error, HTTP 429/5xx after 5 tries, or another unexpected HTTP status) was saved as a free `parseError` row and remembered as seen, so scheduled runs never fetched it again. It is now skipped, not charged, not remembered, logged as a warning and counted in `fetchErrors` / `fetchErrorAccessions` in RUN_SUMMARY; the next run retries it. Truly unreadable filings (no Form D XML anywhere in the folder or full-submission text) are still saved free with `parseError`.

- Sample mode (no contactEmail) now says which filters it ignored: a warning in the log, `filtersIgnoredInSampleMode` and `samplesMatchingFilters` in RUN_SUMMARY, and a note in the status message. Samples are still not filtered (the Store prefill's filters match none of the 3 samples).

- Docs: the 31 of 221 `parseError` rows in the offline end-to-end test (and the 1 in the cost-cap test) are injected on purpose by tests/fake_sec.py (every filing with k % 7 == 6 has no XML); they are not parser failures.
