From d192c52135cd5eec69f374fd882750494c402fc5 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 25 Jul 2026 05:29:23 -0700 Subject: [PATCH] refactor(memory-safety): add a parse wrapper, scan mobile/app, pin the lint wiring - parseJsonWithinStructureLimits guards then parses in one call, so a bare JSON.parse genuinely means unguarded instead of every sanctioned call site needing a justification comment. - scan mobile/app (Expo Router screens) alongside mobile/src. - test that package.json still wires check:bounded-ingress into lint: a gate that can be unwired without a test failing will be unwired by a rebase. Co-authored-by: Orca --- config/bounded-ingress-baseline.json | 2 ++ config/scripts/check-bounded-ingress.mjs | 5 +++-- config/scripts/check-bounded-ingress.test.mjs | 11 +++++++++++ src/shared/memory-safety/json-text-structure-limit.ts | 11 +++++++++++ 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/config/bounded-ingress-baseline.json b/config/bounded-ingress-baseline.json index b0518bb025d..4e699cdf1f4 100644 --- a/config/bounded-ingress-baseline.json +++ b/config/bounded-ingress-baseline.json @@ -1,5 +1,7 @@ { "allowed": { + "mobile/app/h/[hostId]/session/[worktreeId].tsx:json-stringify": 1, + "mobile/app/index.tsx:json-parse": 1, "mobile/src/cache/home-snapshot-cache.ts:json-parse": 1, "mobile/src/cache/home-snapshot-cache.ts:json-stringify": 1, "mobile/src/components/CustomKeyModal.tsx:json-parse": 1, diff --git a/config/scripts/check-bounded-ingress.mjs b/config/scripts/check-bounded-ingress.mjs index a828684042b..77389c80bab 100644 --- a/config/scripts/check-bounded-ingress.mjs +++ b/config/scripts/check-bounded-ingress.mjs @@ -35,7 +35,7 @@ export const PATTERNS = [ { id: 'json-parse', re: /\bJSON\.parse\s*\(/, - hint: 'guard with assertJsonTextStructureWithinLimits (memory-safety/json-text-structure-limit)' + hint: 'use parseJsonWithinStructureLimits (memory-safety/json-text-structure-limit)' }, { id: 'json-stringify', @@ -128,7 +128,8 @@ async function collect(root, dir, out = []) { export async function run(root, { updateBaseline = false } = {}) { const files = [ ...(await collect(root, path.join(root, 'src'))), - ...(await collect(root, path.join(root, 'mobile/src'))) + ...(await collect(root, path.join(root, 'mobile/src'))), + ...(await collect(root, path.join(root, 'mobile/app'))) ] const violations = [] for (const file of files) { diff --git a/config/scripts/check-bounded-ingress.test.mjs b/config/scripts/check-bounded-ingress.test.mjs index f848f2c042d..4668aab72d1 100644 --- a/config/scripts/check-bounded-ingress.test.mjs +++ b/config/scripts/check-bounded-ingress.test.mjs @@ -77,6 +77,17 @@ describe('check-bounded-ingress', () => { ).toEqual([]) }) + it('stays wired into pnpm lint', () => { + // A gate that can be unwired without any test failing WILL be unwired by a rebase — this one + // already was once, and the ratchet cannot detect its own removal. + const root = path.resolve(import.meta.dirname, '../..') + const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')) + expect(pkg.scripts['check:bounded-ingress']).toBe( + 'node config/scripts/check-bounded-ingress.mjs' + ) + expect(pkg.scripts.lint).toContain('check:bounded-ingress') + }) + it('every hint names a real export of the module it points at', () => { // The hint is the text an agent copies when blocked; a stale name is an immediate import error. const root = path.resolve(import.meta.dirname, '../..') diff --git a/src/shared/memory-safety/json-text-structure-limit.ts b/src/shared/memory-safety/json-text-structure-limit.ts index e0ede23db54..3fb7789a437 100644 --- a/src/shared/memory-safety/json-text-structure-limit.ts +++ b/src/shared/memory-safety/json-text-structure-limit.ts @@ -78,3 +78,14 @@ function isStructuralToken(character: string | undefined): boolean { character === ':' ) } + +// Guard-then-parse in one call so a bare `JSON.parse` in review (and in the bounded-ingress ratchet) +// genuinely means "unguarded", instead of every sanctioned call site needing a justification comment. +export function parseJsonWithinStructureLimits( + content: string, + limits: JsonTextStructureLimits +): T { + assertJsonTextStructureWithinLimits(content, limits) + // bounded-by: structure asserted on the line above + return JSON.parse(content) as T +}