This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Wednesday, 23 September 2026
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.
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:
It is a form filler, not a job application agent.
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:
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.
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.
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:
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:
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.
"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:
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:
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.
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.
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:
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.
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:
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.
Several common fields are intentionally awkward:
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 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.
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:
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.
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:
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.
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.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.