Skip to content

enhance(#4629): the Axis-2 raw STATE.md write guard parses the file instead of matching lines, so scope, strings and other write APIs stop fooling it #5279

Open

Description

@davesienkowski

GSD Version

1.16.0 (next @ ed0f3fd)

What existing feature or behavior does this improve?

Axis 2 of the STATE.md write-path guard, scripts/lint-state-write-path-drift.cjs (findRawStateWrites), the terminal check that ADR-4629 Decision 5 names as the enforcement half beside the type system. A #4629 child, requested in the #5119 round-4 review (finding m1).

Current behavior

Axis 2 matches text line by line, and its header says so on purpose (line 114: "String literals are matched, never parsed as an AST"). #5104 / #5119 taught it wrapped calls and same-function variable targets, but a line scan cannot see scope, strings or call shapes, so these stay wrong. #5119 pins each as a known-gap test row that asserts today's output and that it differs from the correct verdict:

  • Scope (false positives, no way to clear them): a sibling arrow or method that binds the same name, a nested function's parameter shadowing an outer variable, p => statePath read as an assignment. ADR-4629 Decision 5 rules out an allowlist, so a false positive here blocks lint:ci until the code is reshaped.
  • Scope (misses): an assignment and write inside a one-line block, or a reassignment in a closed block on the write's line.
  • Strings and regex literals: stripComments does not track them, so a // inside 'http://x' or /\// hides a call that starts on that line (a miss), and a string that only spells the call is reported.
  • Call shapes: only the literal fs.writeFileSync( is matched. fs.promises.writeFile, fs.writeFile, fs.appendFileSync, a destructured or aliased writeFileSync, fs?.writeFileSync and fs['writeFileSync'] are not seen.

Proposed behavior

Replace Axis 2's line scanners with a syntax-only TypeScript AST pass (ts.createSourceFile, no type checker, no program). Visit write calls by callee shape, take the target argument node, and resolve identifiers through the enclosing scope's declarations. Strings, regex literals, comments and block scope then come from the parser instead of from approximations. Axes 1 and 3-5 are out of scope and keep their line scans.

Acceptance:

  • Every row in ci(#5104): the raw STATE.md write guard sees wrapped calls and same-function variable targets #5119's KNOWN_SCOPE_GAPS, KNOWN_TAIL_GAPS (where the file is visible in the function) and KNOWN_MATCH_GAPS flips to its expected verdict, and each row is converted to an ordinary assertion.
  • Every current positive and control row in tests/state-write-path-drift-guard.test.cjs keeps its verdict.
  • The real tree still reports rawWriteViolations: 0, or each new finding is migrated onto the seam in the same PR (no allowlist, ADR-4629 Decision 5).
  • The guard header's design note is rewritten to describe the AST pass, and a parse failure fails closed (reported, never skipped).

Reason and benefit

For contributors: today a correct write that happens to sit in a sibling closure fails lint:ci with no allowlist to clear it, and a real raw STATE.md write can pass by being written through fs.promises.writeFile. Both cost a review round. For users: the guard is what keeps an unlocked STATE.md write (the corruption class #4629 exists to close) from landing, so its blind spots are where that class comes back.

Scope of changes

scripts/lint-state-write-path-drift.cjs (Axis 2 only) and tests/state-write-path-drift-guard.test.cjs. No src/ change, no new dependency: typescript is already a devDependency (package.json), and four test files already load it. Measured on next @ ed0f3fd with typescript 6.0.3: parsing the 72 files under src/, scripts/, gsd-core/bin/, hooks/ and bin/ that contain fs.writeFileSync( takes 295 ms in one process. In an earlier probe the AST pass would replace about 330 lines of scanners (splitStatements, statementSpans, lastBindingInText, nearestTargetBinding, the join-tail helpers).

Breaking changes

None for users. lint:ci results can change: false positives clear, and real writes through the newly seen call shapes get reported.

Alternatives considered

  • Keep extending the line scanner (a brace-depth tracker, a string-aware stripComments). Probed during ci(#5104): the raw STATE.md write guard sees wrapped calls and same-function variable targets #5119: braces in strings, templates and regex literals, brace-less arrow bodies, catch (e) shadowing and block-scoped var each need more parsing, so the tracker turns into a partial parser. stripComments is shared by all five axes, so changing it moves verdicts outside Axis 2.
  • An allowlist for the scope false positives. Rejected by ADR-4629 Decision 5.

Area affected

Workflow / STATE.md write path (epic #4629)

Additional context

Activity

  1. added a commit that references this issue on Oct 10, 2026
  2. davesienkowski commented on Oct 10, 2026

    @davesienkowski
    CollaboratorAuthor

    Acceptance addendum from the #5119 review (5477747649, Major 1 and Minors 2, 3, 8). #5119 head 29f3b6e17 pins these as rows, and the AST pass should flip each one to its expected verdict:

    • Scope. A whole function or arrow on one line (function w(){ const p = join(dir,'STATE.md'); fs.writeFileSync(p,c); }). True block, arrow and method scoping, which ci(#5104): the raw STATE.md write guard sees wrapped calls and same-function variable targets #5119 approximates by refusing to let a literal clear a write across {, } or =>.
    • Module-level constants. const STATE_FILENAME = 'STATE.md' at module level, used inside a function.
    • False positives (KNOWN_TARGET_FALSE_POSITIVES, new table). A same-name arrow parameter that shadows a STATE.md binding. A target derived from STATE.md's content. A STATE.md.bak literal.
    • Call shapes (added to KNOWN_MATCH_GAPS). appendFileSync, renameSync(tmp, statePath), fs.writeFile, an aliased fs, require('fs').writeFileSync, fs . writeFileSync (, fs?.writeFileSync, fs['writeFileSync'].

    BY_DESIGN_FALLBACKS (also new: a parameter or interpolated-template join tail) records intended behavior, so it is not in scope to flip.

    • Added from ci(#5104): the raw STATE.md write guard sees wrapped calls and same-function variable targets #5119 head 3a6b4fb95. Missed writes: a declaration whose value starts on the next line (typed and untyped), nested braces in a type annotation, a ; inside a string-literal type. Four new BY_DESIGN_FALLBACKS rows differ from the parameter and template rows above: they come from the line scan's textual body boundary, so true scoping should resolve them. A non-STATE const tail separated from the write by an unrelated closed block, an object literal, a callback or a ${} interpolation reports today; the AST pass should clear it and flip those rows.

    Generated by AI during review response; reviewed and posted by a human maintainer (@davesienkowski).

  3. trek-e commented on Oct 14, 2026

    @trek-e
    Collaborator

    This was generated by AI during triage.

    Not triaged to a decision yet: this request needs to stand on its own.

    Its acceptance criteria are written against the known-gap tables (KNOWN_SCOPE_GAPS, KNOWN_TAIL_GAPS, KNOWN_MATCH_GAPS, KNOWN_TARGET_FALSE_POSITIVES) and the "round-4" review of #5119. Those tables exist only on the unmerged branch of #5119 (still open); origin/next has no such tables. A request cannot depend on an unmerged change, and work is not stacked on an open PR.

    The underlying gap is real on next: stripComments (scripts/lint-state-write-path-drift.cjs:240-274) is not string- or regex-aware, so a // inside a string hides a real fs.writeFileSync( call on the same line (e.g. const u = 'http://x'; fs.writeFileSync(statePath, c); reports 0 findings), contradicting the function's own docstring (lines 230-238). ADR-4629 Decision 5 rejects the seeded local/no-adhoc-state-write ratchet/allowlist; it does not forbid changing the scanning technique.

    Please restate the acceptance criteria against next as it is now: each shape that should be reported or cleared, written as a concrete fixture, with no reference to rows from an unmerged PR. Once that is in place this goes back to the maintainer for a decision.

  4. added
    needs-reproductionCannot reproduce - waiting for more info
    and removed
    needs-triageNew issue awaiting maintainer review
    on Oct 14, 2026
  5. davesienkowski commented on Oct 16, 2026

    @davesienkowski
    CollaboratorAuthor

    Restated against next @ 46a119f as concrete fixtures. Today findRawStateWrites
    (scripts/lint-state-write-path-drift.cjs) flags only a single-line fs.writeFileSync( whose first
    argument contains statePath or STATE.md.

    Acceptance (every fixture below was run against the guard on next):

    • Must flag, clear today: // in a string or regex before the call, a call split over lines, a variable or constant target (including CRLF, +=, chained assignment, TypeScript annotation), other call shapes.
    • Must clear, flagged today: a STATE.md.bak literal, a string spelling the call, a dirname(statePath) join to another file.
    • Keep today's verdict for the pins and controls.
    • Real tree still reports rawWriteViolations: 0, or findings migrate onto the seam (no allowlist).
    • A parse failure is reported, never skipped.

    The technique is the maintainer's call.

    Fixtures (snippet | expected | today on next)

    Pins (flag | flag):
    P1 fs.writeFileSync(statePath, c);
    P2 fs.writeFileSync(path.join(cwd, 'STATE.md'), c);
    P3 fs.writeFileSync(${cwd}/STATE.md, c);
    P4 fs.writeFileSync(${a({b:1}).dir}/STATE.md, c);

    Must flag (flag | clear):
    A1 const u = 'http://x'; fs.writeFileSync(statePath, c);
    A2 const r = /\//; fs.writeFileSync(statePath, c);
    B1 fs.writeFileSync( NL statePath, NL c);
    B2 fs.writeFileSync(path.join(cwd, NL 'STATE.md'), c);
    C1 const p = path.join(dir, 'STATE.md'); NL fs.writeFileSync(p, c);
    C2 const STATE_FILENAME = 'STATE.md'; NL function w(){ fs.writeFileSync(path.join(d, STATE_FILENAME), c); }
    C3 as C1 with CRLF line endings
    C4 let p = dir; NL p += '/STATE.md'; NL fs.writeFileSync(p, c);
    C5 let p; p = path.join(dir,'STATE.md'); fs.writeFileSync(p, c);
    C6 const a = statePath; const b = a; fs.writeFileSync(b, c);
    C7 (.cts) const p: {a: string} = path.join(dir, 'STATE.md'); NL fs.writeFileSync(p, c);
    C8 (.cts) const p: string = path.join(dir, 'STATE.md'); NL fs.writeFileSync(p, c);
    D1 fs.promises.writeFile(statePath, c) D2 fs.writeFile(statePath, c, cb) D3 fs.appendFileSync(statePath, c) D4 fs.renameSync(tmp, statePath) D5 const { writeFileSync } = require('fs'); writeFileSync(statePath, c) D6 const f = require('fs'); f.writeFileSync(statePath, c) D7 require('fs').writeFileSync(statePath, c) D8 fs?.writeFileSync(statePath, c), fs['writeFileSync'](statePath, c), fs . writeFileSync (statePath, c)
    (D3 and D4 are scope calls for the maintainer.)

    Must clear, flagged today (clear | flag):
    E1 fs.writeFileSync(path.join(cwd, 'STATE.md.bak'), c);
    E2 const s = "fs.writeFileSync(statePath, c)";
    E3 fs.writeFileSync(path.join(path.dirname(statePath), 'ROADMAP.md'), c);

    Controls (clear | clear; must stay clear once variables resolve):
    F1 // fs.writeFileSync(statePath, c) F2 /* fs.writeFileSync(statePath, c) */
    F3 fs.writeFileSync(path.join(cwd, 'ROADMAP.md'), c);
    F4 fs.readFileSync(statePath, 'utf8');
    F5 fs.writeFileSync(out, statePath);
    F6 const a = () => { const p = 'x'; }; NL const b = (p) => { fs.writeFileSync(p, c); };
    F7 const p = path.join(d,'STATE.md'); NL const b = (p) => { fs.writeFileSync(p, c); };
    F8 const r = path.join(path.dirname(statePath), 'ROADMAP.md'); NL fs.writeFileSync(r, c);
    F9 const t = fs.readFileSync(statePath,'utf8').trim(); NL fs.writeFileSync(t, c); (path read from STATE.md content; a data value, not the STATE.md path)

    Generated by AI during triage; reviewed and posted by a human maintainer (@davesienkowski).

  6. added 3 commits that reference this issue on Oct 16, 2026
  7. davesienkowski commented on Oct 16, 2026

    @davesienkowski
    CollaboratorAuthor

    @trek-e the restatement you asked for is in the comment above (fixtures against next, no #5119 rows). Here is the runnable backing for its "today on next" column.

    One command, from a next checkout root, with the script below saved as repro-5279.cjs. On a fresh clone, run npm ci (or npm run build:lib) first, because the guard requires the gitignored, generated gsd-core/bin/lib/pattern.cjs:

    node repro-5279.cjs
    

    It writes each fixture to a temp src/fixture.cts and runs node scripts/lint-state-write-path-drift.cjs --root <tmp> --json. On next @ 19f2d5e (guard identical to 46a119f): ok=13 FN=30 FP=3 of 46.

    • All 38 posted fixtures match their stated verdict on next. Your 'http://x' case is A1: 0 findings.
    • 8 extra shapes also read 0: one-line function, createWriteStream, copyFileSync, fs/promises, ESM import, 'STATE' + '.md', { statePath: p }, fs NL .writeFileSync(.
    • Real tree: rawWriteViolations: 0.
    • typescript and espree are already devDependencies, so a parser adds no dependency.
    • D3/D4 (and createWriteStream/copyFileSync) remain your scope call. "Parse failure is reported" cannot be tested until a parser exists.
    Table (next) and script
    id shape expected next
    P1 pin flag flag
    P2 pin flag flag
    P3 pin flag flag
    P4 pin flag flag
    A1 string-// flag clear (FN)
    A2 regex-// flag clear (FN)
    B1 multiline flag clear (FN)
    B2 multiline flag clear (FN)
    C1 variable flag clear (FN)
    C2 module-const flag clear (FN)
    C3 variable-CRLF flag clear (FN)
    C4 variable-+= flag clear (FN)
    C5 variable-assign flag clear (FN)
    C6 variable-chain flag clear (FN)
    C7 ts-annot-brace flag clear (FN)
    C8 ts-annot flag clear (FN)
    S1 scope-oneline-fn flag clear (FN)
    D1 api:promises.writeFile flag clear (FN)
    D2 api:writeFile flag clear (FN)
    D3 api:appendFileSync flag clear (FN)
    D4 api:renameSync flag clear (FN)
    D5 destructured flag clear (FN)
    D6 aliased-fs flag clear (FN)
    D7 require-inline flag clear (FN)
    D8a optional-chain flag clear (FN)
    D8b bracket-member flag clear (FN)
    D8c spaced-member flag clear (FN)
    G1 api:createWriteStream flag clear (FN)
    G2 api:copyFileSync flag clear (FN)
    G3 api:fs/promises import flag clear (FN)
    G4 api:esm named import flag clear (FN)
    H1 split literal flag clear (FN)
    H2 destructured alias flag clear (FN)
    H3 fs / .writeFileSync( split flag clear (FN)
    E1 fp:.bak literal clear flag (FP)
    E2 fp:string spells call clear flag (FP)
    E3 fp:dirname join clear flag (FP)
    F1 ctl:line comment clear clear
    F2 ctl:block comment clear clear
    F3 ctl:other file clear clear
    F4 ctl:read clear clear
    F5 ctl:state as data clear clear
    F6 ctl:arrow param clear clear
    F7 ctl:shadowing param clear clear
    F8 ctl:dirname var clear clear
    F9 ctl:content-derived clear clear
    #!/usr/bin/env node
    // Repro for open-gsd/gsd-core#5279: runs the Axis-2 raw STATE.md write guard
    // (scripts/lint-state-write-path-drift.cjs) from two revisions against one
    // fixture per shape. Each fixture is written to <tmp>/<id>/src/fixture.cts and
    // scanned with the guard's own CLI: `node <guard> --root <tmp>/<id> --json`.
    // Verdict = flag when summary.rawWriteViolations > 0, else clear.
    'use strict';
    const fs = require('node:fs');
    const path = require('node:path');
    const { execFileSync } = require('node:child_process');
    
    const os = require('node:os');
    // Usage: node repro-5279.cjs [label=guardPath ...]
    // Default: the guard in the current checkout (run from the gsd-core repo root).
    const args = process.argv.slice(2);
    const GUARDS = {};
    for (const a of args.length ? args : ['cwd=scripts/lint-state-write-path-drift.cjs']) {
      const k = a.includes('=') ? a.slice(0, a.indexOf('=')) : 'guard';
      GUARDS[k] = path.resolve(a.slice(a.indexOf('=') + 1));
    }
    const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'repro-5279-'));
    const NL = '\n';
    const BT = '`';
    
    // [id, group, expected, source]
    const FIXTURES = [
      // Pins: today's true positives
      ['P1', 'pin', 'flag', 'fs.writeFileSync(statePath, c);'],
      ['P2', 'pin', 'flag', "fs.writeFileSync(path.join(cwd, 'STATE.md'), c);"],
      ['P3', 'pin', 'flag', 'fs.writeFileSync(' + BT + '${cwd}/STATE.md' + BT + ', c);'],
      ['P4', 'pin', 'flag', 'fs.writeFileSync(' + BT + '${a({b:1}).dir}/STATE.md' + BT + ', c);'],
      // A: comment stripper not string/regex-aware
      ['A1', 'string-//', 'flag', "const u = 'http://x'; fs.writeFileSync(statePath, c);"],
      ['A2', 'regex-//', 'flag', 'const r = /\\//; fs.writeFileSync(statePath, c);'],
      // B: multi-line call
      ['B1', 'multiline', 'flag', 'fs.writeFileSync(' + NL + 'statePath,' + NL + 'c);'],
      ['B2', 'multiline', 'flag', "fs.writeFileSync(path.join(cwd," + NL + "'STATE.md'), c);"],
      // C: variable / constant targets, scope
      ['C1', 'variable', 'flag', "const p = path.join(dir, 'STATE.md');" + NL + 'fs.writeFileSync(p, c);'],
      ['C2', 'module-const', 'flag', "const STATE_FILENAME = 'STATE.md';" + NL + 'function w(){ fs.writeFileSync(path.join(d, STATE_FILENAME), c); }'],
      ['C3', 'variable-CRLF', 'flag', "const p = path.join(dir, 'STATE.md');\r\nfs.writeFileSync(p, c);\r\n"],
      ['C4', 'variable-+=', 'flag', 'let p = dir;' + NL + "p += '/STATE.md';" + NL + 'fs.writeFileSync(p, c);'],
      ['C5', 'variable-assign', 'flag', "let p; p = path.join(dir,'STATE.md'); fs.writeFileSync(p, c);"],
      ['C6', 'variable-chain', 'flag', 'const a = statePath; const b = a; fs.writeFileSync(b, c);'],
      ['C7', 'ts-annot-brace', 'flag', "const p: {a: string} = path.join(dir, 'STATE.md');" + NL + 'fs.writeFileSync(p, c);'],
      ['C8', 'ts-annot', 'flag', "const p: string = path.join(dir, 'STATE.md');" + NL + 'fs.writeFileSync(p, c);'],
      ['S1', 'scope-oneline-fn', 'flag', "function w(){ const p = join(dir,'STATE.md'); fs.writeFileSync(p,c); }"],
      // D: other write APIs / call shapes
      ['D1', 'api:promises.writeFile', 'flag', 'fs.promises.writeFile(statePath, c);'],
      ['D2', 'api:writeFile', 'flag', 'fs.writeFile(statePath, c, cb);'],
      ['D3', 'api:appendFileSync', 'flag', 'fs.appendFileSync(statePath, c);'],
      ['D4', 'api:renameSync', 'flag', 'fs.renameSync(tmp, statePath);'],
      ['D5', 'destructured', 'flag', "const { writeFileSync } = require('fs'); writeFileSync(statePath, c);"],
      ['D6', 'aliased-fs', 'flag', "const f = require('fs'); f.writeFileSync(statePath, c);"],
      ['D7', 'require-inline', 'flag', "require('fs').writeFileSync(statePath, c);"],
      ['D8a', 'optional-chain', 'flag', 'fs?.writeFileSync(statePath, c);'],
      ['D8b', 'bracket-member', 'flag', "fs['writeFileSync'](statePath, c);"],
      ['D8c', 'spaced-member', 'flag', 'fs . writeFileSync (statePath, c);'],
      ['G1', 'api:createWriteStream', 'flag', 'fs.createWriteStream(statePath).end(c);'],
      ['G2', 'api:copyFileSync', 'flag', 'fs.copyFileSync(tmp, statePath);'],
      ['G3', 'api:fs/promises import', 'flag', "const fsp = require('fs/promises'); fsp.writeFile(statePath, c);"],
      ['G4', 'api:esm named import', 'flag', "import { writeFileSync } from 'node:fs';" + NL + 'writeFileSync(statePath, c);'],
      // H: extra leads from the coordinator's PR #5119 audit
      ['H1', 'split literal', 'flag', "fs.writeFileSync(path.join(d, 'STATE' + '.md'), c);"],
      ['H2', 'destructured alias', 'flag', 'const { statePath: p } = ctx;' + NL + 'fs.writeFileSync(p, c);'],
      ['H3', 'fs / .writeFileSync( split', 'flag', 'fs' + NL + '  .writeFileSync(statePath, c);'],
      // E: false positives
      ['E1', 'fp:.bak literal', 'clear', "fs.writeFileSync(path.join(cwd, 'STATE.md.bak'), c);"],
      ['E2', 'fp:string spells call', 'clear', 'const s = "fs.writeFileSync(statePath, c)";'],
      ['E3', 'fp:dirname join', 'clear', "fs.writeFileSync(path.join(path.dirname(statePath), 'ROADMAP.md'), c);"],
      // F: controls
      ['F1', 'ctl:line comment', 'clear', '// fs.writeFileSync(statePath, c)'],
      ['F2', 'ctl:block comment', 'clear', '/* fs.writeFileSync(statePath, c) */'],
      ['F3', 'ctl:other file', 'clear', "fs.writeFileSync(path.join(cwd, 'ROADMAP.md'), c);"],
      ['F4', 'ctl:read', 'clear', "fs.readFileSync(statePath, 'utf8');"],
      ['F5', 'ctl:state as data', 'clear', 'fs.writeFileSync(out, statePath);'],
      ['F6', 'ctl:arrow param', 'clear', "const a = () => { const p = 'x'; };" + NL + 'const b = (p) => { fs.writeFileSync(p, c); };'],
      ['F7', 'ctl:shadowing param', 'clear', "const p = path.join(d,'STATE.md');" + NL + 'const b = (p) => { fs.writeFileSync(p, c); };'],
      ['F8', 'ctl:dirname var', 'clear', "const r = path.join(path.dirname(statePath), 'ROADMAP.md');" + NL + 'fs.writeFileSync(r, c);'],
      ['F9', 'ctl:content-derived', 'clear', "const t = fs.readFileSync(statePath,'utf8').trim();" + NL + 'fs.writeFileSync(t, c);'],
    ];
    
    function verdict(guard, root) {
      let out;
      try {
        out = execFileSync(process.execPath, [guard, '--root', root, '--json'], { encoding: 'utf8' });
      } catch (e) {
        out = e.stdout; // guard exits non-zero when it finds violations
      }
      const j = JSON.parse(out);
      return j.summary.rawWriteViolations > 0 ? 'flag' : 'clear';
    }
    
    const rows = [];
    for (const [id, group, expected, src] of FIXTURES) {
      const root = path.join(TMP, id);
      fs.mkdirSync(path.join(root, 'src'), { recursive: true });
      fs.writeFileSync(path.join(root, 'src', 'fixture.cts'), src);
      const r = { id, group, expected };
      for (const [name, g] of Object.entries(GUARDS)) {
        const v = verdict(g, root);
        r[name] = v;
        r[name + 'Err'] = v === expected ? 'ok' : expected === 'flag' ? 'FN' : 'FP';
      }
      rows.push(r);
    }
    
    const names = Object.keys(GUARDS);
    console.log(`| id | shape | expected | ${names.join(' | ')} |`);
    console.log(`|---|---|---|${names.map(() => '---').join('|')}|`);
    for (const r of rows) {
      const cell = (n) => (r[n + 'Err'] === 'ok' ? r[n] : `${r[n]} (${r[n + 'Err']})`);
      console.log(`| ${r.id} | ${r.group} | ${r.expected} | ${names.map(cell).join(' | ')} |`);
    }
    const tally = (n) => {
      const t = { ok: 0, FN: 0, FP: 0 };
      for (const r of rows) t[r[n + 'Err']]++;
      return `${n}: ok=${t.ok} FN=${t.FN} FP=${t.FP} of ${rows.length}`;
    };
    console.log('');
    for (const n of names) console.log(tally(n));
    fs.rmSync(TMP, { recursive: true, force: true });

    Generated by AI during triage; reviewed and posted by a human maintainer (@davesienkowski).

  8. added 3 commits that reference this issue on Oct 18, 2026
  9. trek-e commented on Oct 18, 2026

    @trek-e
    Collaborator

    This was generated by AI during triage.

    Approved. The restated acceptance criteria are concrete and runnable against next: re-running the posted script on current next gives ok=13, FN=30, FP=3 of 46, matching the report. needs-reproduction is removed.

    Decisions recorded for the scope calls the thread left open:

    • Technique: a syntax-only TypeScript AST pass for Axis 2 only (no type checker). ADR-4629 Decision 5 forbids allowlists, not a scanning technique.
    • Call shapes in scope: writeFileSync, promises.writeFile, writeFile, appendFileSync, renameSync (destination argument), createWriteStream and copyFileSync (destination argument), including aliased, destructured, optional-chained and bracket forms.
    • Files parsed: .cts/.cjs only; prompt files keep their existing handling.
    • Parse failure: reported, never skipped.
    • Unchanged: Axes 1 and 3-5 keep their line scans; the real tree must still report rawWriteViolations: 0, or each new finding migrates onto the seam (no allowlist).
  10. added
    approved-enhancementEnhancement approved — contributor may begin coding
    and removed
    needs-reproductionCannot reproduce - waiting for more info
    on Oct 18, 2026
  11. added 6 commits that reference this issue on Oct 21, 2026
  12. davesienkowski commented on Oct 24, 2026

    @davesienkowski
    CollaboratorAuthor

    @trek-e thanks for the approval. #5279 is sequenced behind #5119: both change the Axis-2 resolver in scripts/lint-state-write-path-drift.cjs, and the AST pass replaces that code. I will not open the PR until #5119 merges, then branch from next and flip the 46-fixture repro (ok=13, FN=30, FP=3 at your re-run) to ok, with the call shapes and parse-failure rule you listed. That covers all 46 rows of the acceptance table you approved, including the split literal (H1) and the destructured alias (H2).

    Generated by AI during triage; reviewed and posted by a human maintainer (@davesienkowski).

  13. added a commit that references this issue on Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

approved-enhancementEnhancement approved — contributor may begin codingarea: workflowPhase execution, ROADMAP, STATE.mdenhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions