Skip to content

signal: Hiring Freeze #5

Description

@LXBStudioLLC

Spec

Tier: Probabilistic
Name: Hiring Freeze

Flag a posting when the employer recently filed a public WARN Act layoff notice. That is a strong correlation that a role listed now may be a ghost: posted during or after a workforce reduction, a backfill that never happens, or a listing left up.

Data source (public, no auth, no scraping)

U.S. state WARN Act notices. The Worker Adjustment and Retraining Notification Act requires employers with 100 or more employees to give 60 days notice of mass layoffs or plant closings, and state labor agencies publish those notices (for example California EDD WARN, New York DOL WARN, Washington ESD, Texas Workforce Commission). These are public government datasets. Read-only, no login, no scraping.

Do not use Layoffs.fyi (no public API, so consuming it means scraping, which breaks Hard Constraint #2), NewsAPI (needs a key), SEC EDGAR scraping, or anything behind authentication.

Each WARN notice gives you: employer name, state, notice or effective date, and number of workers affected. Role-level detail usually is not available. This is employer-level data.

Approach

  1. The signal reads a local cached WARN dataset, a list of { Employer, State, NoticeDate, AffectedCount }, so EvaluateAsync makes no network call and never fetches per posting. Ship a small bundled seed (the same way seed-boards.json works), for example data/warn-notices.json. The working cache can live under %LOCALAPPDATA%\Envoy.
  2. In EvaluateAsync(JobPosting): normalize and fuzzy-match posting.CompanyName against the WARN employer names. Reuse DomScorer.NormalizedSimilarity (the pattern DuplicateJdSignal already uses). Require a strong match to avoid false positives on common names.
  3. If the matched employer filed a WARN notice within the last 90 days, and, when posting.Location is present, in the same state or region, return a SignalResult with a low-to-moderate score and moderate confidence. Return null otherwise (no match, no data, or missing company name).
  4. Refresh (optional, separate from this signal): you can add a small refresher that pulls the public state WARN files into the cache on some cadence (weekly, say) with an 8-second timeout and graceful failure. The signal itself only reads the cache. Keeping the fetch out of EvaluateAsync is what satisfies the per-posting 8-second rule (SIGNAL_AUTHORING rule 6). If you don't build the refresher, the bundled seed is enough for a first version.

Precision rules (bias for precision over recall)

  • Match only on a strong employer-name match (normalized, high similarity). A weak or partial name match must not fire.
  • WARN is employer-level, not role-level. Never claim the specific role was cut. Present it as correlation, for example: "{Employer} filed a WARN notice for {N} workers in {state} on {date}. A role posted now may be a backfill, a different team, or a listing left up."
  • Only recent notices fire (90 days or less). Older notices are dropped.
  • Larger or company-wide notices are stronger than small single-site ones. Scale AffectedCount into the confidence, but cap it at moderate.
  • Never condemn a company by name or return a binary "ghost" or "fake" verdict. This is one weak-to-moderate line of evidence, not a conviction.

Acceptance criteria

  • Implements IGhostSignal with Name = "Hiring Freeze" and Tier = Probabilistic
  • RequiresNetwork = false. EvaluateAsync reads only the local cached WARN dataset. No network call, no scraping, no auth
  • Returns null when there is no match, no data, or no company name
  • Never throws. Catch and return null
  • Human-readable evidence, moderate confidence. No verdict on a named company
  • Ships a small bundled WARN seed dataset with a documented format. Tests use a fixture dataset. No network calls in tests
  • dotnet test passes, including ContainerResolutionTests
  • PR targets main

Quick start

Copy the agent prompt from SIGNAL_AUTHORING.md, paste the Spec block above into it, and hand it to your coding agent.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions