Keep one-letter skills without corrupting others - #50
Disesfgewu wants to merge 5 commits into
Conversation
jserv
left a comment
There was a problem hiding this comment.
Indent via make indent and then squash commits without Claude markers.
1496214 to
2ad5ddb
Compare
Sorry about that! I've run make indent, squashed the commits, and pushed the updates. Could you please take another look when you have a moment? Thanks! |
You didn't. Check git manual carefully for |
|
Oops. I check that again. |
2ad5ddb to
618f25b
Compare
|
Check Git Squash Commits: How to Combine Commits Into One by using |
ColtenOuO
left a comment
There was a problem hiding this comment.
It's recommended to make the commit message title relevant to the actual changes, as this helps others quickly understand the updates when reviewing.
That's very helpful advice. I'll apply this practice to my future commits. |
ColtenOuO
left a comment
There was a problem hiding this comment.
As a reminder, once the issue pointed out is fixed, we should add a regression test for it to ensure the bug doesn't come back in future updates.
618f25b to
5af2df8
Compare
jserv
left a comment
There was a problem hiding this comment.
Rebase latest main branch and refine commit messages properly.
1f9eec9 to
a1f91ad
Compare
unique() filtered candidates shorter than two characters to drop stray punctuation left over from a bad split, but the same filter erased legitimate one-letter skill names such as C and R before a candidate ever got to choose them for the interviewer. Drop the length floor and keep only the empty-string filter it was meant to provide.
Dropping unique()'s length floor to keep one-letter skills also unmasked a latent bug: clean()'s leading-strip regex swallowed a bare digit run, so "5G" lost its "5" and survived as the fabricated skill "G" instead of being dropped like before. Only strip a genuine list marker -- a bullet glyph, or digits immediately followed by "." or ")" and then whitespace. Also drop split fragments that clean() leaves as pure punctuation, since those are leftover delimiters, not skills.
Apply suggestion to handle single digit edge cases and add regular expressions to cover additional document processing patterns.
a1f91ad to
c932cb1
Compare
| const jd = await parseGroundingFile(txt("1. Must know Rust\n2) Should know Go"), "jd"); | ||
| assert.deepEqual(jd.requirements, ["Must know Rust", "Should know Go"]); | ||
| }); | ||
|
|
There was a problem hiding this comment.
| 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"]); | |
| }); |
The tests still don't cover the edge case mentioned earlier. Whenever a bug occurs, it highlights a gap in our current test coverage. We should add a test for this specific scenario to prevent regressions in the future.
There was a problem hiding this comment.
I would add more tests now to edge cases.
|
Also, I don't think it's a good idea to describe the collaboration process in the commit body. It should focus on what was actually changed. This issue is still present in the current commit message. |
Update parsing logic to drop standalone numbers and decimals like 3.14. Refine inline documentation and add unit tests for edge cases.
| // front. | ||
| function clean(line) { | ||
| return line.replace(/^[-*•\d.)\s]+/, "").slice(0, textLimit).trim(); | ||
| return line.trimStart().replace(/^(?:(?:[-*]|\d+[.)])\s+|•\s*)+/, "").slice(0, textLimit).trim(); |
There was a problem hiding this comment.
Requiring whitespace after the marker breaks list styles that main handled. 1.Must know Rust and 1.) Must know Rust now reach the interviewer with the marker still attached, and so do *Should know Go and -- Must know Rust. Letting a numbered marker be followed by a letter keeps 5G and 3D intact, because the digits still need a . or ) right after them:
| return line.trimStart().replace(/^(?:(?:[-*]|\d+[.)])\s+|•\s*)+/, "").slice(0, textLimit).trim(); | |
| return line.trimStart().replace(/^(?:(?:-+|\*)\s+|\d+[.)]+(?=\s|\p{L})\s*|•\s*)+/u, "").slice(0, textLimit).trim(); |
*Should and hierarchical markers like 1.1 Must are still not stripped. Handling them without also eating *nix or 802.11 experience is worth a follow-up with its own tests.
There was a problem hiding this comment.
I would check this later.
Thank for your suggestion. :)
| return [...new Set(values.map(clean).filter((value) => value.length >= 2))].slice(0, max); | ||
| return [...new Set(values.map(clean).filter((value) => { | ||
| if (/\p{L}/u.test(value)) return true; | ||
| if (/^\d+(\.\d+)+$/.test(value)) return false; |
There was a problem hiding this comment.
\d only matches ASCII digits, but the other checks here use \p{N}, so ٣.١٤ and 5.2 skip this check, and the last line then keeps them. The comment above also says 3.14 could come from Python 3.14 being split on whitespace. parseResume only splits on ,, ; and |, so a bare 3.14 can only come from the source text. The PR description says 802.11 passes through, but this rule drops it, and the new test expects that.
| if (/^\d+(\.\d+)+$/.test(value)) return false; | |
| if (/^\p{Nd}+(?:\.\p{Nd}+)+$/u.test(value)) return false; |
| if (/\p{L}/u.test(value)) return true; | ||
| if (/^\d+(\.\d+)+$/.test(value)) return false; | ||
| if (/\p{N}/u.test(value) && /[/%]/.test(value)) return true; | ||
| return /\p{N}.*\p{N}/u.test(value); |
There was a problem hiding this comment.
/\p{N}.*\p{N}/u accepts any token with two digits anywhere in it, so -50, (3.14), 3.14., 1-2 and 2020-2024 all become skills. On main, clean() reduced every one of them to an empty string. Worse, Skills: 2020, 2021, 2022, 2023, 2024, 2025, 2026, 2027, Rust now fills all eight skill slots with years and drops Rust. If the goal is to keep plain digit runs like 27001, match exactly that:
| return /\p{N}.*\p{N}/u.test(value); | |
| return /^\p{Nd}{2,}$/u.test(value); |
|
|
||
| test("a bare multi-digit integer is not treated the same as a digit.digit fragment", async () => { | ||
| // Unlike "3.14", a plain digit run has no dot to make it read as a split | ||
| // version number or GPA, so it keeps surviving the way it always has. |
There was a problem hiding this comment.
"keeps surviving the way it always has" is wrong. On main, clean() stripped every leading digit, so 27001, 2015 and 754 all came out empty and were dropped. Keeping bare numbers is new behavior in this PR, so the comment and the PR description should say so. They should also explain why a year like 2015 belongs in a skills list at all.
There was a problem hiding this comment.
The meaning of this is that if the skill start from “GPA 3.87” for example. It will keep surviving. If it only “3.87” would be filtered.
For second 2015 in next test. The reason that it could keep surviving is “ISO” text in the front. but it still has a problem that we could not figure out if ISO 2015 is a license or not by regular expression.
So in my opinion to deal with these the only way is using LLM or NLP method to do it but it will cost a lot for token and time latency.
There was a problem hiding this comment.
So in my opinion to deal with these the only way is using LLM or NLP method to do it but it will cost a lot for token and time latency.
Use deterministic approaches wherever possible, so we can enforce restrictions through predefined rules.
There was a problem hiding this comment.
I think use the deterministic method is more convenient and would check it later.
But so in these case, for example “Skills: ISO 2016, 27087, 202610” something like these input, for deterministic approach would pass due to ISO text here, but we still need to pass the text in real ISO like “ISO 27001”. We still have no way to solve but we use specific way only for this case.
That’s not a great coding way because if do it, we need to cover all possible cases. 🫠
There was a problem hiding this comment.
ISO does not carry over to the following values here. parseResume() splits Skills: ISO 9001, 27001, 2015 into three independent candidates, so 27001 and 2015 are currently accepted only because bare multi-digit strings pass the filter.
The deterministic rule does not need to verify whether ISO 2016 is a real standard. It only needs to require enough context in each candidate: keep a token if it contains a Unicode letter. That accepts ISO 27001, IEEE 754, 802.11ac, 5G, C, and R, while rejecting ambiguous bare values such as 27001, 2015, and 754.
If a document writes ISO 27001, 27002, inferring that the prefix applies to both values would be a separate parsing feature. The current parser does not preserve that relationship, so accepting the second number would be guessing.
There was a problem hiding this comment.
That’s not a great coding way because if do it, we need to cover all possible cases.
So, consider decoupling this feature in a future pull request. For now, only formatting checks are needed.
ColtenOuO
left a comment
There was a problem hiding this comment.
I think some of the newly added tests seem unrelated to this PR (e.g., single isolated skill) and might not be necessary.
While adding tests for different cases can be beneficial, I think we should have a clear rationale for why each change is being made. Just to clarify my earlier point about missing tests: I meant that we still need a test specifically reproducing the scenario where the bug occurred, rather than adding unrelated test cases to this PR.
Adding unrelated tests without a clear purpose might just introduce unnecessary review overhead.
cc. @jserv what do you think?
|
By the way, the PR description might need to be updated to reflect the latest changes, as some parts appear to be outdated. |
For small projects like this, it is acceptable to combine diverse changes into a single pull request, provided that each change has a clear purpose and the overall changes are not extensive enough to make the review difficult, allowing reviewers to still walk through them effectively. |
What
unique()filtered candidates shorter than two characters, meant to dropstray punctuation left over from a bad split. It also silently dropped
legitimate one-letter skills like
CandRbefore a candidate ever got tochoose them for the interviewer.
Dropping that filter unmasked a second, latent bug:
clean()'s leading-stripregex also swallows a bare digit run, so
5Glost its5and survived as thefabricated skill
Gonce nothing was left to catch the mangled remnant.Why
clean()now only strips a real list markegitsimmediately followed by
.or)and then whitespace -- not a bare leadingdigit.
5G,3D,4K,802.11pass throowRust
still loses its1. `.unique()now keeps a token only if it hasgit,so a split fragment that is pure punctuation (a stray
/,#,&leftover from a bad split) is dropped instead ol.
Testing
scripts/test.sh: 438/438 non-skipped testsdigit-led skills, numbered-list stripping on requirement lines, and
punctuation-only split fragments.
Summary by cubic
Keeps one-letter skills like
CandRduring grounding extraction instead of dropping every candidate shorter than two characters.Fixing that exposed a second bug:
clean()stripped a bare leading digit run as if it were a list marker, so5Glost its5and survived as the fabricated skillG.clean()now strips only real markers such as1.or2)followed by whitespace, so5G,3D, and4Ksurvive while numbered-list stripping on requirement lines still works.unique()now drops pure-punctuation fragments, lone numbers, and ambiguous bare decimals like3.14; letter-bearing tokens, plain digit runs, and values like24/7or100%still pass.Written for commit 20931a6. Summary will update on new commits.