Skip to content

Release 0.4.0: find_emails on ContaGen, native ICP-fit, typed columns, bulk CLI - #32

Merged
yudelevi merged 48 commits into
mainfrom
development
Sep 24, 2026
Merged

yudelevi merged 48 commits into
mainfrom
development

Conversation

@quantumdark

@quantumdark quantumdark commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Release 0.4.0 of discolike and discolike-cli. Full notes in CHANGELOG.md under 0.4.0.

  • Breaking: the ICP-fit verdict is title case on every surface (Yes / No).
  • contacts.generate(find_emails=True) / contacts generate --find-emails: the email finder runs inside the ContaGen task.
  • Native ICP-fit engine (integration_id=NATIVE_ICP_ENGINE), native ContaGen engine, typed columns and include_confidence on DiscoGen.
  • Sub-industry and geo filters on discover/count; sub_industry, coordinates and geo_precision on CompanyProfile.
  • CLI: agent output contract, stable error codes, file inputs (--domains-file, --params-file, --exclude-domains-file), discolike bulk companies|estimate|contacts.

Do not merge before the platform release that puts find_emails, the geo filters and the native engines in the production spec: the check job runs against prod for a PR into main.

RetriggerConfidence Score: 5/5

The PR appears safe to merge, with all previous findings resolved and no new actionable issues introduced since the previous review.

Summary

This release substantially expands the SDK and CLI with native ICP and contact-generation engines, typed DiscoGen columns, geographic and sub-industry filters, file-backed inputs, stable CLI error envelopes, and resumable bulk workflows. Changes since the previous review complete the requested fixes:

  • Resumed bulk company pulls now always re-exclude domains already present in the output CSV.
  • Geo validation now enforces paired coordinates, radius constraints, and the ten-shape aggregate limit.
  • File-reading failures are consistently converted into CLI validation errors.
  • The changelog conflict marker has been removed.

Reviews (2) · Last reviewed commit: "Merge pull request #33 from discolike/fi..."

yudelevi and others added 30 commits September 3, 2026 16:43
The platform now serializes address.state as the subdivision name and resolves
state filters against the selected countries, accepting a code or a name. The
generated request models carry those descriptions; no hand-written SDK code
changes, since state was never constrained client-side.

The contract job diffs against the production spec, so this lands red until
the platform change is deployed.
Refreshes went to the token endpoint without the RFC 8707 resource, so an
authorization server that applies a default resource to resource-less
requests could re-bind the refreshed REST token to another audience.
OAuthCredential now carries the resource from the code exchange, persists
it, and passes it to authlib's refresh_token. Credentials stored before
this release have no resource and keep the previous behavior.
fix(sdk): resend the OAuth resource on token refresh
FindEmailBatchRequest.source_query_id/refs/round are saved-query
plumbing regen picked up from a spec that's ahead of what the CLI
forwards; refs must be index-aligned with a stored query's contacts,
which the CLI never holds, so there's no flag to bind them to.
…y filters

The existing tests only checked field names and the null case, so a wrong
alias, a list-vs-scalar slip, or a bad response type would still pass. Add
populated-request serialization, a populated-response parse, and CLI flag
forwarding (including repeated sub-industry flags) for discover and count.
The API returns latitude/longitude, not lat/lon, so CompanyProfile was
silently dropping both on every profile it parsed. Neither name has
shipped in a release, so nothing downstream moves.

The lat/lon discover and count filters are the query surface and keep
their names.
The sub-industry and radius filters were hand-typed into the generated
module, so the regen check read them as drift and would have deleted
them on the next run: the SDK needs them before the platform release
that puts them in the spec. Declaring them as overlay properties makes
the generator produce what is committed, and the pending ones report
themselves once the spec supplies them.

sub_industry is pinned to a plain string array rather than following
the spec. The platform's enum holds parent-qualified keys only, and a
bare label like ROOFING gets rewritten to CONSTRUCTION/ROOFING by a
validator that runs server-side; generating that enum would reject
bare labels the API accepts.
CompanyProfile reads latitude/longitude, which no deployed API sends
yet, so until the platform release lands every profile would come back
with both unset. Accepting the old lat/lon keys as aliases lets the SDK
release on its own schedule instead of being pinned behind that one.
The contract check against the dev spec failed on /discover and /count:
the platform ships a bbox query param that neither params model declared,
so callers could only reach it through raw params. It goes into the
pending overlay alongside lat/lon/radius, which keeps generation from the
prod spec producing the same file until that release lands.

bbox is a string the server splits, so a transposed or out-of-range
corner reaches the API as an opaque 4xx. Validate the four numbers
client-side: latitudes within ±90, longitudes within ±180, min_lat no
greater than max_lat. Longitude order is left alone, since a box may wrap
the antimeridian with min_lon above max_lon. The validator lives on
DiscolikeRequest because the models are generated and cannot carry one.

Regenerating against the dev spec also picks up the states docs URL now
in the state filter descriptions.
The platform's parse_bbox already refuses degenerate boxes (min_lat equal
to max_lat, min_lon equal to max_lon) and non-finite values, and strips
spaces before parsing. The client-side check accepted all of those, so it
would have passed a box straight to a 4xx while claiming to validate, and
rejected a spaced box the API accepts.
feat: expose the sub-industry and geo-radius filters
Error envelopes on stderr now carry a stable snake_case `code` and the
`exit_code`, alongside the existing exception class name, message, and
status code, so agents branch on a contract rather than a Python class
name. `discolike --help` ends with the output contract, exit-code table,
environment variables, and jq examples; the sixteen commands agents reach
for most document their success JSON shape and common error codes.

Typer's rich formatter reflows epilog text into prose, which destroyed the
aligned tables, so ContractCommand/ContractGroup print the epilog verbatim
after the rich help. `--version` prints the bare CLI version when stdout is
not a TTY.
…n stdout, narrower auth_required

Three review findings on the agent output contract.

The console entry point is now `discolike_cli.main:run`, which invokes the
app with standalone_mode=False so click usage errors (unknown flag, bad
typed value, missing argument, BadParameter) become the documented
`{"code": "validation_error", ...}` envelope on stderr with exit 2 instead
of framework usage text. Help, --version, and the bare-invocation help
screen keep click's behaviour; Ctrl-C exits 130.

`auth login` wrote its success JSON to stderr, contradicting the contract
that success JSON goes to stdout; it now goes through emit(). Progress
lines (URL to open) stay on stderr.

`auth_required` was reported for every status-less AuthenticationError,
but the SDK raises the same class when a stored OAuth credential fails to
refresh. Only the no-credential message maps to auth_required now;
everything else is auth_invalid.
feat(cli): agent output contract in --help and stable error codes
…olume commands

Domain lists and whole-request JSON come from files so 2,000-domain contact
slices and 10,000-domain exclusion pages fit inside a plain discolike call.

- _inputs.py: read_domains_file (CSV 'domain' column or one per line,
  normalized and de-duplicated), read_params_file (JSON object), merge_domains
- _merge_params gains a params_file layer: file < --param < flags
- --domains-file on queries create-exclusion-list, contacts
  discover|search|count|generate, discogen run; validate-icp --file aliased
- --params-file on discover, count, contacts discover|search|count
- --exclude-domains-file on discover (SDK cap of 100 enforced locally)

Closes #23
…tHub Actions

Rich forces color and 80-column panels under Actions, so a substring
assertion on a typer usage error passes locally and fails on CI.
feat(cli): --domains-file, --params-file, --exclude-domains-file on volume commands
Ports the support-side discolike_pipeline.py mechanics into the CLI so the
skills plugin can drive volume pulls with plain discolike calls.

- companies: loop discover at up to 10k per page, save each page as an
  exclusion list <run-name>-round-N for the next page (tails under 20 ride
  on exclude_domain), append to --out per page, resume from that CSV,
  trim the last page to --max-companies, warn at exclusion capacity
- estimate: free contacts count over 1,000-domain slices, capped total
- contacts: slice at 10000 / per-company domains per contacts discover
  call, flatten to one row per contact, checkpoint slices in
  <out>.checkpoint, dedupe persona_id against the run and the existing CSV
- local SDK-model validation before the first billable call, sliding-window
  --rate-limit, retry on RateLimitError / 5xx above the transport's own
  retries, JSON summary on stdout, progress on stderr
- parity test exempts the bulk sites: vocabulary comes through
  --params-file / --param and the paging fields are managed

Closes #24
… null personas, rate limit

Addresses the five review findings on the bulk commands:

- contacts: the checkpoint is stamped with a fingerprint of the domains,
  --per-company and filters it was written for. A rerun with different
  inputs is refused (exit 2, pass --overwrite or another --out) instead of
  treating old slice indexes as done and silently skipping domains.
- contacts: a contact without a persona_id is always written; before, every
  such row shared the dedup key "None" and only the first survived.
- companies: short exclusion tails still ride on exclude_domain, but once the
  inline pile reaches the 20-domain minimum it becomes a saved list
  (<run-name>-round-N-tails), so exclude_domain never exceeds its 100 cap.
- estimate: the capped figure is reported as an upper bound; the cap applies
  to the total, not per company.
- --rate-limit takes a positive value on all three commands; 0 or negative
  used to crash the limiter with an IndexError.
feat(cli): discolike bulk companies|estimate|contacts
The platform's discovery filters stopped being one circle or one box:
`geo` is repeatable, `bbox` is repeatable, and every shape ORs. A caller
stuck on one shape had to fall back to raw params.

`bbox` widens to a list while still taking the single string it took
before, and the same validation the platform applies runs client-side so
a malformed shape fails before the request leaves. The ten-shape cap is
deliberately not copied here - the server owns that number.

`_generated/requests.py` is hand-edited ahead of the deploy, and
`gen_requests.py`'s pending schema matches it, so regenerating once the
platform ships is a no-op rather than drift.
NATIVE_ENGINE, the integration_id wire value and title_validation are
public API now, so pin them before a later regen quietly reshapes them.
Support the native ContaGen engine in contacts.generate
yudelevi and others added 16 commits September 18, 2026 11:57
The API now takes integration_id="native-icp" on /validate/icp and
/discogen/process to score against DiscoLike's own model instead of the
customer's BYOK LLM, so callers who have no LLM key — or don't want the
spend — can still qualify a domain list.

That run returns different columns (ICP Fit / ICP Score / Reasoning
instead of Fit / Confidence / Reasoning), and the submit response has
always said which set applies. The SDK was dropping everything but
task_id, leaving no way to read it short of guessing from the result
keys, so Job/AsyncJob now carry column_name.

The generated request models already accepted the sentinel — the field
is a plain str — so nothing needed loosening; their descriptions pick up
the new wording on the next regen against a deployed spec.
The native model emits one calibrated probability, so the API sends
reasoning back as null and the app grid shows N/A. Our docs left readers
expecting a sentence in that column, which is exactly the assumption the
column_name plumbing exists to prevent: someone reads "Reasoning" in the
native set, writes code that formats it, and gets None at runtime.

Spell out what each native column carries instead - ICP Fit as Yes/No at
a 0.50 threshold on ICP Score, the calibrated probability - and why the
null column is still there: dropping it would make the native result a
different shape from an LLM validation, and callers key off a stable set.

LLM validation docs are untouched; that run still returns a real
explanation.
The API's LLM validation schema was the last surface forcing a lowercase
verdict; it now returns Yes / No like the native engine, DiscoGen and the
app already did, so switching engine on the same endpoint no longer flips
the casing out from under an exact-string filter.

Breaking for clients matching on "yes".
Regenerated from the branch's own OpenAPI spec via gen_requests.py
--spec rather than edited by hand, since that file says not to. The
regen also picks up an integration_id description that had drifted since
native-icp shipped.

Ordering: contract.yml checks a PR targeting development against
DEV_SPEC_URL, so this needs the platform change on api-dev before the
SDK PR opens, not a prod deploy. Until then check_contract.py and
gen_requests.py --check both disagree, because the spec they read does
not carry the field yet.
Support the native ICP-fit model on validate_icp
DiscoGen's typed-column detector (which columns run through TypeSafe
judgment instead of generated prose) is picked server-side from the
query text; the client only opts in with a bool, same shape as
include_confidence which already made the trip last commit. Hand-added
to _generated/requests.py rather than regenerated, since api-dev does
not serve this field yet (same ordering constraint as include_confidence).
The flag has distinct true, false, and unset wire behavior but no
dedicated test exercised any of it for run or run-personas.
A PR stacked on a feature branch is as far ahead of prod as the branch it
targets, but base_ref only matched "development" literally, so the stack
fell through to the prod spec and reported every unreleased field as a
missing one. Fork PRs still get the prod spec, since they never receive
DEV_SPEC_URL.
# Conflicts:
#	CHANGELOG.md
#	packages/discolike-cli/tests/test_discogen_cli.py
Carry the typed_columns opt-in flag through the SDK and CLI
Carry include_confidence through the SDK and CLI
…acts generate

Mirrors the platform's new ContaGen option: the job runs the email finder over
every named, email-less row before completing and fills email / email_status
on those rows. Found addresses bill under the finder's rules; the rest of the
job stays unbilled.
feat: find_emails on ContactGenerateRequest and --find-emails on contacts generate
@yudelevi
yudelevi marked this pull request as ready for review September 24, 2026 00:10
Comment thread packages/discolike-cli/src/discolike_cli/bulk.py Outdated
Comment thread packages/discolike/src/discolike/_models.py
Comment thread packages/discolike-cli/src/discolike_cli/_inputs.py
Comment thread CHANGELOG.md Outdated
A leftover conflict marker from the find_emails rebase shipped in the
0.4.0 changelog.

bulk companies skipped re-excluding the resumed CSV whenever any
--exclusion-query-id was passed, assuming those IDs were the run's own
round lists. A customer suppression list is the common case, so the
next page could return only already-written companies and stop early.
The CSV is now always re-excluded.

File inputs caught only FileNotFoundError and JSON errors; a directory,
permission error or non-UTF-8 file escaped as a traceback instead of
the CLI error envelope.

The SDK mirrored the platform's per-shape geo rules but not the
cross-field ones (lat/lon pairing, radius without a centre, invalid
standalone radius, 10-shape total), so those failed only at the API.
@yudelevi
yudelevi merged commit e9e4a3e into main Sep 24, 2026
14 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.

2 participants