Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions tests/browser/document-grounding.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,197 @@ test("rejects unsupported, spoofed, empty, oversized, and invalid UTF-8 files",
}
});

test("single-character skills like C and R survive extraction", async () => {
// A one-letter name like C or R has a letter, so it is kept. A length floor
// meant to drop stray punctuation used to drop these too.
const resume = await parseGroundingFile(txt("Skills: C, Go, Python, R, Rust"), "resume");
assert.deepEqual(resume.skills, ["C", "Go", "Python", "R", "Rust"]);
});

test("digit-led skills are not mistaken for a numbered-list marker", async () => {
// clean()'s leading-marker strip is meant for real list prefixes like "1. "
// or "2) ", not for a bare digit run: without the "then punctuation" check,
// "5G" loses its "5" and survives as the fabricated skill "G".
const resume = await parseGroundingFile(txt("Skills: C, 5G, 3D, 4K"), "resume");
assert.deepEqual(resume.skills, ["C", "5G", "3D", "4K"]);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
});

test("a numbered-list marker is still stripped from a requirement line", async () => {
const jd = await parseGroundingFile(txt("1. Must know Rust\n2) Should know Go"), "jd");
assert.deepEqual(jd.requirements, ["Must know Rust", "Should know Go"]);
});

Comment thread
Disesfgewu marked this conversation as resolved.
test("a numbered marker before two letters needs no space, a dash run does", async () => {
// "1.Must" has no space after the marker but still loses it. A dash run
// keeps its whitespace requirement, so glued "--Must" is left whole.
const jd = await parseGroundingFile(txt("1.Must know Rust\n1.) Should know Go\n-- Must know Python"), "jd");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dashed fixture carries a space, so the "dashed marker with no space" half of the name is never exercised: --Must know Python is left intact, because the dash branch still requires \s+. Use that input and make the branch handle it, or narrow the name to the numbered case this actually covers.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would narrow the name to the numbered case this actually covers.

assert.deepEqual(jd.requirements, ["Must know Rust", "Should know Go", "Must know Python"]);
const glued = await parseGroundingFile(txt("--Must know Python"), "jd");
assert.deepEqual(glued.requirements, ["--Must know Python"]);
});

test("a run of asterisks is stripped like a run of dashes", async () => {
// Pasted markdown leaves "** " and "*** " in front of a line. Like dashes,
// a run of asterisks is a marker when whitespace follows; glued "**Must"
// is left whole.
const jd = await parseGroundingFile(txt("** Must know Rust\n* Should know Go"), "jd");
assert.deepEqual(jd.requirements, ["Must know Rust", "Should know Go"]);
const resume = await parseGroundingFile(txt("*** Led migration"), "resume");
assert.deepEqual(resume.anchors, ["Led migration"]);
const glued = await parseGroundingFile(txt("**Must know Rust"), "jd");
assert.deepEqual(glued.requirements, ["**Must know Rust"]);
});

test("a version wildcard is not read as a numbered marker", async () => {
// "." follows a digit inside real tokens, so digits plus "." is not enough.
// One letter after it ("9.x") leaves the token whole; two ("1.Go") read as
// a marker.
const jd = await parseGroundingFile(txt("9.x Java experience required\n18.x Node experience"), "jd");
assert.deepEqual(jd.requirements, ["9.x Java experience required", "18.x Node experience"]);
const resume = await parseGroundingFile(txt("Skills: 1.Go, 2.Rust"), "resume");
assert.deepEqual(resume.skills, ["Go", "Rust"]);
});

test("a full-width numbered marker is stripped from a requirement line", async () => {
// U+FF11 and U+FF12 are Nd but not \d, so a \d-based strip would leave
// these markers in place. The escapes keep this file ASCII.
const jd = await parseGroundingFile(
txt("\uff11. Must know Rust\n\uff12) Should know Go\n\uff11.Must know C"),
"jd",
);
assert.deepEqual(jd.requirements, ["Must know Rust", "Should know Go", "Must know C"]);
});

test("a full-width numbered marker is stripped from a resume, and a digit-led skill survives", async () => {
const resume = await parseGroundingFile(
txt("Skills: \uff15G, C\n\uff11. Led project Alpha\n\uff12) Built project Beta"),
"resume",
);
assert.deepEqual(resume.skills, ["\uff15G", "C"]);
assert.deepEqual(resume.anchors, ["Led project Alpha", "Built project Beta"]);
});

test("a split fragment that is pure punctuation is dropped, not kept as a skill", async () => {
// A stray delimiter that lands as its own fragment has no letter, so it is
// dropped.
const resume = await parseGroundingFile(txt("Skills: C, /, Go, #, &, Java"), "resume");
assert.deepEqual(resume.skills, ["C", "Go", "Java"]);
});

test("lone numeric fragments are not kept as skills", async () => {
const resume = await parseGroundingFile(txt("Skills: Python, 1, 1., Rust"), "resume");
assert.deepEqual(resume.skills, ["Python", "Rust"]);
});

test("a single isolated skill with no delimiter still survives extraction", async () => {
const resume = await parseGroundingFile(txt("Skills: Python"), "resume");
assert.deepEqual(resume.skills, ["Python"]);
});

test("a skills header missing its colon is not treated as a skills line", async () => {
// parseResume only recognizes "skill(s)/technologies/stack" followed by ":",
// so a header that drops the colon must yield no skills at all rather than
// matching loosely on the leading word.
const resume = await parseGroundingFile(txt("Skills Python, Go"), "resume");
assert.deepEqual(resume.skills, []);
});

test("header casing, synonyms, and stray whitespace around the colon are tolerated", async () => {
const resume = await parseGroundingFile(txt("TECHNOLOGIES : Python, Go"), "resume");
assert.deepEqual(resume.skills, ["Python", "Go"]);
});

test("empty segments from doubled-up delimiters are dropped, not kept as blank skills", async () => {
const resume = await parseGroundingFile(txt("Skills: Python,, Go;;Rust||C++"), "resume");
assert.deepEqual(resume.skills, ["Python", "Go", "Rust", "C++"]);
});

test("a slash or percent does not keep a letterless token", async () => {
// "24/7" and "100%" are the same digit-plus-symbol shape as "-50" or
// "1-2" -- there is no principled reason to exempt these two symbols and
// not others, so only a letter keeps a token.
const resume = await parseGroundingFile(txt("Skills: C++11, 24/7, 100%, v2, 5, -5"), "resume");
assert.deepEqual(resume.skills, ["C++11", "v2"]);
});

test("a bare digit.digit shape is dropped as an orphaned version or GPA fragment", async () => {
// "3.14", "5.2", and "802.11" can't be told apart from a GPA or a version
// number split off its software name -- there is no letter left to say
// which one it is, so the whole shape is dropped, standard or not.
const resume = await parseGroundingFile(txt("Skills: Python, 3.14, 5.2, 802.11, Go"), "resume");
assert.deepEqual(resume.skills, ["Python", "Go"]);
});

test("a bare multi-digit integer gets no special treatment either", async () => {
// With no dot, "27001" and "2015" cannot be read as a version or a GPA
// either. Whether split off a prefix by parseResume's "," / ";" / "|"
// split or typed alone, they have no letter, so they are dropped like
// "3.14" is and only "ISO 9001" is kept.
const resume = await parseGroundingFile(txt("Skills: ISO 9001, 27001, 2015"), "resume");
assert.deepEqual(resume.skills, ["ISO 9001"]);
});

test("a bare part number is an accepted loss, a named one is kept", async () => {
// A systems resume lists these, but "8051" and "2025" are the same shape
// and the filter cannot tell a part from a year, so it drops both bare
// forms. Naming the part is what keeps it, which is the trade the letter
// rule makes on purpose.
const resume = await parseGroundingFile(
txt("Skills: 6502, 8051, 68000, 2025, MOS 6502, Intel 8051, Z80"),
"resume",
);
assert.deepEqual(resume.skills, ["MOS 6502", "Intel 8051", "Z80"]);
});

test("a generation suffix or org prefix carries a standard's number through", async () => {
// A letter suffix ("ac", "ax") or a name prefix ("IEEE", "Wi-Fi") gives the
// number a letter, so the token is kept.
const resume = await parseGroundingFile(txt("Skills: 802.11ac, 802.11ax, IEEE 802.11, Wi-Fi 802.11"), "resume");
assert.deepEqual(resume.skills, ["802.11ac", "802.11ax", "IEEE 802.11", "Wi-Fi 802.11"]);
});

test("a standard survives named but not split off as a bare number", async () => {
// "IEEE 754" and "ISO 27001" keep their org name, so the letter carries
// them through same as any other skill. Once "754" is split off from
// "IEEE" it is just a bare digit run with no letter left to scope it, and
// is dropped the same way "802.3" is -- both are real standards, but
// neither token carries anything to say so on its own.
const resume = await parseGroundingFile(txt("Skills: IEEE 754, ISO 27001, IEEE, 754, 802.3"), "resume");
assert.deepEqual(resume.skills, ["IEEE 754", "ISO 27001", "IEEE"]);
});

test("a non-ASCII decimal digit dotted fragment is dropped like its ASCII equivalent", async () => {
// A full-width or Arabic-Indic digit is not a letter, so a GPA or version
// fragment spelled with them is dropped like "3.14" is.
const resume = await parseGroundingFile(txt("Skills: Python, 3.14, ٣.١٤, Go"), "resume");
assert.deepEqual(resume.skills, ["Python", "Go"]);
});

test("digits and symbols elsewhere in a token do not keep it", async () => {
// None of these carry a letter, so none of them get to survive as a
// negative number, a parenthesized GPA, a digit range, or a year range --
// the same rule that drops a bare "27001" drops these too.
const resume = await parseGroundingFile(txt("Skills: Python, -50, (3.14), 1-2, 2020-2024, Rust"), "resume");
assert.deepEqual(resume.skills, ["Python", "Rust"]);
});

test("a shared prefix does not carry over to the values after it", async () => {
// ISO does not scope "124141" or "2015" just because it appeared earlier
// on the line -- parseResume splits Skills: ISO 27001, 124141, ISO 8981,
// 2015 into four independent candidates, and each one is judged only on
// what it itself carries. "ISO 27001" and "ISO 8981" keep their own "ISO",
// but "124141" and "2015" reached this filter with no letter of their own
// and are dropped, even though a human reader might guess they belong to
// the same certification family.
const resume = await parseGroundingFile(txt("Skills: ISO 27001, 124141, ISO 8981, 2015"), "resume");
assert.deepEqual(resume.skills, ["ISO 27001", "ISO 8981"]);
});

test("stacked list markers on one line are stripped in full, not just the first", async () => {
const jd = await parseGroundingFile(txt("1. - Must know Rust\n* 2) Should know Go"), "jd");
assert.deepEqual(jd.requirements, ["Must know Rust", "Should know Go"]);
});

test("selection requires consent and storage is one-time", () => {
const extracted = { requirements: ["Must know Rust"], skills: ["Rust"], anchors: ["Built a parser"] };
const selected = { requirements: [0], skills: [], anchors: [0] };
Expand Down
43 changes: 41 additions & 2 deletions web/document-grounding.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,51 @@ function normalizeLines(text) {
.split(/\r?\n/).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
}

// A bullet glyph is stripped on its own, because it is a list glyph and not
// part of any name, so it needs no space after it to say so. A run of dashes
// or asterisks stripped that freely would eat into real content, so those
// still only count as a marker once whitespace after them confirms it. A
// run, not one character: "-- " and "** " are what pasted markdown leaves.
//
// A numbered marker is stripped without a following space, because "1.Must"
// and "1.) Must" are common paste shapes and the "1." should not reach the
// interviewer. Its digits must be followed by "." or ")", which keeps "5G"
// and "3D" whole, and then by whitespace or two letters. A "." alone is not
// enough, since it follows a digit inside real tokens: "9.x Java" has one
// letter after it and stays whole, while "1.Must" and "1.Go" lose their
// marker. That is a heuristic, not a rule: "1.C experience" keeps its
// marker and "3.js experience" loses its "3.".
// \p{Nd} rather than \d, so a full-width digit is a marker digit like an
// ASCII one.
function clean(line) {
return line.replace(/^[-*•\d.)\s]+/, "").slice(0, textLimit).trim();
return line.trimStart()
.replace(/^(?:(?:-+|\*+)\s+|\p{Nd}+[.)]+(?=\s|\p{L}{2})\s*|•\s*)+/u, "")
.slice(0, textLimit).trim();
}
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

// A token with no letter is digits and symbols only, wearing a list item's
// clothes, and that is not a skill on its own.
//
// A letter is what makes a token legible as a named thing: "ISO 27001" and
// "IEEE 754" keep the org name that scopes their number, "5G" and "3D" carry
// their own label, and a plain "5" or "27001" or "2015" carries no such
// scope, whether it arrived alone -- "Skills: 2025" -- or split off a shared
// prefix by parseResume's "," / ";" / "|" split -- "Skills: ISO 27001,
// 124141, 2015". Either way, there is nothing left to tell whether it is
// still part of a standard, a separate one, or an unrelated year. Rather
// than guess, every letterless token is dropped, with no exception: "24/7"
// and "100%" are the same digit-plus-symbol shape as "-50", "1-2", and
// "2020-2024", and none of them carry a letter to claim a meaning others
// would have to guess at.
//
// That costs bare part numbers -- 6502, 8051, 68000 -- which are real skills
// on a systems resume and have no letter either. The loss is accepted, not
// overlooked: "8051" and "2025" are the same shape, four bare digits, and
// nothing in the token says which is a part number and which is a year, so
// a filter that goes by shape and keeps one keeps the other. Both are
// dropped. A part number that names itself, "MOS 6502" or "Z80", is kept.
function unique(values, max) {
return [...new Set(values.map(clean).filter((value) => value.length >= 2))].slice(0, max);
return [...new Set(values.map(clean).filter((value) => /\p{L}/u.test(value)))].slice(0, max);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every letterless token goes, and on a systems resume that is the part numbers: 6502, 8051, 68000, 802.3 and 99.999% all cleared the old floor and clear nothing here. The rationale above covers years, percentages and split standards but not bare part numbers, which is the one class the audience named in the first test of this change lists without a letter anywhere near them. Worth saying whether that loss is accepted, since the title's bug (a two-character floor hiding C and R) does not require it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, tokens like 8051 or 6502 are format-wise indistinguishable from years (e.g., 2015) or arbitrary numbers without deeper contextual analysis.

Since we currently lack the context to tell them apart, treating letterless tokens uniformly is a trade-off we accept for now. I would update the PR rationale to explicitly call out this limitation.

}

function parseJd(lines) {
Expand Down