feat(oss-writing): check the mechanical prose tells and gate on them - #24
Open
svyatov wants to merge 10 commits into
Open
feat(oss-writing): check the mechanical prose tells and gate on them#24svyatov wants to merge 10 commits into
svyatov wants to merge 10 commits into
Conversation
Extraction blanks masked regions rather than deleting them, so a finding's line and column stay true to the source a reader has open. A four-space indent under a list item is a CommonMark continuation paragraph, not a code block, so masking every indent blind would stop checking real prose and return a false clean. The list rule is a heuristic rather than a parser, and it errs toward prose.
A pattern is an offence when no correct repo prose contains it, and a
suspicion when the catalog documents a legitimate use or the same
spelling has an ordinary technical noun sense. That rule, not the seed
list, decides each entry, so `cache key` prints and does not block.
Emoji detection uses `\p{Extended_Pictographic}`. `\p{Emoji}` matches the
ASCII digits and `#`, which would report every version number.
The Claude co-author trailer is matched without regard to case, because
git and GitHub both write `Co-authored-by` and the tell is the same one
either way.
`blazing` and `effortless` join the promotional adjectives row of
`references/tells.md`. R-DOC-05 already names both, and neither appeared
in the catalog the script has to agree with.
The sentence start is the first word, or the word after a leading label token: a first or second token ending in `:`, or a first token ending in `.`. Without that rule the checker reports this repository's own house style, where a rule heading reads `### R-DOC-01: The README opens with one sentence` and a procedural heading reads `## Step 1: Find STANDARD.md`. `Go` and `Actions` stay out of the default allowlist. Both are ordinary English words, and shipping them buys a false negative on `## read the Actions log` to avoid a false positive a repository clears with one entry of its own. A repository declares its own proper nouns in `.oss-kit.json`, under an `oss-writing` key, so later settings have a file to go in. Two auditors passing different flags would otherwise score the same repository differently, which is the failure R-DOC-05's rationale exists to prevent. The 51 headings this repository trips are all product names: Claude Code, GitHub Actions, Agent Skills, OpenSSF Scorecard, Keep a Changelog.
The mode restricts both the file set and the pattern set to that rule's `Check:` line, and promotes every finding to an offence, because `oss-audit` scores pass or fail from the exit code and needs the same answer on two runs. The file list is sorted for the same reason: readdir order is not stable across platforms. It reads `git ls-files` rather than the filesystem. `.gitignore` excludes `docs/` in full here, and that directory holds agent-facing planning documents `AGENTS.md` says are untracked by design, so a plain recursive walk fails this repository on prose it deliberately excludes from its own standard. Outside a git checkout the walk is the fallback. The mode takes the repository root as its one positional argument, since `oss-audit` scores a repository that is usually not the working directory, and rejects a second one so a mistyped path cannot read as a clean run. `--allow` is rejected here: two auditors passing different flags would score the same repository differently, so the allowlist comes from `.oss-kit.json` at the root. From `CHANGELOG.md` it takes the text under every `##` heading, excluding the preamble, the heading lines, and the link reference definitions, so a version range and a compare URL are not read as prose.
Both hooks resolve the checker from their own directory rather than from the repository root. Installing one skill puts it under a host-specific path such as `.claude/skills/oss-writing/`, where a root-relative lookup would take the skip branch on every commit and report nothing forever. The installer points `core.hooksPath` at the shipped directory rather than copying either hook anywhere, so there is one source and nothing to drift. It runs the checker over tracked Markdown first and leaves `pre-commit` disabled where that comes back dirty: the hook reads whole files, so in a repository with a backlog it blocks every commit on prose nobody in that commit wrote, which is how a hook gets uninstalled. The switch is `osskit.precommit`, which the hook reads on each run. `commit-msg` drops git's own comment lines and everything below the scissors line `git commit --verbose` adds, so a diff is not read as prose.
The drift test asserts every pattern the script carries is named in `references/tells.md` or `SKILL.md`, so deleting a tell from the prose without touching the script fails CI. A regex source is not searchable prose, so each entry carries an explicit token for this. The two dash checks in `authored-docs.test.ts` now call the checker over the same file set, which removes the second `proseOnly` implementation. That loosens one thing: the old test read raw bytes, so it caught a dash inside a fence on the three site pages, and routing it through the prose stripper stops that. Reproducing quoted material verbatim is what the skill's own Do not touch section requires, so this is the correct behaviour rather than a regression.
…e clause The `Check:` line names the script, following the precedent R-SKL-02 set by naming the validator, and scopes its evidence to prose outside code blocks, code spans, and quoted material, which is what the checker measures and what the skill's own Do not touch section requires. It also drops the phrase `describes the project`. No token match can decide whether an adjective describes the project, so the qualifier was never scored, and leaving it in the standard would have left the script contradicting the rule it implements. Dropping it tightens what the rule requires, which the README's Versioning section calls an incompatible change: a repository that passed yesterday can fail today. The version is below 1.0.0, so it ships in a MINOR release and `CHANGELOG.md` says so. An auditor without `skills/oss-writing/` installed marks the rule unknown rather than scoring it by reading. `oss-audit` supports a single-skill install, so the script may be absent, and unknown is the third state that skill already defines for evidence it cannot reach. The README's Versioning section now covers the checker's interface beside the validator's, because every audit depends on it once a `Check:` line names it, and a patch release renaming `--rule` would break audits silently. `bun run lint:prose` runs the checker over every tracked Markdown file. `CONTRIBUTING.md` documents it, which is what makes R-CI-02 bind, and the `check` job runs it. The `runtime` job runs the audit mode beside the validator, so both shipped scripts prove the same Node 22 floor on the same matrix.
…twice Two regex literals sat inside loops, one per line of a fenced block and one per heading, where every other pattern in the file is a module-level constant. Hoisting them matches the surrounding style and removes the per-iteration allocation. findConfig read each candidate file in full and threw the contents away, only to prove the path existed, and then readConfigAllow read the same path again to parse it. A stat is what an existence walk needs, and isFile keeps the walk stepping past a directory that happens to carry the name.
maskProse built its character array by spreading the string, which yields one element per code point, then indexed it with offsets counted in UTF-16 units. One emoji put the two a slot apart, so masking landed off its target and the code inside a fence was scanned as prose. Splitting keeps both in the same units. The inline-header-bullet pattern flagged the Breaking marker R-CHG-01 requires on every incompatible changelog entry. The two rules contradicted each other, and CI would have rejected a correctly marked entry. The marker is now exempt, and this release's own entry carries it. The rule mode fell back to walking the filesystem when git ls-files failed, so the same tree scored one way inside a checkout and another way outside it. That is the disagreement the mode exists to prevent, so it now reports the rule unknown and exits 2. The walk had no other caller and is gone. An empty file set now says so too, because a silent exit 0 reads as a pass with no evidence behind it. Four callers passed file names without the option terminator the script already supports, so a Markdown file whose name begins with a hyphen was read as a flag and never checked. One of them was the command the installer prints for a reader to paste into their own CI. The installer recorded core.hooksPath with symlinks unresolved, which left an agent loaded through a linked path comparing it against the real directory and offering the wiring again every session. findConfig now stops at the repository root, because an allowlist decides what counts as an offence and one above the checkout would change how the repository scores.
The check reported every capitalized word past a heading's sentence start, so it had to know which capitalized words are proper nouns. That set is unbounded, which is why it carried DEFAULT_ALLOW, an `--allow` flag, `.oss-kit.json` with a bounded walk to the repository root, and a label heuristic protecting this repository's own `R-DOC-01:` and `Step 1:` headings. Across 371 headings here it reported nothing, and needed 11 allowlist entries to stay that quiet. Deciding on the whole heading retires the question. Three or more words past the first, all capitalized, is Title Case; `## configure GitHub Actions` clears on the count and `## read the Actions log` clears on `the`, so no repository declares its own proper nouns. `### R-DOC-01: The README opens with one sentence` clears because the rest is a sentence, which retires the label heuristic, and the Contributor Covenant's headings run one or two words, which retires the code of conduct exemption. What escapes is Title Case that lowercases its short prepositions, so `## Getting Started with Docker` goes unreported. R-DOC-05's `Check:` line now asks for what a checker can decide rather than for sentence case throughout, which loosens what the rule requires. A finding quotes its heading from the source rather than the masked copy, so a code span reads as the author wrote it. `maskProse` normalizes CRLF, so that copy is normalized first; slicing the argument would run a byte per line off on a CRLF file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This repository's prose rules could only be enforced by a reader, so R-DOC-05 was scored by judgment and the house style drifted between files.
oss-writingnow ships a dependency-free checker atskills/oss-writing/scripts/check-tells.mjsthat finds the mechanical subset of its tells, plus a--rule R-DOC-05mode that produces that rule's evidence foross-audit. Git hooks and CI run it, and the skill offers to install the wiring once, on evidence.The tradeoff is that half the catalog stays unenforced. A pattern the token alone cannot decide, such as
keyincache key, prints as a suspicion and leaves the exit code alone; only a pattern with no legitimate use in technical prose fails a build. Blocking on suspicions would make a checker people uninstall.A model now runs the checker over a draft rather than reading its own prose against a list, and stops offering the hooks once
core.hooksPathnames the shipped directory. No failing run sits behind this. The rules were unenforceable rather than misapplied, so the driver was the gap itself, not a transcript.R-DOC-05's
Check:line names the checker and drops the phrasedescribes the project, which no token match can decide. That tightens the rule, so a repository that passed yesterday can fail today.Verification beyond the checks CI runs: a review found four defects, and each was reproduced in a throwaway checkout and re-run after its fix. An emoji ahead of a fenced block leaked the code inside it into the scan, because the mask was built per code point and indexed per UTF-16 unit. The checker rejected the Breaking marker R-CHG-01 requires, which would have deadlocked CI against a correctly marked changelog entry. The rule mode answered from the filesystem outside a git checkout, scoring one tree two ways. Four callers omitted the option terminator, so a Markdown file whose name begins with a hyphen was read as a flag and never checked. Node 22 and 24 were not exercised locally and are covered by the CI matrix; an independent cross-model review was attempted and timed out without producing output.
BREAKING CHANGE: R-DOC-05 requires more than it did. Run
node skills/oss-writing/scripts/check-tells.mjs --rule R-DOC-05 .and clear what it reports.Affects: R-DOC-05, oss-writing, oss-audit