Conversation
- web/: static site with live results for every style, click to copy, shareable ?t= URLs, dark mode, no build step (imports index.js directly) - netlify/functions/api.js: single Functions 2.0 handler for /api/* (styles, :style/:text, all, chain, batch) with CORS, caching, limits - netlify.toml: publish web/, copy index.js at build, security headers - Tests for the API handler via Request objects and a browser-safety guard - Docs: README Try it online + HTTP API sections, CLAUDE.md, AGENTS.md
- Trigger CI for pull requests against any branch so stacked PRs get checks - npm audit fix: clears 22 Dependabot alerts in dev-only transitive deps
… in range npm audit fix had bumped vitest to 4.1 / vite 8, which requires Node 20.19+ and broke the Node 18 CI job. Revert that and update only the transitive packages that have fixes within vitest 4.0.18's ranges (18 of 22 alerts). The remaining 4 are in vite 7 / @vitest/mocker and are dev-only; they clear when Node 18 support is dropped.
… polish - Require Node 22.12+; CI matrix 22/24; vitest 5, eslint 10; zero audit findings - API: GET /api/badge/:style/:text (SVG), /api/lorem/:count?style=, /api/spell/:text?style= (nspell + dictionary-en as devDependencies, bundled into the function; the npm package stays zero-dependency) - Site: chain builder (+ on each row, ?chain= in the URL), Open Graph and Twitter meta tags, 404 page, version in footer from /api - 141 tests, 100% coverage on index.js, cli.js, api.js
- web: keep '+' in text intact in shareable URLs (only chain uses literal +)
- cli: flags may follow text ('capstring hello --all' no longer drops the text)
- api/spell: 500-char limit, at most 50 suggestions per request (reported as
'limited'), dedupe case variants, uncache a failed dictionary load, log
unexpected errors before the 500
- api/lorem: count must be plain digits (no hex, exponent, whitespace)
- api/badge: ?label= capped at 100 chars
- api: export RESERVED segments and test they never collide with STYLES
- index: tokenize lazily so non-code styles skip toWords
- vitest: testTimeout was nested under coverage and ignored
- CLAUDE.md: branch settings live in the Netlify UI, not netlify.toml
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 073075b73e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| command = "cp index.js web/capstring.js" | ||
| functions = "netlify/functions" | ||
| # Skip the build when nothing deployable changed (docs-only commits) | ||
| ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF -- index.js web netlify netlify.toml package.json" |
There was a problem hiding this comment.
Force uncached Netlify deploys to run
On an initial or cache-cleared Git deploy, Netlify sets CACHED_COMMIT_REF equal to COMMIT_REF, so this git diff --quiet exits 0 and cancels the build instead of running the copy and function-bundling steps. Netlify documents both the equal-ref behavior and that exit code 0 stops the build (environment variables, ignore commands); explicitly return 1 when the refs match before running the diff.
AGENTS.md reference: AGENTS.md:L63-L65
Useful? React with 👍 / 👎.
| "prepublishOnly": "npm run lint && npm run test:coverage" | ||
| "prepublishOnly": "npm run lint && npm run test:coverage", | ||
| "web:prep": "cp index.js web/capstring.js", | ||
| "dev": "npm run web:prep && netlify dev" |
There was a problem hiding this comment.
Install the CLI invoked by the dev script
On a fresh checkout, npm install does not provide a netlify executable because netlify-cli is absent from both dependencies and the lockfile, so npm run dev exits with netlify: not found unless contributors happen to have an unrelated global installation. Netlify's installation guide requires either a global install or adding netlify-cli as a development dependency; add the local dependency so the documented command is reproducible. Netlify CLI installation documentation
AGENTS.md reference: AGENTS.md:L65-L65
Useful? React with 👍 / 👎.
| /** The curl command for the chain, the selected style, or all styles */ | ||
| const curlFor = (text) => { | ||
| const encoded = encodeURIComponent(text); | ||
| if (chain.length) return `curl ${API}/chain/${chain.join('+')}/${encoded}`; |
There was a problem hiding this comment.
Shell-escape the generated curl URL
For input containing characters that encodeURIComponent leaves untouched, such as don't or foo(bar), the displayed unquoted command is invalid shell syntax (' opens an unmatched quote and parentheses are parsed by the shell). Since this value is explicitly offered through “Copy curl,” percent-encode the remaining shell metacharacters or safely quote the entire URL before presenting it.
Useful? React with 👍 / 👎.
| if (corrections.length >= MAX_SPELL_SUGGESTIONS) { | ||
| limited = true; | ||
| return word; | ||
| } | ||
| const [suggestion] = checker.suggest(word); |
There was a problem hiding this comment.
Count spellcheck attempts rather than successful corrections
When checker.suggest(word) returns no suggestion, corrections.length never increases, so the advertised limit does not bound the expensive operation at all. For example, repeating the test's known-unsuggestable xqzjvwpk within the 500-character limit invokes suggest() for every occurrence without ever reaching this guard; track suggestion attempts separately so malformed public requests cannot bypass the intended CPU bound.
AGENTS.md reference: AGENTS.md:L64-L64
Useful? React with 👍 / 👎.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
- netlify.toml: force the build when CACHED_COMMIT_REF equals COMMIT_REF (first or cache-cleared deploys) instead of skipping it - npm run dev uses npx netlify-cli so it works without a global install - web: percent-encode shell metacharacters in the generated curl line - api/spell: the 50-suggestion budget counts attempts, so unsuggestable words can no longer bypass the CPU bound
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5501475b1e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "@eslint/js": "^10.0.1", | ||
| "@vitest/coverage-v8": "^5.0.3", | ||
| "dictionary-en": "^4.0.0", | ||
| "eslint": "^10.11.0", |
There was a problem hiding this comment.
Match ESLint's Node 22.13 development floor
On Node 22.12.x—which the package's engines range and documentation explicitly support—an engine-strict npm ci fails because the locked ESLint 10.11.0 and several of its packages require ^22.13.0 (see package-lock.json around lines 983-1028). Either retain an ESLint version supporting 22.12 or raise and consistently document the minimum development Node version.
Useful? React with 👍 / 👎.
| if (head === 'styles') { | ||
| requireMethod(req, 'GET'); | ||
| noExtraSegments(rest); | ||
| return respond({ count: STYLES.length, styles: STYLES, categories: CATEGORIES }, { pretty }); |
There was a problem hiding this comment.
Honor the requested text format for the styles route
For GET /api/styles?format=txt, getFormat() accepts txt, but this direct respond() call omits the parsed format and therefore still returns JSON with an application/json content type. This contradicts the documented global ?format=txt behavior; provide an appropriate plain-text representation here or explicitly reject/limit that option for this endpoint.
Useful? React with 👍 / 👎.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0aae7367a6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| functions = "netlify/functions" | ||
| # Skip the build when nothing deployable changed (docs-only commits) | ||
| # Netlify sets CACHED_COMMIT_REF == COMMIT_REF on first or cache-cleared deploys; exit 1 forces the build then | ||
| ignore = "test \"$CACHED_COMMIT_REF\" = \"$COMMIT_REF\" && exit 1; git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF -- index.js web netlify netlify.toml package.json" |
There was a problem hiding this comment.
Rebuild when the dependency lockfile changes
When a commit changes only package-lock.json—for example, a Dependabot transitive dependency update—this path-limited command sees no difference and Netlify skips the build, leaving the deployed function on the old locked dependencies. Git documents that --quiet implies --exit-code, returning 0 when the selected paths have no differences (git-diff options); include package-lock.json among the deployable paths.
Useful? React with 👍 / 👎.
| if (input.length > MAX_TEXT) return { input, output: null, error: 'text_too_long' }; | ||
| return { input, output: capstring(input, style) }; | ||
| }); | ||
| return respond({ style, count: results.length, results }, { pretty, cache: false }); |
There was a problem hiding this comment.
Honor text formatting in batch responses
When a client sends POST /api/batch?format=txt, getFormat() accepts the requested format, but this direct respond() call omits it and always returns JSON with application/json. That contradicts the global plain-text behavior advertised in web/index.html:55 and README.md:220; either serialize batch results as text and pass format, or explicitly reject txt for this endpoint.
Useful? React with 👍 / 👎.
|
@codex review |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 44b3c4efaa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (styles.length > MAX_CHAIN) throw new ApiError(400, 'chain_too_long', `Chain at most ${MAX_CHAIN} styles.`); | ||
| styles.forEach(getStyle); | ||
| const text = getText(textSegments, query); | ||
| const output = styles.reduce((acc, s) => capstring(acc, s), text); |
There was a problem hiding this comment.
Bound intermediate chain output
A chain of expanding styles can exhaust the function's memory even though the input and chain length pass their limits. For example, applying binary five times to a permitted 2,000-character input produces about 118 MB, while subsequent iterations exceed a gigabyte and continue toward the allowed ten styles. Enforce a size limit after each reduction step, rather than bounding only the initial text and number of styles, so a single public request cannot crash the function.
Useful? React with 👍 / 👎.
|
|
||
| const params = new URLSearchParams(location.search); | ||
| if (params.get('t')) input.value = params.get('t'); | ||
| chain = (params.get('chain') ?? '').split(/[+,\s]/).filter((s) => STYLES.includes(s)).slice(0, MAX_CHAIN); |
There was a problem hiding this comment.
Bound chain expansion when loading shared URLs
Because shared URLs may supply ten arbitrary valid styles, visiting a URL whose chain repeats binary can synchronously allocate enormous strings during the initial render(). With a 2,000-character t value, five binary entries already produce about 118 MB and a sixth exceeds 1 GB, so a crafted share link can freeze or terminate the visitor's tab. Limit intermediate output size or reject repeated expanding transformations before rendering the restored chain.
Useful? React with 👍 / 👎.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 02d6837d6f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
| }); | ||
|
|
||
| document.getElementById('chain-copy').addEventListener('click', () => copy(chainResult(), `chain: ${chainResult()}`)); |
There was a problem hiding this comment.
Copy the rendered chain result
When a chain contains random, clicking “Copy result” calls chainResult() twice instead of copying chainOut, so the clipboard value can differ from both the visible output and the value named in the toast. Compute the result once—or copy chainOut.textContent—so users receive the result they selected.
Useful? React with 👍 / 👎.
| const body = format === 'txt' | ||
| ? STYLES.map((s) => `${s}\t${results[s]}`).join('\n') | ||
| : { input: text, count: STYLES.length, results }; |
There was a problem hiding this comment.
Escape multiline values in all-style text output
For valid input containing CR/LF, styles such as same, title, and upper preserve those line breaks, so inserting results directly into this newline-delimited format destroys the one-row-per-style structure. For example, /api/all/a%0Ab?format=txt returns 58 lines for 37 styles, making the output ambiguous to consumers; escape embedded line breaks as the CLI table does or use an unambiguous framing format.
Useful? React with 👍 / 👎.
| const payload = format === 'txt' | ||
| ? results.map((r) => r.output ?? '').join('\n') | ||
| : { style, count: results.length, results }; |
There was a problem hiding this comment.
Preserve batch record boundaries for multiline outputs
Although the batch route now forwards format=txt, the new newline join is ambiguous when a valid input itself transforms to multiline text. For example, two same inputs ['a\nb', 'c'] produce three apparent output lines, violating the route's intended one-output-line-per-text behavior; escape embedded CR/LF or use a framing format that preserves item boundaries.
Useful? React with 👍 / 👎.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a00acc6148
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const variants = new Set([word, word.toLowerCase(), word.toUpperCase(), word[0].toUpperCase() + word.slice(1).toLowerCase()]); | ||
| return [...variants].some((variant) => checker.correct(variant)); | ||
| }; | ||
| const output = text.replace(/[A-Za-z]+/g, (word) => { |
There was a problem hiding this comment.
Preserve contractions as single spellcheck tokens
For correctly spelled contractions, the regex checks each alphabetic fragment instead of the dictionary entry containing the apostrophe. For example, 51 repetitions of isn't fit within the 500-character limit but cause isn to be treated as an unknown word each time, exhausting the suggestion budget, setting limited, potentially rewriting correct text, and leaving a real typo later in the request unchecked. Include internal straight and typographic apostrophes in each word token.
Useful? React with 👍 / 👎.
Summary
Stacked on #8. Adds the fun face (website) and the serious face (HTTP API) of capstring, in this repo, with zero build step. Also raises the Node floor to 22.12 so the test toolchain can move to vitest 5 / eslint 10 and clear every Dependabot alert.
Website (
web/)+on any row to compose up to 10 styles; result, copy button, and/api/chaincurl update live. Shareable as?chain=upper+reverse.?t=URLs, dark mode, mobile layout, Open Graph + Twitter meta, 404 page, version in footer from/api../capstring.js, which the Netlify build copies fromindex.js.API (
netlify/functions/api.js, one Functions 2.0 handler owning/api/*)GET /api/styles,GET /api/:style/:text,GET /api/all/:text,GET /api/chain/:styles/:text,POST /api/batchGET /api/badge/:style/:text(shields-style SVG,?label=),GET /api/lorem/:count?style=,GET /api/spell/:text?style=(nspell + dictionary-en, lazy-loaded so other routes don't pay for it)Cache-Controlpublic except forrandomand POST,?format=txt,?pretty=1,?text=override{ error: { code, message } }responses.Dependencies:
nspellanddictionary-enare devDependencies on purpose. esbuild bundles them into the function; the npm package stays zero-dependency.dictionary-enis marked external +included_filesinnetlify.tomlbecause it reads its.aff/.dicfiles by path.Node: engines
>=22.12.0, CI matrix 22 and 24. Node 18 and 20 are end-of-life. Documented under Behavior changes in CHANGELOG.Test plan
npm run lintclean (eslint 10)npm run test:coverage141 tests, 100% statements / branches / functions / lines on index.js, cli.js, api.jsnpm audit0 vulnerabilitiesnpm pack --dry-runstill excludes web/ and netlify/npm run dev: curled transform, slug, batch, spell (helo speling→Hello Spelling), badge (SVG), lorem (?style=kebab), 404 page,/capstring.js?t=, click-to-copy toast fired, curl line switched to the selected style, no console errorsAfter merging:
netlify initin the Brian Funk team as sitecapstring, production branchmaster, branch deploys fordev.