[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M78039RQPMDWJBZHRPRHBN40":3,"skill-same:01M78039RQPMDWJBZHRPRHBN40":45},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":24,"commitSha":25,"skillDirectory":26,"forksCount":27,"repositoryHtmlUrl":28,"repositoryHomepageUrl":6,"ownerHtmlUrl":29,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":30,"install":31,"resources":44},"01M78039RQPMDWJBZHRPRHBN40","hardening-a-parser-port",null,"Use when porting or writing a recursive-descent parser, lexer, or tree-walker over untrusted text in the @effected monorepo — the class of hardening the cloud reviewer scans for on every migration. Covers stack-overflow depth guards (on EVERY recursion surface, which a facade has N of — not two), numeric bound guards that must reject NaN and non-integers, code-point range checks scoped to formats with wide escapes, prototype-pollution, control-character rejection, and the invariant that malformed input must fail through the typed error channel, never as an unhandled defect.","spencerbeggs\u002Feffected","spencerbeggs","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F109114?v=4","01M77ZTHB70K5Y2VVTRCFY50A5","C. Spencer Beggs","01M77ZTHCC41NXBT3X906K6QM1",8,"MIT","https:\u002F\u002Fgithub.com\u002Fspencerbeggs\u002Feffected\u002Fblob\u002Fmain\u002Fplugin\u002Fbuilds\u002Fclaude\u002Fskills\u002Fhardening-a-parser-port\u002FSKILL.md",[],[],"2026-11-15 14:19:17",false,"---\nname: hardening-a-parser-port\ndescription: Use when porting or writing a recursive-descent parser, lexer, or tree-walker over untrusted text in the @effected monorepo — the class of hardening the cloud reviewer scans for on every migration. Covers stack-overflow depth guards (on EVERY recursion surface, which a facade has N of — not two), numeric bound guards that must reject NaN and non-integers, code-point range checks scoped to formats with wide escapes, prototype-pollution, control-character rejection, and the invariant that malformed input must fail through the typed error channel, never as an unhandled defect.\n---\n\n# Hardening a parser port\n\nA pure parser takes untrusted strings in and must produce values or *typed\ndomain errors* — never a crash, never an unhandled Effect defect. These are\nthe recurring hardening items the cloud reviewer re-derives against source on\nevery `@effected` parser migration (jsonc, yaml, and the boundary-tier ports\nto come). Write each guard together with its hostile-input regression test; a\nhardening claim without a test is unverified.\n\n## No unbounded recursion — enumerate every recursion surface, then close each\n\nAny recursive descent over untrusted input needs a depth cap, or deeply-nested\ninput throws `RangeError: Maximum call stack size exceeded` as a defect that\nescapes the typed channel.\n\n**Enumerate the surfaces before you guard any of them.** A *recursion surface* is\nany function that re-enters itself over user-controlled structure. A package's\npublic facade with N recursive helpers has **N surfaces, not two** — and they do\nnot share a cap just because they share a tree. Grep for self-recursion across\n`src\u002F`, list what you find, and close every entry on the list. The two-stage\nCST\u002Fcomposer pipeline below is **one instance of this rule, not the frame**.\n\n`@effected\u002Fjsonc` is the worked example: an *iterative* scanner and a\nsingle-stage recursive-descent parser, yet **six** independent surfaces, each of\nwhich overflowed the stack on `\"[\".repeat(20000) + \"]\".repeat(20000)` before it\nwas closed:\n\n| # | surface | closed by |\n| --- | --- | --- |\n| 1 | `internal\u002Fparser.ts` value mode (`parseValue`→`parseArray`\u002F`parseObject`) | cap |\n| 2 | `internal\u002Fparser.ts` tree mode (`parseValueTree`→`…Tree`) | cap |\n| 3 | `JsoncNode.evaluateNode` (backs `toValue`) | cap |\n| 4 | `JsoncVisitor.visitGen` | cap |\n| 5 | `Jsonc.deepEqual` | cap |\n| 6 | `internal\u002Fnavigate.skipValue` | **rewritten iteratively** |\n\nTwo remedies, and the second is often the better one:\n\n- **Cap it** at `MAX_NESTING_DEPTH`; on exhaustion emit a single fatal diagnostic\n  and return a leaf placeholder instead of recursing. Pair `enter`\u002F`exit` in\n  `try\u002Ffinally` at every collection entry.\n- **Remove the recursion.** `navigate.skipValue` skips a value by counting\n  bracket depth over the flat token stream (`navigate.ts:155`). Being\n  non-recursive it *cannot* overflow, so `navigate` and `JsoncModifier` need no\n  cap — and no test can regress one they don't have. Prefer this wherever the\n  walk is a skip or a scan rather than a transform.\n\nThen, whichever remedy:\n\n- **Measure the real overflow point and set the cap with wide margin.** Do not\n  guess — a multi-frame recursion chain overflows far shallower than \"levels of\n  nesting\" suggests (yaml overflowed at ~900 composer levels because each level\n  is several stack frames; the cap is 256).\n- Emit ONE fatal diagnostic (`e.code === \"NestingDepthExceeded\"` deduped), not\n  one per level.\n- **Paired codec directions must count depth in the same units — container\n  descent, never leaves.** toml's parse guarded at the opening bracket only\n  (scalar leaves unchecked) while its stringify guarded at the top of every\n  node, leaves included: a document at exactly the cap with a **non-empty**\n  innermost element parsed but failed to re-emit — a typed failure both ways,\n  so nothing defected, but the round-trip broke at the exact boundary and the\n  \"same cap on both sides\" claim in the docs was quietly false. Neither a\n  679-case compliance corpus nor a differential oracle caught it, because the\n  only fixture at the bound had an *empty* innermost container (no leaf to\n  reach the phantom extra level). The fix and the regression discipline: hoist\n  guards into the container branches on both directions, and pin a round-trip\n  test at exactly the cap with a non-empty innermost value\n  (effected PR #31 review; fix `ecc5f1a4`).\n\n### The two-stage pipeline is a special case\n\nIn a two-stage `lex → build-tree → walk-tree` pipeline (yaml's CST parser then\ncomposer), the two surfaces are *coupled*, so their caps are chosen together:\n\n- Cap the **tree-walker** (composer) at `MAX_NESTING_DEPTH` (yaml: 256).\n- Cap the **tree-builder** (CST parser) slightly ABOVE it (yaml: 264), so the\n  walker's user-facing diagnostic fires first when the capped tree is walked; the\n  builder's guard is the backstop that keeps tree *construction* from overflowing.\n\nWhere a post-hoc walker instead runs over an **already-bounded** tree (jsonc's\n`evaluateNode`, `deepEqual`), **equal** caps are correct — the builder's output can\nnever exceed the walker's cap, so the walker's guard fires only on a hand-built\ntree. Do not mis-flag equal caps as a defect in that shape, and do not demand a\n+8 offset where the two surfaces are independent rather than chained.\n\n## Algorithmic complexity is its own hardening class — the DoS vector can be time, not stack\n\nDepth caps close the stack-overflow vector. Some grammars have a second,\nindependent vector: **superlinear parse time on small, flat input** — no\nrecursion, no nesting, nothing for a depth guard to see. Markdown is the\ncanonical case: nested strong emphasis (`*a **a **a …`), thousands of\nunmatched emphasis openers or link brackets, and repeated `[ (](` patterns\ndrive a naive matcher quadratic (or worse) while staying a few bytes wide and\none level deep. yaml's alias-expansion bomb — bounded by a materialized-node\nbudget (`AliasCountExceeded`), not a depth cap — is the same lesson from the\nmemory side: **amplification vectors are orthogonal to nesting depth.**\n\nTwo consequences for a port:\n\n- **The guard is the algorithm, not a limit constant** — with one recorded\n  carve-out. CommonMark's delimiter-stack emphasis algorithm and its\n  \"openers and closers multiple of 3\" rule exist precisely to make\n  pathological delimiter runs linear; an engine that \"simplifies\" them\n  re-opens the vector. Port the algorithm faithfully — there is no bolt-on\n  cap that fixes a quadratic matcher. **The carve-out: when the SPEC itself\n  sanctions a limit, the limit IS the algorithm's fix.** The bare\n  link-destination scan is quadratic on unclosed-link input in the\n  reference implementation too, and cmark's fix is `MAX_LINK_DEST_PARENS`\n  (32) under the spec's explicit \"implementations may impose limits on\n  parentheses nesting\" license — adopted verbatim in `@effected\u002Fmarkdown`\n  after the vendored reference was confirmed to share the flaw\n  (2026-08-02: 30k unclosed links, 5.2s → 30ms). The discriminator: a\n  spec-sanctioned cap bounds a *scan the grammar cannot otherwise\n  terminate*; a bolt-on cap papers over a matcher whose fix is a better\n  algorithm. Check the reference's own limit constants before assuming the\n  rule forbids constants.\n- **The regression instrument is a vendored pathological corpus with\n  timeout assertions.** cmark's `pathological_tests.py` is the pattern:\n  ~25 named cases (`\"*a **a\" * 65000`, 65k mismatched openers\u002Fclosers, deep\n  bracket nests, reference collisions), each asserting output *shape* via\n  regex under a wall-clock timeout — no HTML fixture needed. Vendor it and\n  run it as its own mandatory suite. Two operational rules from running one\n  at scale: prefer **ratio-based scaling guards** (time N vs 2N vs 4N;\n  instrumentation and hardware cancel) over absolute budgets where the\n  claim is complexity, and if the suite CALIBRATES its budgets against a\n  measured input, **recalibrate after any engine perf fix** — a calibration\n  input that rode the removed hot path now measures ~nothing, silently\n  un-scaling every other budget (this exact failure fired 2026-08-02).\n\n**A green conformance corpus says nothing about complexity.** Every\nconformance example is small and well-formed; an engine can pass all of them\nand still hang on twenty repeated bytes. For any grammar whose spec (or whose\nreference implementation's test tree) ships pathological cases, treat that\nsuite as a second, non-optional corpus — and where none exists, write the\nhostile generator yourself (`\"[\".repeat(65000)`-style) before declaring the\nengine hardened.\n\n## Numeric bound guards — `if (n \u003C LIMIT)` silently admits `NaN` and non-integers\n\nThe guard on a **numeric option** (a `maxDepth`, a size cap, an iteration bound)\nhas an obvious spelling that is wrong in a way invisible to review and to a\ngreen suite. `NaN \u003C 1` is `false`, and so is `2.5 \u003C 1` — every relational\ncomparison against `NaN` is `false`. So:\n\n```ts\nif (maxDepth \u003C 1) return yield* Effect.die(...);   \u002F\u002F NaN and 2.5 sail past\nfor (let depth = 0; depth \u003C maxDepth; depth++) { ... }  \u002F\u002F 0 \u003C NaN is ALSO false\n```\n\n`maxDepth: NaN` skips the guard **and** runs the loop zero times, returning an\nempty result — indistinguishable from a legitimate empty result, which is the\nexact silent outcome the guard was written to forbid. A fractional bound\ntruncates at a non-integer instead of dying. These are the two inputs a caller\nmost plausibly passes by accident: an unparsed `Number(env.MAX_DEPTH)`, or a\ncomputed average.\n\nGuard integrality and range together, and prove it:\n\n```ts\nif (!Number.isInteger(maxDepth) || maxDepth \u003C 1) {\n  return yield* Effect.die(new Error(`maxDepth must be a positive integer, received ${maxDepth}`));\n}\n```\n\nShip a test for `NaN` and one for a fractional value, each **watched failing\nagainst the bare `\u003C LIMIT` comparison** — a guard whose failure mode is\n\"returns empty\" is untestable by inspection. And when you document the guard,\nstate the full predicate: prose that says \"`maxDepth \u003C 1` is a defect\" invites\na reader to conclude `2.5` is accepted (this exact partial documentation\nshipped alongside the partial guard in `@effected\u002Fwalker` before both were\nfixed).\n\n## Range-check `String.fromCodePoint` fed by parsed hex — in formats that have wide escapes\n\n**Scope this check to the format before you go looking.** It applies only where an\nescape can denote a code point above the Unicode maximum `U+10FFFF`:\n\n| format | widest escape | max value | check needed? |\n| --- | --- | --- | --- |\n| YAML | `\\U00110000` (8 hex) | unbounded | **yes** |\n| JSON \u002F JSONC | `￿` (4 hex) | `0xFFFF` | **no** — structurally impossible |\n\nA JSON-family scanner using `String.fromCharCode` on a 4-hex value is *already*\ncorrect and needs no guard; `@effected\u002Fjsonc` is exactly this case. Do not go\nhunting for a `fromCodePoint` range hazard in a format that cannot express one,\nand do not flag its absence as a finding.\n\nWhere the format *does* have wide escapes, `String.fromCodePoint` throws a\n`RangeError` that escapes as a defect. Validate before the call:\n\n```ts\nconst cp = \u002F* parse hex *\u002F;\nif (cp \u003C= 0x10ffff) value += String.fromCodePoint(cp);\nelse return makeErrorToken(\u002F* … *\u002F); \u002F\u002F typed error, not a throw\n```\n\nGuard **both** the lexer (which builds the scalar value inline and throws first,\nmid-scan) and any composer re-decode of the same source.\n\n## `__proto__` and special keys → own data properties\n\nWhen building an object from parsed key\u002Fvalue pairs, a bare `obj[key] = value`\nmutates the prototype for `key === \"__proto__\"`. Route keys through\n`Object.defineProperty`, matching `JSON.parse` semantics:\n\n```ts\nif (key === \"__proto__\") {\n Object.defineProperty(obj, key, { value, writable: true, enumerable: true, configurable: true });\n} else {\n obj[key] = value;\n}\n```\n\nDo this at the single value-extraction choke point (`Node.toValue`), so both\nthe value path and any tree-walker share it.\n\nThe guidance above is for objects **you** build. When untrusted records flow\nthrough v4 `Schema.Record` instead, decode is already pollution-safe: a\n`__proto__` key survives decoding as an ordinary **own data property** of the\noutput — it is neither dropped nor written to the prototype (probed in\nthe `@effected\u002Flockfiles` hostility suite: a `__proto__` pnpm importer decodes\ninto an entry literally named `__proto__` with `Object.prototype` unpolluted).\nTwo consequences: don't pre-filter such keys expecting decode to choke on\nthem, and if you later copy a decoded record by hand, that copy loop is a new\n`obj[key] = value` surface needing the `defineProperty` route above.\n\n**The read side is a separate hazard, and it needs no hostile key at all.**\n`Schema.Record` decodes into a plain `{}` inheriting `Object.prototype`, so an\nunguarded `obj[key]` read with an untrusted or caller-controlled `key` resolves\n*inherited* members: `tags[\"constructor\"]` returns a function, `\"toString\"`,\n`\"hasOwnProperty\"` likewise — handed onward typed as your value type. Read\nuntrusted maps only through an ownership check:\n\n```ts\nconst value = Object.hasOwn(map, key) ? map[key] : undefined;\n```\n\nApply it at **every** indexed read of a decoded record, and sweep for the one\ncall site that skipped it — the ts-vfs port guarded its whole resolution\nmachinery and still shipped one unguarded read (`resolveVersion`'s dist-tag\nlookup), where `resolveVersion(name, \"constructor\")` genuinely returned an\ninherited function as a \"version\" until the PR #67 review caught it. Keys\nenumerated by `Object.keys`\u002F`Object.entries` are own-only and safe; it is the\n*lookup by external key* that must be guarded. Pin it with a hostile-ref test\n(`\"constructor\"`, `\"__proto__\"`, `\"toString\"`).\n\n## Test membership with `Object.hasOwn`, never bracket notation\n\nThe write path is only half of it. **Reading** a key off a parsed record with\nbracket notation inherits from `Object.prototype`:\n\n```ts\n\u002F\u002F UNSOUND — returns Option.some(\u003CFunction>) for a manifest with no deps at all.\nconst dep = deps[\"constructor\"];        \u002F\u002F → a Function, off Object.prototype\nif (dep !== undefined) return Option.some(dep);\n\n\u002F\u002F SOUND:\nif (Object.hasOwn(deps, name)) return Option.some(deps[name]);\n```\n\nVerified: `({})[\"constructor\"]` is a `Function`, while\n`Object.hasOwn({}, \"constructor\")` is `false`. A method declared to return\n`Option\u003Cstring>` therefore handed back an `Option.some(\u003CFunction>)` — **unsound,\nnot merely untidy**, and it type-checks perfectly because the index signature\npromises a `string`.\n\nEvery key an attacker names (`constructor`, `toString`, `valueOf`, `hasOwnProperty`)\nis a live hit. The tell in the real case: **every sibling predicate already used\n`Object.hasOwn`** — an inconsistency across a family of guards is a defect, not a\nstyle preference. Grep the family, not the line.\n\n## Reject unescaped C0 control characters\n\nRaw control characters below `0x20` (except tab `0x09`, LF `0x0a`, CR `0x0d`)\nare not printable per most text-format specs (YAML 1.2 §5.1, JSON §7). Scan the\ndocument's raw span once and emit a fatal diagnostic; escaped forms in\nquoted scalars never appear raw in the source, so scanning source text is\nsufficient.\n\n## `JSON.parse` returns `null`, a number, or a string — for VALID JSON\n\nA `=== undefined` guard after `JSON.parse` **never fires**. The four characters\n`null` are a perfectly valid `package.json`, and they parse to `null`, not\n`undefined`:\n\n| input | `JSON.parse` → | caught by `=== undefined`? |\n| --- | --- | --- |\n| `null` | `null` (`typeof` `\"object\"`!) | **no** |\n| `42` | `42` | **no** |\n| `\"hi\"` | `\"hi\"` | **no** |\n| `true` | `true` | **no** |\n\n```ts\n\u002F\u002F WRONG — `parsed.name` throws a TypeError on `null`, escaping as a DEFECT.\nconst parsed = JSON.parse(text);\nif (parsed === undefined) return yield* Effect.fail(new InvalidJson({ path }));\n\n\u002F\u002F RIGHT — reject anything that is not a non-null object.\nif (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return yield* Effect.fail(new InvalidShape({ path }));\n}\n```\n\nThis was live in **three** places in one package, each throwing a `TypeError` on\na property read — malformed input escaping as an **unhandled defect**, which is\nprecisely the invariant below. `typeof null === \"object\"` is what makes the\nplausible guard (`typeof parsed !== \"object\"`) insufficient on its own.\n\n## Path bases: `git diff --name-only` is REPO-relative, `git ls-files` is CWD-relative\n\nProbed in a scratch repo, both commands run from the subdirectory `pkgs\u002Falpha`,\nwith the **same file** modified:\n\n| command | printed |\n| --- | --- |\n| `git ls-files` | `nested.txt` — relative to the **cwd** |\n| `git diff --name-only` | `pkgs\u002Falpha\u002Fnested.txt` — relative to the **repo top-level** |\n\nOne file, two spellings. Put both in a `Set` and it holds two entries; compare\nthem and nothing matches.\n\n**Normalize each result from its OWN base — never from a single assumed one.**\n\n```ts\n\u002F\u002F WRONG — resolving ls-files against the repo root yields \u003Croot>\u002Fnested.txt,\n\u002F\u002F a path that does not exist. This IS the bug.\nresolve(repoRoot, p);\n\n\u002F\u002F RIGHT — each command's output resolved against the base it actually used:\nconst fromLsFiles = resolve(cwdTheCommandRanIn, p);   \u002F\u002F cwd-relative\nconst fromDiff    = resolve(repoTopLevel, p);         \u002F\u002F repo-relative\n```\n\n`git rev-parse --show-toplevel` gives the repo base; `--show-prefix` gives the\ncwd's offset from it. Simplest of all, make `ls-files` agree with `diff` at the\nsource: **`git ls-files --full-name`** prints repo-relative paths (verified — it\nreturned `pkgs\u002Falpha\u002Fnested.txt`).\n\nTwo further asymmetries the probe exposed, both easy to miss: `ls-files` is also\n**scoped** to the cwd (it listed only files under `pkgs\u002Falpha`), while\n`diff --name-only` reports the **whole repo** — so the two commands do not even\ncover the same set of files.\n\nThe generalizable rule, which is the reusable part:\n\n> **When combining the output of two commands, never assume they share a path\n> base. Verify each one's base independently.** Mixing bases is *silently\n> correct* whenever the two happen to coincide — which is the common case, and\n> the one your tests will cover.\n\nThat last clause is the testing consequence: **a fixture where the workspace root\nand the git root coincide cannot distinguish the correct implementation from the\nbroken one.** Any test for path-base handling must place the workspace root\n*below* the git root, or it proves nothing.\n\n## Structurally indistinguishable parses: position is the only sound discriminator\n\npnpm 11 writes a **two-document** `pnpm-lock.yaml` under `configDependencies`.\nNo *structural* rule can pick the right document: both carry `lockfileVersion`,\n`importers` and `packages`, and the preamble validates cleanly against the same\nschema. A \"pick the document that looks like a lockfile\" heuristic selects the\nwrong one and reports success.\n\n**POSITION is the only sound discriminator** — the real lockfile is the last (or\nthe nth) document, by the format's own framing rule. Which is exactly why the bug\nwas silent: a structural heuristic *did* find a valid-looking answer.\n\nGeneralize it:\n\n> When two candidate parses are structurally indistinguishable, a structural\n> heuristic is not a rule — it is a coin flip that happens to land right on your\n> fixtures. Discriminate on the framing (position, document index, an explicit\n> marker), and make the choice explicit in the code.\n\n## A port that builds a `RegExp` from input: force its compile inside the typed catch\n\n`new RegExp(source)` succeeding proves only that the syntax parsed. V8 builds\nthe code on the first `exec`, and a source too large for it throws a\n`SyntaxError` (\"Regular expression too large\") there. It builds that code\nseparately for a one-byte (Latin-1) subject and a two-byte one, each on its\nfirst `exec` of that kind, and a source of non-Latin-1 literals compiles\ntrivially for a one-byte subject, which can never match it. If the first\n`exec` of either kind is a match, the throw escapes a total `matches` as a\ndefect, outside every typed catch. `@effected\u002Fglob` met both: about 32,000\nliteral characters and one wildcard threw on `matches(\"x\")`, and\n`\"ā\".repeat(33_000) + \"*\"` survived a one-byte warm-up and threw on\n`matches(\"ā…\")`.\n\nSo run the regexp once on each kind of subject where it is built, inside the\ncatch that turns a `SyntaxError` into the typed failure:\n\n```ts\nconst compileOrFail = (source: string, flags: string): RegExp => {\n  try {\n    const regexp = new RegExp(source, flags)\n    regexp.test(\"\") \u002F\u002F forces V8's lazy one-byte compile\n    regexp.test(\"\\u0100\") \u002F\u002F and its two-byte one, so a match is never the first exec of either\n    return regexp\n  } catch (e) {\n    if (e instanceof SyntaxError) throw new GuardExceeded(\"InvalidRegExp\", 0, 0)\n    throw e\n  }\n}\n```\n\nOnly flags without `g` or `y` make that `test` stateless. A spy that counts\n`RegExp` constructions during matching cannot see this defect, because no\nconstruction happens there: pin it with a row whose source is too large, and\nassert that compiling fails typed and that `matches` never throws.\n\n## A string replacement reads `$` patterns: substitute with `split`\u002F`join`\n\n`String.prototype.replace` and `replaceAll` with a STRING replacement read\n`$&`, `$$`, `` $` ``, `$'` and `$\u003Cname>` as patterns, so substituting\nuntrusted text with `template.replaceAll(\"*\", match)` turns a match of\n`a$&b` into `a*b` and `` a$`b `` into the text before the star. Wherever the\nreplacement is data (a path segment, a file name, a pattern match), substitute\nliterally:\n\n```ts\nconst substituteEveryStar = (template: string, match: string): string => template.split(\"*\").join(match)\nconst substituteFirstStar = (template: string, match: string): string => {\n  const at = template.indexOf(\"*\")\n  return at === -1 ? template : template.slice(0, at) + match + template.slice(at + 1)\n}\n```\n\nA replacer function (`replace(re, () => match)`) is also literal. The one\nexception is deliberate parity with a reference that itself uses a string\nreplacement: `@effected\u002Ftsconfig-json` keeps `replace(\u002F\\*\u002Fg, subpath)`\nbecause tsc does, and pins it with a test.\n\n## A consumer-side regex over `raw` spans must strip continuations first\n\nA parser that keeps each token's `raw` source slice keeps whatever the format\nremoves before it tokenizes. Shell is the sharp case: bash deletes every\nbackslash-newline (`\\⏎`) before it reads a token, and so does\n`@effected\u002Fshell`, which resolves `[\\⏎[` to `[[`. But `raw` keeps the `\\⏎` as\nwritten. A guard that runs a pattern over `raw` (`\u002F\\(\\(\u002F`, `\u002F^\\[\\[$\u002F`,\n`\u002FPS4=\u002F`) misses the split spelling, and bash runs the joined one:\n`[\\⏎[ x -eq 0 ]]`, `(\\⏎( x ))` and `P\\⏎S4=…` each passed a guard that matched\n`raw`.\n\nStrip continuations before any text match, the way the format does: in\n`@effected\u002Fshell`, match `ShellWord.joined` and `ShellAssignment.joined`, never\n`raw`. When the parser offers no joined accessor, join exactly where the\nformat joins (bash keeps `\\⏎` inside single quotes and after an escaping\nbackslash) and test every position a continuation can take in the pattern's\ninput. A port that exposes `raw` should also expose the joined text, so each\nconsumer does not have to write its own stripping and get it wrong.\n\nThe same blindness hits a guard keyed on parse structure when the program\nre-parses an argument at run time. bash's `declare -a 'x=($(cmd))'` is a\nquoted word to the parser, with no array part. But `declare` re-reads a\n`( … )` value as a compound assignment when its target is an array, and runs\n`cmd`. `printf -v`, `read` and `trap` re-read their arguments in the same way.\nA rule that looks for the parsed construct (an `opaque` array part) never sees\nthe quoted spelling of the same construct. For each argument the program\nre-reads, refuse any value that is not literal or that could take the\nre-read form after quote removal.\n\n## The invariant that ties it together\n\n> Malformed input fails through the documented typed error channel\n> (`Effect\u003C_, DomainError>`), NEVER as an unhandled defect.\n\nAny `throw` inside the engine that the facade does not catch-and-materialize\nis a defect leak. The cycle-firewall design already keeps the engine\nfacade-free (it emits raw records; the facade builds the typed error), so the\nengine's job is to *return* an error record or error token, not throw. Where a\nthrow is unavoidable on a hot path (e.g. a circular-reference guard), throw a\ndedicated internal error class and have the facade `Effect.try({ catch })` it\ninto the domain error.\n\nTest it the right way (v4):\n\n```ts\nconst error = yield* Effect.flip(Yaml.parse(hostileInput));\nassert.strictEqual(error._tag, \"YamlParseError\");        \u002F\u002F typed, not a defect\n\u002F\u002F or, to prove it is not a defect at all:\nconst r = yield* Effect.result(Yaml.parse(hostileInput)); \u002F\u002F never throws\nassert.strictEqual(r._tag, \"Failure\");\n```\n\nA `try\u002Fcatch` around `Effect.runSync` that catches a `RangeError` is the smell\nthis skill exists to eliminate — that is a defect that reached the caller.\n\n### Scope: this governs INPUT, not caller-supplied callbacks\n\nThe invariant is about **data crossing the boundary**. It does **not** extend to\ncallbacks the caller hands you that already declare an `Effect` error channel —\na migration step's `up: (raw) => Effect\u003Cunknown, E>`, a `validate` hook, a\n`VersionAccess.get`. Those are *producers* with a typed channel of their own.\n\n**A `throw` from one of them is a contract violation — a programmer bug, not a\ndata condition — and must stay a defect.** Do not `try\u002Fcatch` a user callback\ninto a typed error. If you do, the consumer's `catchTag(\"MigrationError\")`\nsilently swallows their own null-deref and carries on with a half-migrated\nconfig. Effect's defect\u002Ffailure split exists precisely to keep those apart, and\nthe library cannot tell a typo from a data condition — which is why the *author*\ndeclares intent by calling `Effect.fail`.\n\nThe distinction that decides it: **does the callback's result participate in the\noperation's result?**\n\n| callback | result used? | a `throw` should be |\n| --- | --- | --- |\n| `options.validate` — its return value is handed back to the caller | yes | a **defect** |\n| a migration's `up` — its output becomes the config | yes | a **defect** |\n| an `emit` \u002F observability hook — result discarded (`Effect\u003Cvoid>`) | no | **absorbed** (`Effect.catchDefect`), and logged |\n\n`Effect.try` \u002F `Effect.tryPromise` around a *host* function stays correct —\n`JSON.parse`, `crypto.subtle.decrypt`, `fs.readFileString`. Those have no channel\nbut a throw or a rejection. That is not analogous to a callback that has one and\ndeclined to use it.\n\nOne more trap: guarding only the *synchronous construction* of a callback's\neffect catches the least idiomatic way to write it. `Effect.suspend(run)`\nnormalizes a construction-time throw into the same defect channel as a throw\ninside `Effect.sync` or `Effect.gen`, so all shapes behave identically. Anything\nnarrower routes the same bug to the typed channel or the defect channel\ndepending on an invisible detail of how the caller built their effect.\n\n## How the reviewer checks\n\nIt re-derives each claim against source: **every** recursion surface enumerated\nand then either capped (paired `enter`\u002F`exit` in `try\u002Ffinally`) or provably\nnon-recursive; the `0x10FFFF` bound before `fromCodePoint` *in formats with wide\nescapes*; the `defineProperty` route for `__proto__`; the C0 scan. Ship the guard\nAND its hostile-input test in the same commit, or the claim reads as unverified.\n\nTwo ways this check goes wrong in both directions. Guarding only the surfaces the\ntwo-stage frame predicts leaves the rest of the facade — `toValue`, `equals`,\n`visit`, `modify` — overflowing on the same input. Demanding a guard on a surface\nwhose format or control flow cannot reach it manufactures a finding. Enumerate,\nthen judge each surface on its own.\n","# Hardening a parser port\n\nA pure parser takes untrusted strings in and must produce values or *typed\ndomain errors* — never a crash, never an unhandled Effect defect. These are\nthe recurring hardening items the cloud reviewer re-derives against source on\nevery `@effected` parser migration (jsonc, yaml, and the boundary-tier ports\nto come). Write each guard together with its hostile-input regression test; a\nhardening claim without a test is unverified.\n\n## No unbounded recursion — enumerate every recursion surface, then close each\n\nAny recursive descent over untrusted input needs a depth cap, or deeply-nested\ninput throws `RangeError: Maximum call stack size exceeded` as a defect that\nescapes the typed channel.\n\n**Enumerate the surfaces before you guard any of them.** A *recursion surface* is\nany function that re-enters itself over user-controlled structure. A package's\npublic facade with N recursive helpers has **N surfaces, not two** — and they do\nnot share a cap just because they share a tree. Grep for self-recursion across\n`src\u002F`, list what you find, and close every entry on the list. The two-stage\nCST\u002Fcomposer pipeline below is **one instance of this rule, not the frame**.\n\n`@effected\u002Fjsonc` is the worked example: an *iterative* scanner and a\nsingle-stage recursive-descent parser, yet **six** independent surfaces, each of\nwhich overflowed the stack on `\"[\".repeat(20000) + \"]\".repeat(20000)` before it\nwas closed:\n\n| # | surface | closed by |\n| --- | --- | --- |\n| 1 | `internal\u002Fparser.ts` value mode (`parseValue`→`parseArray`\u002F`parseObject`) | cap |\n| 2 | `internal\u002Fparser.ts` tree mode (`parseValueTree`→`…Tree`) | cap |\n| 3 | `JsoncNode.evaluateNode` (backs `toValue`) | cap |\n| 4 | `JsoncVisitor.visitGen` | cap |\n| 5 | `Jsonc.deepEqual` | cap |\n| 6 | `internal\u002Fnavigate.skipValue` | **rewritten iteratively** |\n\nTwo remedies, and the second is often the better one:\n\n- **Cap it** at `MAX_NESTING_DEPTH`; on exhaustion emit a single fatal diagnostic\n  and return a leaf placeholder instead of recursing. Pair `enter`\u002F`exit` in\n  `try\u002Ffinally` at every collection entry.\n- **Remove the recursion.** `navigate.skipValue` skips a value by counting\n  bracket depth over the flat token stream (`navigate.ts:155`). Being\n  non-recursive it *cannot* overflow, so `navigate` and `JsoncModifier` need no\n  cap — and no test can regress one they don't have. Prefer this wherever the\n  walk is a skip or a scan rather than a transform.\n\nThen, whichever remedy:\n\n- **Measure the real overflow point and set the cap with wide margin.** Do not\n  guess — a multi-frame recursion chain overflows far shallower than \"levels of\n  nesting\" suggests (yaml overflowed at ~900 composer levels because each level\n  is several stack frames; the cap is 256).\n- Emit ONE fatal diagnostic (`e.code === \"NestingDepthExceeded\"` deduped), not\n  one per level.\n- **Paired codec directions must count depth in the same units — container\n  descent, never leaves.** toml's parse guarded at the opening bracket only\n  (scalar leaves unchecked) while its stringify guarded at the top of every\n  node, leaves included: a document at exactly the cap with a **non-empty**\n  innermost element parsed but failed to re-emit — a typed failure both ways,\n  so nothing defected, but the round-trip broke at the exact boundary and the\n  \"same cap on both sides\" claim in the docs was quietly false. Neither a\n  679-case compliance corpus nor a differential oracle caught it, because the\n  only fixture at the bound had an *empty* innermost container (no leaf to\n  reach the phantom extra level). The fix and the regression discipline: hoist\n  guards into the container branches on both directions, and pin a round-trip\n  test at exactly the cap with a non-empty innermost value\n  (effected PR #31 review; fix `ecc5f1a4`).\n\n### The two-stage pipeline is a special case\n\nIn a two-stage `lex → build-tree → walk-tree` pipeline (yaml's CST parser then\ncomposer), the two surfaces are *coupled*, so their caps are chosen together:\n\n- Cap the **tree-walker** (composer) at `MAX_NESTING_DEPTH` (yaml: 256).\n- Cap the **tree-builder** (CST parser) slightly ABOVE it (yaml: 264), so the\n  walker's user-facing diagnostic fires first when the capped tree is walked; the\n  builder's guard is the backstop that keeps tree *construction* from overflowing.\n\nWhere a post-hoc walker instead runs over an **already-bounded** tree (jsonc's\n`evaluateNode`, `deepEqual`), **equal** caps are correct — the builder's output can\nnever exceed the walker's cap, so the walker's guard fires only on a hand-built\ntree. Do not mis-flag equal caps as a defect in that shape, and do not demand a\n+8 offset where the two surfaces are independent rather than chained.\n\n## Algorithmic complexity is its own hardening class — the DoS vector can be time, not stack\n\nDepth caps close the stack-overflow vector. Some grammars have a second,\nindependent vector: **superlinear parse time on small, flat input** — no\nrecursion, no nesting, nothing for a depth guard to see. Markdown is the\ncanonical case: nested strong emphasis (`*a **a **a …`), thousands of\nunmatched emphasis openers or link brackets, and repeated `[ (](` patterns\ndrive a naive matcher quadratic (or worse) while staying a few bytes wide and\none level deep. yaml's alias-expansion bomb — bounded by a materialized-node\nbudget (`AliasCountExceeded`), not a depth cap — is the same lesson from the\nmemory side: **amplification vectors are orthogonal to nesting depth.**\n\nTwo consequences for a port:\n\n- **The guard is the algorithm, not a limit constant** — with one recorded\n  carve-out. CommonMark's delimiter-stack emphasis algorithm and its\n  \"openers and closers multiple of 3\" rule exist precisely to make\n  pathological delimiter runs linear; an engine that \"simplifies\" them\n  re-opens the vector. Port the algorithm faithfully — there is no bolt-on\n  cap that fixes a quadratic matcher. **The carve-out: when the SPEC itself\n  sanctions a limit, the limit IS the algorithm's fix.** The bare\n  link-destination scan is quadratic on unclosed-link input in the\n  reference implementation too, and cmark's fix is `MAX_LINK_DEST_PARENS`\n  (32) under the spec's explicit \"implementations may impose limits on\n  parentheses nesting\" license — adopted verbatim in `@effected\u002Fmarkdown`\n  after the vendored reference was confirmed to share the flaw\n  (2026-08-02: 30k unclosed links, 5.2s → 30ms). The discriminator: a\n  spec-sanctioned cap bounds a *scan the grammar cannot otherwise\n  terminate*; a bolt-on cap papers over a matcher whose fix is a better\n  algorithm. Check the reference's own limit constants before assuming the\n  rule forbids constants.\n- **The regression instrument is a vendored pathological corpus with\n  timeout assertions.** cmark's `pathological_tests.py` is the pattern:\n  ~25 named cases (`\"*a **a\" * 65000`, 65k mismatched openers\u002Fclosers, deep\n  bracket nests, reference collisions), each asserting output *shape* via\n  regex under a wall-clock timeout — no HTML fixture needed. Vendor it and\n  run it as its own mandatory suite. Two operational rules from running one\n  at scale: prefer **ratio-based scaling guards** (time N vs 2N vs 4N;\n  instrumentation and hardware cancel) over absolute budgets where the\n  claim is complexity, and if the suite CALIBRATES its budgets against a\n  measured input, **recalibrate after any engine perf fix** — a calibration\n  input that rode the removed hot path now measures ~nothing, silently\n  un-scaling every other budget (this exact failure fired 2026-08-02).\n\n**A green conformance corpus says nothing about complexity.** Every\nconformance example is small and well-formed; an engine can pass all of them\nand still hang on twenty repeated bytes. For any grammar whose spec (or whose\nreference implementation's test tree) ships pathological cases, treat that\nsuite as a second, non-optional corpus — and where none exists, write the\nhostile generator yourself (`\"[\".repeat(65000)`-style) before declaring the\nengine hardened.\n\n## Numeric bound guards — `if (n \u003C LIMIT)` silently admits `NaN` and non-integers\n\nThe guard on a **numeric option** (a `maxDepth`, a size cap, an iteration bound)\nhas an obvious spelling that is wrong in a way invisible to review and to a\ngreen suite. `NaN \u003C 1` is `false`, and so is `2.5 \u003C 1` — every relational\ncomparison against `NaN` is `false`. So:\n\n```ts\nif (maxDepth \u003C 1) return yield* Effect.die(...);   \u002F\u002F NaN and 2.5 sail past\nfor (let depth = 0; depth \u003C maxDepth; depth++) { ... }  \u002F\u002F 0 \u003C NaN is ALSO false\n```\n\n`maxDepth: NaN` skips the guard **and** runs the loop zero times, returning an\nempty result — indistinguishable from a legitimate empty result, which is the\nexact silent outcome the guard was written to forbid. A fractional bound\ntruncates at a non-integer instead of dying. These are the two inputs a caller\nmost plausibly passes by accident: an unparsed `Number(env.MAX_DEPTH)`, or a\ncomputed average.\n\nGuard integrality and range together, and prove it:\n\n```ts\nif (!Number.isInteger(maxDepth) || maxDepth \u003C 1) {\n  return yield* Effect.die(new Error(`maxDepth must be a positive integer, received ${maxDepth}`));\n}\n```\n\nShip a test for `NaN` and one for a fractional value, each **watched failing\nagainst the bare `\u003C LIMIT` comparison** — a guard whose failure mode is\n\"returns empty\" is untestable by inspection. And when you document the guard,\nstate the full predicate: prose that says \"`maxDepth \u003C 1` is a defect\" invites\na reader to conclude `2.5` is accepted (this exact partial documentation\nshipped alongside the partial guard in `@effected\u002Fwalker` before both were\nfixed).\n\n## Range-check `String.fromCodePoint` fed by parsed hex — in formats that have wide escapes\n\n**Scope this check to the format before you go looking.** It applies only where an\nescape can denote a code point above the Unicode maximum `U+10FFFF`:\n\n| format | widest escape | max value | check needed? |\n| --- | --- | --- | --- |\n| YAML | `\\U00110000` (8 hex) | unbounded | **yes** |\n| JSON \u002F JSONC | `￿` (4 hex) | `0xFFFF` | **no** — structurally impossible |\n\nA JSON-family scanner using `String.fromCharCode` on a 4-hex value is *already*\ncorrect and needs no guard; `@effected\u002Fjsonc` is exactly this case. Do not go\nhunting for a `fromCodePoint` range hazard in a format that cannot express one,\nand do not flag its absence as a finding.\n\nWhere the format *does* have wide escapes, `String.fromCodePoint` throws a\n`RangeError` that escapes as a defect. Validate before the call:\n\n```ts\nconst cp = \u002F* parse hex *\u002F;\nif (cp \u003C= 0x10ffff) value += String.fromCodePoint(cp);\nelse return makeErrorToken(\u002F* … *\u002F); \u002F\u002F typed error, not a throw\n```\n\nGuard **both** the lexer (which builds the scalar value inline and throws first,\nmid-scan) and any composer re-decode of the same source.\n\n## `__proto__` and special keys → own data properties\n\nWhen building an object from parsed key\u002Fvalue pairs, a bare `obj[key] = value`\nmutates the prototype for `key === \"__proto__\"`. Route keys through\n`Object.defineProperty`, matching `JSON.parse` semantics:\n\n```ts\nif (key === \"__proto__\") {\n Object.defineProperty(obj, key, { value, writable: true, enumerable: true, configurable: true });\n} else {\n obj[key] = value;\n}\n```\n\nDo this at the single value-extraction choke point (`Node.toValue`), so both\nthe value path and any tree-walker share it.\n\nThe guidance above is for objects **you** build. When untrusted records flow\nthrough v4 `Schema.Record` instead, decode is already pollution-safe: a\n`__proto__` key survives decoding as an ordinary **own data property** of the\noutput — it is neither dropped nor written to the prototype (probed in\nthe `@effected\u002Flockfiles` hostility suite: a `__proto__` pnpm importer decodes\ninto an entry literally named `__proto__` with `Object.prototype` unpolluted).\nTwo consequences: don't pre-filter such keys expecting decode to choke on\nthem, and if you later copy a decoded record by hand, that copy loop is a new\n`obj[key] = value` surface needing the `defineProperty` route above.\n\n**The read side is a separate hazard, and it needs no hostile key at all.**\n`Schema.Record` decodes into a plain `{}` inheriting `Object.prototype`, so an\nunguarded `obj[key]` read with an untrusted or caller-controlled `key` resolves\n*inherited* members: `tags[\"constructor\"]` returns a function, `\"toString\"`,\n`\"hasOwnProperty\"` likewise — handed onward typed as your value type. Read\nuntrusted maps only through an ownership check:\n\n```ts\nconst value = Object.hasOwn(map, key) ? map[key] : undefined;\n```\n\nApply it at **every** indexed read of a decoded record, and sweep for the one\ncall site that skipped it — the ts-vfs port guarded its whole resolution\nmachinery and still shipped one unguarded read (`resolveVersion`'s dist-tag\nlookup), where `resolveVersion(name, \"constructor\")` genuinely returned an\ninherited function as a \"version\" until the PR #67 review caught it. Keys\nenumerated by `Object.keys`\u002F`Object.entries` are own-only and safe; it is the\n*lookup by external key* that must be guarded. Pin it with a hostile-ref test\n(`\"constructor\"`, `\"__proto__\"`, `\"toString\"`).\n\n## Test membership with `Object.hasOwn`, never bracket notation\n\nThe write path is only half of it. **Reading** a key off a parsed record with\nbracket notation inherits from `Object.prototype`:\n\n```ts\n\u002F\u002F UNSOUND — returns Option.some(\u003CFunction>) for a manifest with no deps at all.\nconst dep = deps[\"constructor\"];        \u002F\u002F → a Function, off Object.prototype\nif (dep !== undefined) return Option.some(dep);\n\n\u002F\u002F SOUND:\nif (Object.hasOwn(deps, name)) return Option.some(deps[name]);\n```\n\nVerified: `({})[\"constructor\"]` is a `Function`, while\n`Object.hasOwn({}, \"constructor\")` is `false`. A method declared to return\n`Option\u003Cstring>` therefore handed back an `Option.some(\u003CFunction>)` — **unsound,\nnot merely untidy**, and it type-checks perfectly because the index signature\npromises a `string`.\n\nEvery key an attacker names (`constructor`, `toString`, `valueOf`, `hasOwnProperty`)\nis a live hit. The tell in the real case: **every sibling predicate already used\n`Object.hasOwn`** — an inconsistency across a family of guards is a defect, not a\nstyle preference. Grep the family, not the line.\n\n## Reject unescaped C0 control characters\n\nRaw control characters below `0x20` (except tab `0x09`, LF `0x0a`, CR `0x0d`)\nare not printable per most text-format specs (YAML 1.2 §5.1, JSON §7). Scan the\ndocument's raw span once and emit a fatal diagnostic; escaped forms in\nquoted scalars never appear raw in the source, so scanning source text is\nsufficient.\n\n## `JSON.parse` returns `null`, a number, or a string — for VALID JSON\n\nA `=== undefined` guard after `JSON.parse` **never fires**. The four characters\n`null` are a perfectly valid `package.json`, and they parse to `null`, not\n`undefined`:\n\n| input | `JSON.parse` → | caught by `=== undefined`? |\n| --- | --- | --- |\n| `null` | `null` (`typeof` `\"object\"`!) | **no** |\n| `42` | `42` | **no** |\n| `\"hi\"` | `\"hi\"` | **no** |\n| `true` | `true` | **no** |\n\n```ts\n\u002F\u002F WRONG — `parsed.name` throws a TypeError on `null`, escaping as a DEFECT.\nconst parsed = JSON.parse(text);\nif (parsed === undefined) return yield* Effect.fail(new InvalidJson({ path }));\n\n\u002F\u002F RIGHT — reject anything that is not a non-null object.\nif (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return yield* Effect.fail(new InvalidShape({ path }));\n}\n```\n\nThis was live in **three** places in one package, each throwing a `TypeError` on\na property read — malformed input escaping as an **unhandled defect**, which is\nprecisely the invariant below. `typeof null === \"object\"` is what makes the\nplausible guard (`typeof parsed !== \"object\"`) insufficient on its own.\n\n## Path bases: `git diff --name-only` is REPO-relative, `git ls-files` is CWD-relative\n\nProbed in a scratch repo, both commands run from the subdirectory `pkgs\u002Falpha`,\nwith the **same file** modified:\n\n| command | printed |\n| --- | --- |\n| `git ls-files` | `nested.txt` — relative to the **cwd** |\n| `git diff --name-only` | `pkgs\u002Falpha\u002Fnested.txt` — relative to the **repo top-level** |\n\nOne file, two spellings. Put both in a `Set` and it holds two entries; compare\nthem and nothing matches.\n\n**Normalize each result from its OWN base — never from a single assumed one.**\n\n```ts\n\u002F\u002F WRONG — resolving ls-files against the repo root yields \u003Croot>\u002Fnested.txt,\n\u002F\u002F a path that does not exist. This IS the bug.\nresolve(repoRoot, p);\n\n\u002F\u002F RIGHT — each command's output resolved against the base it actually used:\nconst fromLsFiles = resolve(cwdTheCommandRanIn, p);   \u002F\u002F cwd-relative\nconst fromDiff    = resolve(repoTopLevel, p);         \u002F\u002F repo-relative\n```\n\n`git rev-parse --show-toplevel` gives the repo base; `--show-prefix` gives the\ncwd's offset from it. Simplest of all, make `ls-files` agree with `diff` at the\nsource: **`git ls-files --full-name`** prints repo-relative paths (verified — it\nreturned `pkgs\u002Falpha\u002Fnested.txt`).\n\nTwo further asymmetries the probe exposed, both easy to miss: `ls-files` is also\n**scoped** to the cwd (it listed only files under `pkgs\u002Falpha`), while\n`diff --name-only` reports the **whole repo** — so the two commands do not even\ncover the same set of files.\n\nThe generalizable rule, which is the reusable part:\n\n> **When combining the output of two commands, never assume they share a path\n> base. Verify each one's base independently.** Mixing bases is *silently\n> correct* whenever the two happen to coincide — which is the common case, and\n> the one your tests will cover.\n\nThat last clause is the testing consequence: **a fixture where the workspace root\nand the git root coincide cannot distinguish the correct implementation from the\nbroken one.** Any test for path-base handling must place the workspace root\n*below* the git root, or it proves nothing.\n\n## Structurally indistinguishable parses: position is the only sound discriminator\n\npnpm 11 writes a **two-document** `pnpm-lock.yaml` under `configDependencies`.\nNo *structural* rule can pick the right document: both carry `lockfileVersion`,\n`importers` and `packages`, and the preamble validates cleanly against the same\nschema. A \"pick the document that looks like a lockfile\" heuristic selects the\nwrong one and reports success.\n\n**POSITION is the only sound discriminator** — the real lockfile is the last (or\nthe nth) document, by the format's own framing rule. Which is exactly why the bug\nwas silent: a structural heuristic *did* find a valid-looking answer.\n\nGeneralize it:\n\n> When two candidate parses are structurally indistinguishable, a structural\n> heuristic is not a rule — it is a coin flip that happens to land right on your\n> fixtures. Discriminate on the framing (position, document index, an explicit\n> marker), and make the choice explicit in the code.\n\n## A port that builds a `RegExp` from input: force its compile inside the typed catch\n\n`new RegExp(source)` succeeding proves only that the syntax parsed. V8 builds\nthe code on the first `exec`, and a source too large for it throws a\n`SyntaxError` (\"Regular expression too large\") there. It builds that code\nseparately for a one-byte (Latin-1) subject and a two-byte one, each on its\nfirst `exec` of that kind, and a source of non-Latin-1 literals compiles\ntrivially for a one-byte subject, which can never match it. If the first\n`exec` of either kind is a match, the throw escapes a total `matches` as a\ndefect, outside every typed catch. `@effected\u002Fglob` met both: about 32,000\nliteral characters and one wildcard threw on `matches(\"x\")`, and\n`\"ā\".repeat(33_000) + \"*\"` survived a one-byte warm-up and threw on\n`matches(\"ā…\")`.\n\nSo run the regexp once on each kind of subject where it is built, inside the\ncatch that turns a `SyntaxError` into the typed failure:\n\n```ts\nconst compileOrFail = (source: string, flags: string): RegExp => {\n  try {\n    const regexp = new RegExp(source, flags)\n    regexp.test(\"\") \u002F\u002F forces V8's lazy one-byte compile\n    regexp.test(\"\\u0100\") \u002F\u002F and its two-byte one, so a match is never the first exec of either\n    return regexp\n  } catch (e) {\n    if (e instanceof SyntaxError) throw new GuardExceeded(\"InvalidRegExp\", 0, 0)\n    throw e\n  }\n}\n```\n\nOnly flags without `g` or `y` make that `test` stateless. A spy that counts\n`RegExp` constructions during matching cannot see this defect, because no\nconstruction happens there: pin it with a row whose source is too large, and\nassert that compiling fails typed and that `matches` never throws.\n\n## A string replacement reads `$` patterns: substitute with `split`\u002F`join`\n\n`String.prototype.replace` and `replaceAll` with a STRING replacement read\n`$&`, `$$`, `` $` ``, `$'` and `$\u003Cname>` as patterns, so substituting\nuntrusted text with `template.replaceAll(\"*\", match)` turns a match of\n`a$&b` into `a*b` and `` a$`b `` into the text before the star. Wherever the\nreplacement is data (a path segment, a file name, a pattern match), substitute\nliterally:\n\n```ts\nconst substituteEveryStar = (template: string, match: string): string => template.split(\"*\").join(match)\nconst substituteFirstStar = (template: string, match: string): string => {\n  const at = template.indexOf(\"*\")\n  return at === -1 ? template : template.slice(0, at) + match + template.slice(at + 1)\n}\n```\n\nA replacer function (`replace(re, () => match)`) is also literal. The one\nexception is deliberate parity with a reference that itself uses a string\nreplacement: `@effected\u002Ftsconfig-json` keeps `replace(\u002F\\*\u002Fg, subpath)`\nbecause tsc does, and pins it with a test.\n\n## A consumer-side regex over `raw` spans must strip continuations first\n\nA parser that keeps each token's `raw` source slice keeps whatever the format\nremoves before it tokenizes. Shell is the sharp case: bash deletes every\nbackslash-newline (`\\⏎`) before it reads a token, and so does\n`@effected\u002Fshell`, which resolves `[\\⏎[` to `[[`. But `raw` keeps the `\\⏎` as\nwritten. A guard that runs a pattern over `raw` (`\u002F\\(\\(\u002F`, `\u002F^\\[\\[$\u002F`,\n`\u002FPS4=\u002F`) misses the split spelling, and bash runs the joined one:\n`[\\⏎[ x -eq 0 ]]`, `(\\⏎( x ))` and `P\\⏎S4=…` each passed a guard that matched\n`raw`.\n\nStrip continuations before any text match, the way the format does: in\n`@effected\u002Fshell`, match `ShellWord.joined` and `ShellAssignment.joined`, never\n`raw`. When the parser offers no joined accessor, join exactly where the\nformat joins (bash keeps `\\⏎` inside single quotes and after an escaping\nbackslash) and test every position a continuation can take in the pattern's\ninput. A port that exposes `raw` should also expose the joined text, so each\nconsumer does not have to write its own stripping and get it wrong.\n\nThe same blindness hits a guard keyed on parse structure when the program\nre-parses an argument at run time. bash's `declare -a 'x=($(cmd))'` is a\nquoted word to the parser, with no array part. But `declare` re-reads a\n`( … )` value as a compound assignment when its target is an array, and runs\n`cmd`. `printf -v`, `read` and `trap` re-read their arguments in the same way.\nA rule that looks for the parsed construct (an `opaque` array part) never sees\nthe quoted spelling of the same construct. For each argument the program\nre-reads, refuse any value that is not literal or that could take the\nre-read form after quote removal.\n\n## The invariant that ties it together\n\n> Malformed input fails through the documented typed error channel\n> (`Effect\u003C_, DomainError>`), NEVER as an unhandled defect.\n\nAny `throw` inside the engine that the facade does not catch-and-materialize\nis a defect leak. The cycle-firewall design already keeps the engine\nfacade-free (it emits raw records; the facade builds the typed error), so the\nengine's job is to *return* an error record or error token, not throw. Where a\nthrow is unavoidable on a hot path (e.g. a circular-reference guard), throw a\ndedicated internal error class and have the facade `Effect.try({ catch })` it\ninto the domain error.\n\nTest it the right way (v4):\n\n```ts\nconst error = yield* Effect.flip(Yaml.parse(hostileInput));\nassert.strictEqual(error._tag, \"YamlParseError\");        \u002F\u002F typed, not a defect\n\u002F\u002F or, to prove it is not a defect at all:\nconst r = yield* Effect.result(Yaml.parse(hostileInput)); \u002F\u002F never throws\nassert.strictEqual(r._tag, \"Failure\");\n```\n\nA `try\u002Fcatch` around `Effect.runSync` that catches a `RangeError` is the smell\nthis skill exists to eliminate — that is a defect that reached the caller.\n\n### Scope: this governs INPUT, not caller-supplied callbacks\n\nThe invariant is about **data crossing the boundary**. It does **not** extend to\ncallbacks the caller hands you that already declare an `Effect` error channel —\na migration step's `up: (raw) => Effect\u003Cunknown, E>`, a `validate` hook, a\n`VersionAccess.get`. Those are *producers* with a typed channel of their own.\n\n**A `throw` from one of them is a contract violation — a programmer bug, not a\ndata condition — and must stay a defect.** Do not `try\u002Fcatch` a user callback\ninto a typed error. If you do, the consumer's `catchTag(\"MigrationError\")`\nsilently swallows their own null-deref and carries on with a half-migrated\nconfig. Effect's defect\u002Ffailure split exists precisely to keep those apart, and\nthe library cannot tell a typo from a data condition — which is why the *author*\ndeclares intent by calling `Effect.fail`.\n\nThe distinction that decides it: **does the callback's result participate in the\noperation's result?**\n\n| callback | result used? | a `throw` should be |\n| --- | --- | --- |\n| `options.validate` — its return value is handed back to the caller | yes | a **defect** |\n| a migration's `up` — its output becomes the config | yes | a **defect** |\n| an `emit` \u002F observability hook — result discarded (`Effect\u003Cvoid>`) | no | **absorbed** (`Effect.catchDefect`), and logged |\n\n`Effect.try` \u002F `Effect.tryPromise` around a *host* function stays correct —\n`JSON.parse`, `crypto.subtle.decrypt`, `fs.readFileString`. Those have no channel\nbut a throw or a rejection. That is not analogous to a callback that has one and\ndeclined to use it.\n\nOne more trap: guarding only the *synchronous construction* of a callback's\neffect catches the least idiomatic way to write it. `Effect.suspend(run)`\nnormalizes a construction-time throw into the same defect channel as a throw\ninside `Effect.sync` or `Effect.gen`, so all shapes behave identically. Anything\nnarrower routes the same bug to the typed channel or the defect channel\ndepending on an invisible detail of how the caller built their effect.\n\n## How the reviewer checks\n\nIt re-derives each claim against source: **every** recursion surface enumerated\nand then either capped (paired `enter`\u002F`exit` in `try\u002Ffinally`) or provably\nnon-recursive; the `0x10FFFF` bound before `fromCodePoint` *in formats with wide\nescapes*; the `defineProperty` route for `__proto__`; the C0 scan. Ship the guard\nAND its hostile-input test in the same commit, or the claim reads as unverified.\n\nTwo ways this check goes wrong in both directions. Guarding only the surfaces the\ntwo-stage frame predicts leaves the rest of the facade — `toValue`, `equals`,\n`visit`, `modify` — overflowing on the same input. Demanding a guard on a surface\nwhose format or control flow cannot reach it manufactures a finding. Enumerate,\nthen judge each surface on its own.\n",{"name":5,"description":7},"main","11d6b3ae59538b0e16ad67c315ecb0c25fcb0125","plugin\u002Fbuilds\u002Fclaude\u002Fskills\u002Fhardening-a-parser-port",2,"https:\u002F\u002Fgithub.com\u002Fspencerbeggs\u002Feffected","https:\u002F\u002Fgithub.com\u002Fspencerbeggs","2026-11-14 07:03:25",{"source":8,"skillName":5,"commands":32,"prompt":42,"zipAvailable":43},[33,36,39],{"tool":34,"command":35},"npx","npx skills add spencerbeggs\u002Feffected --skill hardening-a-parser-port",{"tool":37,"command":38},"bunx","bunx skills add spencerbeggs\u002Feffected --skill hardening-a-parser-port",{"tool":40,"command":41},"pnpm","pnpm dlx skills add spencerbeggs\u002Feffected --skill hardening-a-parser-port","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fspencerbeggs\u002Feffected\u002Fblob\u002Fmain\u002Fplugin\u002Fbuilds\u002Fclaude\u002Fskills\u002Fhardening-a-parser-port\u002FSKILL.md",true,[],[46,53,60,67,74,81],{"skillNo":47,"name":48,"nameZh":6,"description":49,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":50,"categories":51,"keywords":52,"publishedAt":19},"01M780NVTD800KMFV869YNZED6","testing-actions","Use when writing or reviewing tests for @effected\u002Fgithub-actions, @effected\u002Fgithub, @effected\u002Fsbom or @effected\u002Fcommands — stubbing the GitHub API, writing a service's layerTest or makeTest double, testing the pre\u002Fmain\u002Fpost lifecycle through Action.run, or debugging why a test hits the real network or hangs.","https:\u002F\u002Fgithub.com\u002Fspencerbeggs\u002Feffected\u002Fblob\u002Fmain\u002Fplugin\u002Fskills\u002Ftesting-actions\u002FSKILL.md",[],[],{"skillNo":54,"name":55,"nameZh":6,"description":56,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":57,"categories":58,"keywords":59,"publishedAt":19},"01M780NPDSGFC0FCP7696NW11J","supply-chain-attestation","Use when generating a CycloneDX SBOM, checking NTIA minimum elements, building an in-toto statement or a SLSA provenance predicate, signing into a Sigstore DSSE bundle, or uploading an attestation to GitHub.","https:\u002F\u002Fgithub.com\u002Fspencerbeggs\u002Feffected\u002Fblob\u002Fmain\u002Fplugin\u002Fskills\u002Fsupply-chain-attestation\u002FSKILL.md",[],[],{"skillNo":61,"name":62,"nameZh":6,"description":63,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":64,"categories":65,"keywords":66,"publishedAt":19},"01M780NGP4QNZDH8T8Q8WZ18SA","structuring-an-action","Use when laying out a new GitHub Action repository on the @effected suite, deciding where a piece of code belongs (an entry point, a step, a shared service, a shim), or reviewing an existing action's file structure against the canonical shape.","https:\u002F\u002Fgithub.com\u002Fspencerbeggs\u002Feffected\u002Fblob\u002Fmain\u002Fplugin\u002Fskills\u002Fstructuring-an-action\u002FSKILL.md",[],[],{"skillNo":68,"name":69,"nameZh":6,"description":70,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":71,"categories":72,"keywords":73,"publishedAt":19},"01M780NCD52H1Y03FS83WJ8BT5","running-commands-and-tools","Use when running a subprocess in Effect v4, spawning a command, capturing stdout\u002Fstderr\u002Fexit code, checking whether a CLI tool is installed or which copy to run, running a package-manager script through the project's launcher, detaching a background process, or redacting a secret from argv or captured output.","https:\u002F\u002Fgithub.com\u002Fspencerbeggs\u002Feffected\u002Fblob\u002Fmain\u002Fplugin\u002Fskills\u002Frunning-commands-and-tools\u002FSKILL.md",[],[],{"skillNo":75,"name":76,"nameZh":6,"description":77,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":78,"categories":79,"keywords":80,"publishedAt":19},"01M780N823DWNRMSY5X2NT5MSR","release-and-publish","Use when publishing a package to npm, reading the npm registry from Effect v4, checking whether a version is already published, cutting a release tag or GitHub release from an action, applying a release-age gate, or deriving tracking tags for GitHub Actions distribution.","https:\u002F\u002Fgithub.com\u002Fspencerbeggs\u002Feffected\u002Fblob\u002Fmain\u002Fplugin\u002Fskills\u002Frelease-and-publish\u002FSKILL.md",[],[],{"skillNo":82,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":83,"categories":84,"keywords":85,"publishedAt":19},"01M780MXRF6H20KCESTSSFB3HQ","https:\u002F\u002Fgithub.com\u002Fspencerbeggs\u002Feffected\u002Fblob\u002Fmain\u002Fplugin\u002Fskills\u002Fhardening-a-parser-port\u002FSKILL.md",[],[]]