lucidRESUME: The Job Form Is Another Projection (English)

A job application form should not trigger another round of biographical invention. If I already have a reviewed career ledger, the form is simply one more place to project it.

This is the next experiment in my lucidRESUME series. The first article argues that a résumé should work rather like a scientific paper: readable prose for people, explicit structure for machines, and citations which connect claims to evidence. This one asks what happens when the next machine is a job application form.

NOTE: lucidRESUME is a research project. It changes quickly, is not an application automation product, and is not yet intended for ordinary end-user use.

lucidRESUME release


The Form Is Not a New Source of Truth

The usual form filler begins with the page. It reads a question, asks a model to answer it, and hopes the resulting prose happens to be true.

That is backwards for lucidRESUME.

The source of truth is the complete career record: human prose, accepted claims, concepts, dates, links and evidence. A particular résumé is a projection from that record. A Word document is another projection. The compact cJobML reference list is another. A form field belongs in the same family.

reviewed career ledger + visible form field -> proposed value or explicit gap

The arrow does not grant permission to invent a better candidate. It means "find something already supported which answers this question".

That gives the browser extension a deliberately unambitious job:

  1. Load a published full JobML document chosen by the user.
  2. Build a catalogue from current, accepted evidence.
  3. Read the visible empty fields on the active page.
  4. Propose exact ledger values for fields it can support.
  5. Say gap when it cannot.
  6. Fill only the values the user selected.
  7. Never submit the form.

It is a form filler, not a job application agent.

Why Put a Small Model in the Browser?

HTML gives us useful deterministic signals. autocomplete="given-name" is a fairly good indication that a field wants a first name. type="email" is not a particularly difficult semantic puzzle either.

Recruitment forms quickly become less civilised:

  • "Tell us about a time you led an engineering team through change."
  • "Which of these technologies have you used commercially?"
  • "Explain your experience of regulated delivery."
  • "What makes you suitable for this role?"

The page can express the same question in dozens of ways. This is a reasonable place to use a language model for matching. It is a terrible place to let one write an unchecked autobiography.

Chrome's Prompt API gives extensions access to the browser's local Gemini Nano model from Chrome 138. The model is downloaded separately, has hardware and storage requirements, and will not be available on every machine. Chrome states that subsequent local inference does not send the data to Google or another third party.

That last property matters. A complete career ledger contains personal data, employment history and contact details. Shipping every field and every claim to a remote model would be an unpleasant default for a convenience feature.

It also means the extension must remain useful without the model. Direct identity and contact mapping works deterministically. Everything else becomes a visible gap. "I cannot establish this" is a valid program result.

Prompt API Is a Browser Capability, Not a Bundled Model

There is a useful deployment difference between shipping a GGUF file with an application and calling Chrome's Prompt API. The extension does not package, download or host Gemini Nano. Chrome owns the model lifecycle and exposes a browser API over it.

The application has to ask what is possible on the current machine:

const modelOptions = {
    expectedInputs: [{ type: "text", languages: ["en"] }],
    expectedOutputs: [{ type: "text", languages: ["en"] }]
};

const availability = await LanguageModel.availability(modelOptions);

// unavailable | downloadable | downloading | available

The same options must be passed to availability() and create(). Chrome's Prompt API documentation is quite explicit about that because support can differ by modality and language. The current text API supports a limited set of declared languages, so the extension declares English rather than leaving the browser to guess. This also removed a warning found during the real-browser test.

If the model is downloadable, creating the session can trigger the initial download. The UI therefore has to expose that state and its progress. If it is unavailable, the feature must still have a coherent non-AI path. In this case that path is simple: deterministic contact fields still work and unresolved questions remain gaps.

The session is short-lived:

const session = await LanguageModel.create({
    ...modelOptions,
    monitor(monitor) {
        monitor.addEventListener("downloadprogress", event =>
            showProgress(event.loaded));
    }
});

try {
    const response = await session.prompt(prompt, {
        responseConstraint: schema
    });
    return JSON.parse(response);
} finally {
    session.destroy();
}

Destroying it matters. The model is shared browser capability, but each session holds context and consumes resources. Chrome's session guidance recommends destroying sessions which are no longer needed and keeping unrelated tasks out of the same conversational history. A form batch should not inherit the semantic debris of the previous employer's form.

The API is also unavailable in Web Workers at present. That is one reason the model call lives in the side-panel document rather than the extension service worker. This is a Chrome feature with explicit version, language and hardware constraints, not yet a portable browser baseline. The architecture cannot make its truth guarantees depend on it being present.

Chrome also owns the exact model version. Two eligible machines can receive a different browser or model update and make different semantic choices. That is fine for proposals which pass through deterministic validation. It would be a poor foundation for a decision which had to reproduce bit-for-bit across an organisation.

There are now three quite different meanings of "local enough" in application architecture:

Approach Model lifecycle Data path during inference Main trade-off
Browser built-in model Browser installs and updates it Prompt remains on the device Little deployment work, but limited hardware coverage and little control over the model version
Packaged GGUF / ONNX model The application chooses and ships or downloads it Prompt remains on the device More control and wider application design, but the product owns model size, acceleration and updates
Remote model API Provider operates it Prompt leaves the device Broad capability and consistent deployment, but network, cost and data-governance concerns

I use the second approach in desktop experiments such as Avalonia with LLamaSharp. The browser approach is more attractive for this extension because Chrome already sits beside the form and can supply a small semantic mapper. Neither choice changes the rule that durable facts live outside the model.

What In-Browser Models Are Actually Good For

An in-browser model has access to something a remote API usually does not: the small, immediate context already in front of the user. That produces a useful class of tasks:

  • classify or route a short piece of page text;
  • map an unfamiliar label onto a known application field;
  • extract a few typed values from text the user has selected;
  • summarise the current article, message or document;
  • translate or proofread a draft without uploading it;
  • create alt text for an image already open in the page;
  • perform local semantic search over a small, prepared collection;
  • and suggest an edit which remains visibly reversible.

The common property is not "AI in the browser". It is a bounded input, a narrow task and a result which can be checked, ignored or undone.

That is the same shape as my deterministic voice-form experiment: the model translates ambiguous human input into a candidate structured action, while normal code owns the form state. It is also an instance of Constrained Fuzziness, where a probabilistic component proposes and an explicit boundary decides what survives.

Small local models are particularly well suited to these jobs because breadth is less important than a cheap, detectable failure. I covered that distinction in No, Small Models Are Not the "Budget Option". The browser model does not need to know my career. It needs to recognise that "professional profile URL" probably refers to one of a small set of known links.

There are equally clear poor uses:

  • inventing facts which are not present in the input;
  • making legal, medical, employment or financial decisions;
  • answering questions which require current external knowledge;
  • silently changing durable application state;
  • processing a huge raw corpus when retrieval could first reduce it;
  • and providing identical results across every user and device.

I made the data-reduction argument in Reduced RAG: retrieve a small amount of relevant material before asking a model to judge it. The extension follows that advice on a tiny scale. It ranks the fact catalogue for the current batch and sends at most forty facts, rather than dropping a ten-page résumé and its entire evidence graph into every prompt.

Likewise, Why LLMs Fail as Sensors argues that a model should not be the first component asked to rediscover structure we can extract directly. HTML input types, autocomplete tokens, existing values and JobML fingerprints are ordinary signals. Use them first. The model handles the remaining semantic ambiguity.

Local Inference Changes Privacy; It Does Not Solve It

"Runs locally" is valuable, but it is not a complete privacy policy.

Local inference removes one important data transfer. The prompt and result do not need to travel to a model provider for each request. It can also continue after the model has been downloaded when the network is unavailable. That is a substantial improvement for a task involving employment history, contact data and answers on a third-party recruitment page.

Several other data paths still exist:

  • the extension fetches the ledger from the endpoint the user supplied;
  • the application page already belongs to a third party;
  • browser extensions run with permissions which must be kept narrow;
  • local storage, logs or analytics can preserve data long after inference;
  • another extension or compromised device can still expose local information;
  • and model output can contain material copied from malicious page text.

On-device does not mean the model is entitled to every local document. It means the application can choose a smaller disclosure boundary.

For this extension that boundary is visible in the design:

  • endpoint access is requested for one host at runtime;
  • form access comes from the active tab after a user gesture;
  • the ledger body remains in side-panel memory;
  • only relevant fact snippets are sent to the local model;
  • no form value is sent to lucidRESUME or a cloud model;
  • model output is treated as untrusted;
  • and filling remains a separate, reversible user action.

Chrome's own built-in AI guidance recommends minimising model input, using structured output, treating generated content as untrusted, preserving user control and allowing edits to be undone. Those are good rules whether inference happens in a browser, an Avalonia app via LLamaSharp, or a remote service.

The Chrome Web Store also treats locally processed personal data as user data. Its user-data guidance still applies even when nothing is uploaded to a model vendor. Before store publication the extension needs a public privacy policy which describes what is read, stored and filled. "Local" is an implementation fact, not a waiver from explaining the feature to the person using it.

This is another reason I prefer evidence selection over open generation here. DoomSummarizer and lucidRAG use citations so a reader can move from a synthesis back to its sources. The form filler uses the same idea at much smaller scale: every proposed answer carries the ledger facts which caused it to appear.

The Architecture

flowchart LR
    J[Published full JobML] --> V[Parse and verify evidence]
    V --> F[Closed fact catalogue]
    P[Visible empty page fields] --> D[Deterministic field matching]
    F --> D
    D -->|direct match| R[Reviewable proposals]
    D -->|unresolved| M[Chrome Prompt API]
    F --> M
    M --> B[Deterministic response boundary]
    B -->|supported| R
    B -->|unsupported| G[Explicit gaps]
    R --> U[User selects values]
    U --> X[Fill fields, never submit]

    classDef source fill:none,stroke:#176b61,stroke-width:2px
    classDef process fill:none,stroke:#496966,stroke-width:1.5px
    classDef boundary fill:none,stroke:#956900,stroke-width:2px
    classDef gap fill:none,stroke:#a63446,stroke-width:2px
    class J,P,F source
    class V,D,M,R,U,X process
    class B boundary
    class G gap

The important box is not the model. It is the deterministic boundary after the model.

Page labels, option text and even ledger prose are untrusted input. A prompt can tell the model not to follow instructions embedded in those values, but a prompt is not a security boundary. The response still has to survive ordinary code.

First Build a Fact Catalogue

The extension does not hand the complete YAML document to the model and ask it to make sense of everything. It first produces a bounded catalogue containing:

  • the résumé owner's name and contact details from the human header;
  • named entities such as roles and projects;
  • accepted claims which still have current evidence;
  • the concepts supported by those claims;
  • and the original human prose behind valid evidence references.

This distinction fixed a real defect during review. My first implementation rejected a stale prose passage but still admitted its parent claim and concepts to the catalogue. That meant the long prose answer was unavailable, yet the model could still use the stale claim for a short answer.

The corrected rule is:

const validProse = evidence.flatMap((item, index) => {
    if (isInvalidEvidenceState(item.state)) return [];

    const exact = resolveCurrentProse(
        item.ref,
        item.selector?.exact,
        passages);

    if (!exact) return [];

    const expected = item.fingerprint?.text;
    if (!expected && !item.selector?.exact) return [];
    if (expected && expected.toLowerCase() !== fnv1a64(exact).toLowerCase())
        return [];

    return [{ item, index, exact }];
});

const hasCurrentExternalEvidence = evidence.some(item =>
    !!item.uri && !isInvalidEvidenceState(item.state));

if (validProse.length === 0 && !hasCurrentExternalEvidence)
    return;

A reference such as #example-role:p2 locates today's paragraph. It does not by itself establish that today's paragraph is the one a person reviewed last week. The extension therefore also requires either the stored FNV-1a fingerprint or an exact text selector before it promotes prose into the verified catalogue.

If that evidence drifts, the prose, claim and its concepts all disappear from the fillable fact set. The editor can reconcile the change. The browser extension does not quietly reinterpret it.

The Model Selects; Code Projects

For unresolved fields, the model receives a list of field descriptions and a small set of relevant fact IDs. It is asked to return mappings, not answers.

The Prompt API supports structured output through a JSON Schema. The schema restricts the response to known field IDs, known fact IDs and two states: proposal or gap.

const schema = {
    type: "object",
    properties: {
        mappings: {
            type: "array",
            items: {
                type: "object",
                properties: {
                    field_id: { type: "string", enum: fieldIds },
                    status: { type: "string", enum: ["proposal", "gap"] },
                    fact_ids: {
                        type: "array",
                        uniqueItems: true,
                        items: { type: "string", enum: factIds }
                    },
                    extract: { type: "string" },
                    option_value: { type: "string" },
                    reason: { type: "string" }
                },
                required: ["field_id", "status", "fact_ids", "reason"],
                additionalProperties: false
            }
        }
    },
    required: ["mappings"],
    additionalProperties: false
};

That constrains the shape, but shape is not truth. The application applies a second set of rules after parsing the result:

  • a short text value must be an exact contiguous substring of a cited fact;
  • a long answer can contain only complete, verified human prose passages;
  • a select or radio answer must be one of the page's current options;
  • model-derived proposals always begin unchecked;
  • and unsupported answers become gaps.

The model might decide that an evidence passage is relevant to a leadership question. It cannot paraphrase the passage into something more impressive. The value comes from the ledger.

This is the same separation used by the résumé compiler:

semantic selection != textual authority

Human prose stays human because the machine chooses existing prose rather than regenerating it at the last possible moment.

Some Questions Should Remain Blank

Several common fields are intentionally awkward:

  • salary expectations;
  • current work authorisation;
  • future sponsorship requirements;
  • availability and notice period;
  • demographic declarations;
  • consent;
  • and motivation for this particular employer.

A career ledger may contain some of those answers, but it often should not. Employment history does not establish a desired salary. Using AWS does not establish permission to work in the United Kingdom. A résumé certainly does not establish consent.

The extension prompt calls these cases out, but deterministic projection remains the final guard. If no selected fact supports an answer, the interface shows a red gap and leaves the field alone.

The lucidRESUME evidence filler showing exact identity proposals and explicit gaps in a narrow Chrome side panel

The screenshot is from the browser test fixture. The on-device model was not available in that Chromium build, so it shows the deterministic fallback. Five identity and contact values are proposed. Employer, leadership, sponsorship and salary remain gaps. That is less flashy than generated prose, but considerably more useful than a confident lie.

Permissions Have to Match the Claim

The extension uses Chrome's activeTab permission to work with the current form only after the user invokes it. The full JobML URL is entered by the user, and the extension requests optional host access at runtime for that host. Installing it does not grant permanent access to every website.

The current privacy boundary is deliberately narrow:

  • the endpoint must use HTTPS, except for local development;
  • credentials cannot be embedded in the endpoint URL;
  • the response stream and parser both enforce a 4 MB limit;
  • the fetched ledger stays in side-panel memory;
  • only the endpoint URL is stored locally;
  • visible empty fields in the main page are examined;
  • existing values are checked again immediately before filling;
  • and there is no submit operation.

Cross-origin embedded forms are not handled in this first version. Nor are file uploads. Both can wait until the smaller trust model is understood.

Testing the Refusal Path

Most form-filler demos test whether fields contain text. For this experiment the more important assertions concern what was not filled.

The TypeScript suite currently covers:

  • full JobML parsing;
  • endpoint restrictions;
  • FNV-1a parity with JobML;
  • stale fingerprint and selector rejection;
  • removal of claims whose evidence has drifted;
  • refusal to promote prose with only a bare reference;
  • deterministic identity and contact matching;
  • protection against filling a referee or manager with the candidate's details;
  • structured Prompt API invocation;
  • exact-substring enforcement;
  • human-prose-only long answers;
  • and unchecked model proposals.

The real browser test loads a local full JobML endpoint and a nine-field application form. It then checks:

JobML facts loaded       10
Empty fields found        9
Supported proposals       5
Explicit gaps             4
Fields filled             5
Existing values changed   0
Forms submitted           0

It also gives the first field an existing inline outline before analysis. The extension temporarily replaces that outline to show evidence status, then the test rescans the page and confirms the original style returns. That sounds like a fussy detail until a helpful extension quietly deletes a site's own focus or accessibility styling.

The Prompt API contract is covered with a controlled browser API stub because the test Chromium does not ship the local model. That verifies the structured request, language declaration, response constraint and session cleanup. A real model quality benchmark still needs machines which meet Chrome's current model requirements. The deterministic fallback has no such dependency.

The full .NET solution also remains green at 388 tests. The extension is small, but it sits beside the compiler and evidence formats rather than becoming a separate truth system.

What This Experiment Actually Proves

It does not prove that every recruitment form can be filled. They cannot. Some questions need a fresh human decision, some pages use inaccessible embedded controls, and some employers ask for information which has no sensible place in a professional evidence ledger.

It does establish a more useful boundary for language models in this workflow. A small model can resolve semantic variation without owning the facts or the words. It can point from a strange form question to likely evidence, while code checks whether the proposed value is one the ledger can actually emit.

That returns to the original scientific-paper idea. A paper-reading system can help locate the relevant citation. It should not alter the cited experiment to make the current question easier to answer. In the same way, the form filler can locate a professional claim and its supporting prose. It does not get to revise the candidate.

career ledger -> résumé -> JobML -> application form

These are different resolutions and different surfaces over one reviewed body of evidence. The form is merely the newest projection.

The extension source and implementation notes are in the lucidRESUME repository, with the fuller design and threat model alongside the JobML specifications.

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.