Adapter Contract

An Adapter is a language-neutral dock contract. It names how JobCache hands bounded work to source-specific code and how an Observation comes back.

The implementation behind the dock is a blackbox. It may be TypeScript, Rust, WASM, a Tauri native helper, browser automation, Jina, a feed reader, or a later adapter kit. The dock does not care. The only output across the dock is Observation.

Core shared contracts live in @cv/jobcache: Observation, field/cell vocabulary, evidence refs, object refs, chunk refs, annotations, embeddings, and source-neutral validation. Implementation helpers may live in jobcache/ingest or in a later adapter kit. They must not become a second submitted-output model.

Claude-specific execution guidance lives in jobcache/ingest/CLAUDE_ADAPTER_EXECUTION_CONTRACT.md. The canonical dock type is @cv/jobcache/adapter.

Convergence rule: JobCache top-down tasks and CareerVector bottom-up URL imports both emit and consume the same @cv/jobcache Observation shape. The shared ad data is ad_id-centered; workspace jobs stay private and are never Adapter output. RADAR is the CareerVector workspace entry surface around DB search, manual URL add, and candidate import; it is not itself an Adapter.

Adapter dock -> blackbox implementation -> Observation

Vocabulary

Use the words from glossary.md:

Term Exact meaning
family Reusable job-site type, such as Workday, Greenhouse, jobs.ch, RSS, or custom HTML.
source Concrete job site, board, company career page, or tenant.
Adapter Code for a platform/source, exposed through a language-neutral dock contract.
implementation Private blackbox behind the Adapter dock. It may use any suitable runtime or helper.
Method Small source-method label for dashboards and routing, such as sitemap-jsonld, html-bespoke, headless-proxy, or feed-api.
Env Runtime capability bundle passed through the dock: userAgent, optional rateLimit, fetch, and optional signal.
ObservationOptions Runtime envelope hints used to complete an Observation.
ParsedAd Internal normalized parse helper, not submitted output. It exists only before Observation.
binding Configuration that connects one source to one Adapter, including seeds, limits, and source hints.
RADAR CareerVector workspace entry surface for finding or importing jobs from shared DB search, manual URL add, or machine-made candidates. Not an Adapter, Method, source, or data shape.
task One bounded unit of work. Current task kinds are discover-urls, fetch-page, parse-page, chunk-text, enrich-observation, embed-text, or verify-observation.
task class Resource/policy class for work. Current classes are scrape, chunk, enrich, embed, and verify.
lease Temporary assignment of one task to one device/session.
Device Runtime/place where code runs and leases are executed.
session One running app instance/process on a device.
Scraper Adapter plus Device while executing a scrape task.
Observation Canonical task output submitted through submitObservation.
submitObservation Server path that validates schema, lease, and policy, writes current ad payload cells, updates global device trust, and queues verify-observation tasks on fresh conflict.
role URL-family identity above one or more language-specific ads; semantic cross-URL reduction is async work outside the hot path.
ad Shared URL x language identity for one public listing or source presence under a role.
workspace job CareerVector workspace-private job record. It is never Adapter output.
field Named public ad attribute with type, visibility, and reducer rules.
cell Sparse payload for one field on one ad.
evidence Source material or reference supporting observations and cells.
chunk Stable text segment derived from evidence or resolved text.
embedding Vector record for a stable input key. It rides inside an Observation when produced by a device task.

Avoid provider, worker, runner, and product-model use of posting. Use Scraper only for the runtime event of Adapter plus Device execution, never for the shared data model.

Current Dock

The live TypeScript dock is intentionally small:

export interface Env {
  userAgent: string;
  rateLimit?: { perMinute: number };
  fetch: typeof globalThis.fetch;
  signal?: AbortSignal;
}

export interface Adapter {
  readonly id: string;
  readonly version: string;
  readonly label: string;
  readonly homepage: string;
  readonly method: Method;
  discover(env: Env): Promise<string[]>;
  observe(
    url: string,
    env: Env,
    opts?: ObservationOptions,
  ): Promise<Observation | null>;
}

This shape is a dock, not a claim that every implementation must be TypeScript. Env carries runtime capabilities only. ObservationOptions carries envelope hints: taskId, leaseId, device/session/app versions, adapter identity, timestamps, timing, bytes, resource policy, content hash, optional adId, and structured task errors. ParsedAd may be used inside current helpers to normalize a source parse before building the Observation, but it must not cross the submitted-output boundary. CareerVector bottom-up single-URL import calls scrapeUrl(), which runs the ordered browser-compatible adapter/provider chain and returns scrape telemetry plus the same Observation payload when one can be produced. Raw source/API/provider output stays inside adapter internals. Observation is the canonical submitted task output. Adapters must not put DB writes, trust policy, queue priority, or workspace data behind this path.

The current ingest tree speaks Observation directly. New compatibility layers need explicit justification and should be deleted in the same slice that removes their last caller.

Observation construction sequence:

  1. Normalize url and resolved_url; derive url_hash.
  2. Provide URL and language evidence. The server-side writer derives canonical ad_id with adIdFromUrlLanguage(url, language) and canonical role_id with roleIdFromUrl(url); any supplied ad id is treated as a hint to verify, not as authority.
  3. Hash retained source material into content_hash.
  4. Map source values only into FIELD_VOCABULARY and place every value under fields[field].cell.
  5. Create stable local evidence_refs; every referenced evidence ID must exist inside the same observation.
  6. Add optional object, chunk, annotation, search chunk, and embedding records only when produced by the task.
  7. Compute observation_hash and validate through ObservationSchema.

Observation Shape

Adapter work ultimately contributes one Observation. The canonical schema lives in @cv/jobcache/contract.

interface Observation {
  schema_id: "cv.jobcache";
  schema_version: "v0";
  task_id: string;
  lease_id?: string;
  ad_id: string;
  device_id: string;
  session_id: string;
  app_version: string;
  device_runtime_version: string;
  adapter_id: string;
  adapter_version: string;
  status: "success" | "partial" | "failed";
  fetched_at: string;
  observed_at?: string;
  url: string;
  resolved_url: string;
  url_hash: string;
  content_hash: string;
  observation_hash: string;
  timing_ms: number;
  bytes_read: number;
  resource_policy: ObservationResourcePolicySnapshot;
  fields: Record<FieldKey, ObservedField>;
  evidence_refs: EvidenceRef[];
  object_refs?: ObjectRef[];
  chunk_refs?: ChunkRef[];
  annotations?: Annotation[];
  search_chunks?: ChunkRef[];
  embeddings?: Embedding[];
  errors: TaskError[];
}

interface ObservedField {
  cell: CellPayload;
  evidence_ref_ids?: string[];
  observed_at?: string;
}

Successful observations must include at least one field, chunk, annotation, search chunk, or embedding. Every evidence_ref_ids[] entry must point at an evidence_refs[] entry in the same observation. The envelope anchors to one ad_id; do not add role or workspace-job addresses.

Fields use the shared field vocabulary:

title, organization, organization_url, description, description_html, location,
locality, region, country_code, remote, employment_type, language, industry,
salary, salary_min, salary_max, salary_currency, salary_period, workload,
source, source_id, source_ref, url, resolved_url, canonical_url, posted_at,
valid_through, raw

Cells are one of:

type CellPayload =
  | { kind: "value"; value: JsonValue; value_hash?: string }
  | { kind: "unknown"; reason?: string }
  | { kind: "empty"; reason?: string }
  | { kind: "redacted"; reason?: string; policy_id?: string };

Evidence refs use these exact kinds:

source-url, content-hash, observation-hash, text-range, html, json, screenshot,
object, chunk

Object refs use online S3-compatible storage only:

store: b2 | r2 | s3 | minio | garage | other-s3
kind: html | json | screenshot | text | observation | embedding-vector | blob

Chunk refs describe stable text segments from source evidence or resolved text. Annotations describe enrichment output such as summaries, skills, seniority, role-family, facets, search text, language, or other metadata. Search chunks are retrieval-oriented chunk refs. Embeddings are vector records for stable text or chunk keys; they remain inside Observation and may point at an embedding-vector object ref.

Do not document or design local filesystem paths as durable cold evidence. Object storage is replaceable evidence. Cockroach tree identities, current cells, provenance, and current cell history are the business record.

Boundaries

Adapters may:

  • discover candidate public URLs or source IDs for one source or family
  • fetch bounded public source material for one task
  • parse source material into shared field map entries with cells
  • produce evidence refs, object refs, chunk refs, annotations, and embeddings
  • classify source-specific stale or closed markers
  • emit structured errors with retryability
  • include source hints that contain selectors, URL patterns, or public metadata

Adapters must:

  • stay behind the Adapter dock and Observation schema
  • preserve source provenance, hashes, timing, and byte counts
  • anchor submitted observations to one ad_id
  • attach observations, fields, evidence, and chunks to the shared ad boundary
  • keep retained large raw material in online S3-compatible cold evidence
  • use stable input keys so unchanged content does not trigger repeated work
  • let submitObservation validate schema, lease, and policy
  • let server-side DB code decide current cells, conflict handling, global trust updates, and verification scheduling

Adapters must not:

  • receive Cockroach, Postgres, or DATABASE_URL credentials
  • receive permanent object-store credentials
  • write directly to Cockroach
  • define a new vacancy, listing, or business data model
  • write workspace-private CVL, notes, files, conversations, evaluations, or UI state
  • read workspace-private data
  • decide role clustering, global freshness, trust, deduplication, or current cell writes
  • implement Elo, Bayesian reputation, or any other trust algorithm
  • schedule verification or queue priority from source-specific code
  • store raw HTML/API payloads as hot SQL blobs
  • use browser profiles, cookies, credentials, or session reuse
  • introduce source-specific global orchestration hacks

File Ownership For Adapter Work

Adapter implementers may own adapter-scoped files only after the user asks for implementation:

  • jobcache/ingest/src/adapters/<family-or-source>.ts
  • jobcache/ingest/src/adapters/registry.ts for narrow registration changes
  • jobcache/ingest/test/adapters/<family-or-source>.test.ts
  • jobcache/ingest/test/fixtures/<source-or-family>-*.{html,json,xml,txt}
  • jobcache/wiki/content/architecture/adapter-contract.md for contract notes
  • jobcache/ingest/CLAUDE_ADAPTER_HANDOFF.md for handoff updates

Architecture owner territory unless explicitly reassigned:

  • lib/jobcache/src/contract.ts
  • jobcache/shared/src/device-contract.ts
  • jobcache/migrations/
  • jobcache/infra/
  • jobcache/interface/
  • jobcache/ops/, jobcache/qa/, and jobcache/status/
  • CareerVector workspace APIs, Yjs documents, and UI code

If an adapter needs a contract change, stop and write the requested schema change as a question. Do not silently expand the model.

STOP Conditions

Adapter implementers must stop and ask for direction before:

  • adding fields outside FIELD_VOCABULARY
  • adding object, evidence, chunk, annotation, embedding, task, task-class, or method kinds
  • adding role, workspace job, user, CVL, note, file, evaluation, or UI-state addressing to adapter output
  • touching Cockroach/Postgres schema, migrations, submitObservation, direct DB writes, or DATABASE_URL
  • implementing trust, deduplication, current-cell, queue-priority, or verification-scheduling policy
  • using permanent object-store credentials, browser profiles, cookies, credentials, stealth, bypass, or session reuse
  • running live scraping, paid services, production DB/object-store access, or deploys without explicit user approval

Validation Commands

For adapter docs only:

bun --filter @cv/jobcache-wiki check

For adapter implementation:

bun --filter @cv/jobcache-ingest test
bun --filter @cv/jobcache-ingest check

Do not run live scrape endpoints, Browserbase, Firecrawl, production DB, object-store, or deploy commands unless the user explicitly asks.

Acceptance Criteria

Adapter work is acceptable when:

  • vocabulary uses Adapter, Method, Env, ObservationOptions, ParsedAd, Observation, field, cell, and ad correctly
  • successful observations validate against the shared Observation schema
  • field names are from FIELD_VOCABULARY
  • source-backed fields point to valid evidence refs
  • Adapter implementations stay behind the dock
  • trust, scheduling, DB writes, and workspace-private data stay outside Adapter code
  • object refs use an online S3-compatible store kind
  • chunks have stable hashes and ranges when ranges are known
  • no adapter/device path gets DB credentials or permanent object-store credentials
  • no second submitted-output model is introduced
  • tests cover friendly fixtures and at least one stale/irrelevant failure path
  • source-specific code stays in adapter-owned files
Source: jobcache/wiki/content/architecture/adapter-contract.md