* refactor(#3187): give state field extraction one canonical owner stateFieldValue in state-document.cts becomes the single owner of the #1760 frontmatter-then-body fallback chain. The new whole-repo guard found 14 independent re-derivations where the epic scoped 5, all now routed through it: cmdStateSnapshot (11), cmdStatePrune (2) and smart-entry fmScalar (1). state validate was a gate that could not fail. Every warning it could emit sat behind a phase resolved without the frontmatter tier, so a STATE.md whose phase lives only in frontmatter skipped the drift scan entirely and returned valid:true. It also read unstripped content, letting a frontmatter status: key shadow the body field (#1255 class). Both fixed; output gains a scope field so could-not-look stops being output-identical to looked-and-clean. Verified on the remote runner. * docs(#3187): document the state validate scope field and its reason codes Adds docs/how-to/interpret-state-validate-results.md so a reader can tell nothing-to-report from could-not-look, updates the COMMANDS.md and USER-GUIDE.md entries, corrects the CONTEXT.md glossary overstatement about Current Position sole ownership, and drops the changeset fragment. * fix(#3187): close three drift-guard evasion shapes and test the refuse path The isolated adversarial review found the ladder detector was evadable by ordinary reformatting, not just deliberately: a member or computed operand (fm.key / fm[key]) missed the bare-identifier backreference, a swapped tier order missed a hardcoded number-then-boolean sequence, and a ladder wrapped across lines missed single-line detection. All three now caught, each with its own test plus a proven boundary control. The frontmatter-parse refuse path on the destructive complete-phase route was unreachable and therefore untested. It is now driven by an injected parse failure and asserts STATE.md is byte-identical after the refusal, rather than shipping untested defensive code on a path that rewrites user state. Verified on the remote runner. * fix(#3187): widen the drift guard to the prompt layer and disclose tier-2 changes The code-review spec axis found the guard's scan surface was src/ only, which is Decision 4(d)'s forbidden allowlist one directory wide - and it had a live miss: gsd-core/workflows/smart-entry.md tells an agent to read status from frontmatter or the body, a prose expression of this same chain. The surface now covers the prompt layer. That one site carries a permanent written exemption rather than a ratchet: it is the gsd-tools-is-down fallback, so it cannot call the owner by construction, and a ratchet would imply removable debt that does not exist. Two tier-2 output changes shipped undisclosed and are now named in the changeset and docs: complete-phase's idempotency guard consulting frontmatter, and the workstream inventory resolving frontmatter-only fields. docs/COMMANDS.md gains a state complete-phase entry, which it never had. Also records Amendment 5 on ADR-3180, extracts the duplicated frontmatter-parse block the epic's own thesis forbids, and re-points two assertions from free-form warning prose onto the structured drift object. Verified on the remote runner. * chore(#3187): backfill changeset PR number pr:0 placeholder replaced with the real PR number now that #3283 exists. --------- Co-authored-by: sim <sim@local>
806 lines
41 KiB
JavaScript
806 lines
41 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
/**
|
|
* Anti-divergence drift guard for the STATE.md FIELD-EXTRACTION FALLBACK
|
|
* CHAIN (epic #3180, issue #3187, ADR-3180 Decision 4, spec §7.7).
|
|
*
|
|
* The derivation this guard protects is "read a STATE.md field: prefer the
|
|
* YAML frontmatter scalar (string, trimmed-non-empty; or number/boolean
|
|
* coerced to a string), else fall back to the body field via
|
|
* `stateExtractField(body, bodyField)`". `src/state-document.cts` is
|
|
* DESIGNATED the single canonical owner of this grammar (issue #3187 Phase
|
|
* 5), but as of this guard's authorship (Phase 5's guard-first step, per
|
|
* ADR-3180 Amendment 3's standing rule) the canonical function does not yet
|
|
* exist there — this script is written to discover every existing
|
|
* re-derivation BEFORE any src/ file is touched, exactly the ordering
|
|
* `lint-plan-count-drift.cjs` and its siblings established.
|
|
*
|
|
* Per ADR-3180 Decision 4(a) this guard discovers call sites by SCANNING THE
|
|
* WHOLE `src/` TREE, not by consulting an allowlist of known files. Per
|
|
* Decision 4(d) that surface is widened further still: `src/` alone is
|
|
* itself the forbidden allowlist, one directory wide, so this guard ALSO
|
|
* scans the prompt-layer markdown (`gsd-core/workflows`, `commands`,
|
|
* `agents`, `skills`) for a PROSE re-derivation of the same chain — see the
|
|
* "PROMPT-LAYER PROSE DETECTION" section below `findStateFieldDrift`.
|
|
*
|
|
* DETECTION: FUNCTION-SCOPED CO-OCCURRENCE, not a bounded line-window.
|
|
* An earlier draft of this guard used a fixed line-distance window between
|
|
* the ladder and the fallback call. That shape was REJECTED: it produced a
|
|
* documented "known miss" on live copies inside the very function it was
|
|
* scanning (state.cts `cmdStateSnapshot`'s `Current Plan` / `Total Plans in
|
|
* Phase` / `Status` / `Progress` / `Last Activity` reads all sit further
|
|
* from the ladder than any defensible line count), which means the guard
|
|
* could report "0 re-derivations" after only the nearest few copies were
|
|
* migrated while several more of the SAME kind survived untouched in the
|
|
* SAME function — "a zero it did not earn" (ADR-3180 Decision 4(a)). A fixed
|
|
* N is also trivially gameable by reflow (Goodhart's law): moving a call one
|
|
* line further from its ladder silences the guard without changing the
|
|
* derivation at all. The window is dropped entirely; there is no magic
|
|
* number anywhere in this detection.
|
|
*
|
|
* The invariant instead: a NAMED FUNCTION that BOTH (a) contains a
|
|
* "frontmatter scalar coercion ladder" — the SAME operand compared via
|
|
* `typeof OPERAND === 'number'` and `typeof OPERAND === 'boolean'`
|
|
* (TYPEOF_TIER_CLAUSE_RE, order-independent, member/computed operands
|
|
* included, an optional `typeof OPERAND === 'string'` tier tolerated
|
|
* anywhere among them — see the ternary shape in `cmdStatePrune` below),
|
|
* within LADDER_WINDOW_LINES of each other — ANYWHERE in its own body,
|
|
* INCLUDING inside a nested closure it defines, AND (b) calls
|
|
* `stateExtractField(` anywhere in that same body, is re-deriving the
|
|
* fm-else-body fallback chain. EVERY `stateExtractField(` call line inside
|
|
* such a function is reported, not just the first.
|
|
*
|
|
* LADDER DETECTION, evasion-resistant shapes (see "LADDER DETECTION DETAIL"
|
|
* below for the full account, including the one shape still NOT caught):
|
|
* - operand may be a bare identifier (`v`), a dotted member expression
|
|
* (`fm.key`), or a computed access (`fm[key]`) — matched via
|
|
* OPERAND_SOURCE and compared as captured TEXT, not a bare-identifier
|
|
* backreference.
|
|
* - the two required tiers ('number', 'boolean') may appear in EITHER
|
|
* order; an optional third 'string' tier may sit anywhere among them.
|
|
* - the two clauses may sit on the SAME line (the classic single-line
|
|
* `||` chain) or on DIFFERENT lines within LADDER_WINDOW_LINES of each
|
|
* other — including as two entirely separate `if` statements, not just
|
|
* one expression wrapped across a line break.
|
|
*
|
|
* FUNCTION ATTRIBUTION. Two declaration shapes are recognised as opening a
|
|
* new named function scope:
|
|
* - `function NAME(...) {` (FUNCTION_DECL_RE) — top-level OR nested, at
|
|
* any indentation. `src/smart-entry.cts`'s `fmScalar` (line 144) is this
|
|
* shape, and is itself top-level.
|
|
* - `const NAME = (...): ReturnType => {` (ARROW_CONST_RE) — an arrow
|
|
* function assigned to a `const`, with a block body (`=> {`, not an
|
|
* expression body like `=> ({...})`, which never opens a new function
|
|
* frame — its `{` is an object literal, still counted toward brace
|
|
* depth, but attributes no name). `src/state.cts`'s `cmdStateSnapshot`
|
|
* defines its `fmScalar` (line 1464) this way, NESTED inside
|
|
* `cmdStateSnapshot` itself.
|
|
* `ARROW_CONST_RE`'s pattern requires `=>\s*\{` literally, so it only ever
|
|
* matches a line that already carries its opening brace — pushed
|
|
* immediately. `FUNCTION_DECL_RE` carries no such guarantee: a multi-line
|
|
* signature (parameters and/or a return-type annotation spilling onto later
|
|
* lines) matches on a line with NO `{` at all. An earlier version of this
|
|
* guard pushed such a match immediately anyway, recording `openDepth` at the
|
|
* ENCLOSING scope's depth rather than the function's own — for a top-level
|
|
* function, `openDepth: 0`, and because real code never reaches negative
|
|
* depth, a frame pushed with `openDepth: 0` could NEVER pop, sitting at the
|
|
* bottom of the stack for the rest of the file and silently misattributing
|
|
* every later line with no OTHER open frame to it. `src/state.cts`'s
|
|
* `preferNewerLastActivity` (a 4-line signature, `{` on its own line) is the
|
|
* live instance that surfaced this; it caused no observed false violation
|
|
* only because nothing ever called `stateExtractField(` at true module scope
|
|
* after it, not because the tracking was sound. `buildFunctionInfo` now
|
|
* DEFERS a `FUNCTION_DECL_RE` match (`pendingDeclName`) across lines until
|
|
* the first subsequent line whose brace count actually increases, and pushes
|
|
* the frame THERE, with `openDepth` computed from that line's `depth` —
|
|
* matching every other frame's push convention. A pending name is abandoned
|
|
* (never pushed) if a `;` terminates the statement before any `{` appears —
|
|
* a type-only declaration, `declare function`, or overload signature, none
|
|
* of which open a body.
|
|
* Function scopes NEST via a brace-depth stack: entering either shape pushes
|
|
* a frame; the frame pops once brace depth returns below the depth recorded
|
|
* when it was pushed. A line is attributed to the INNERMOST currently-open
|
|
* named frame (falling back to whatever enclosing frame IS open — typically
|
|
* the nearest enclosing top-level function — when a line sits between two
|
|
* sibling nested scopes; module-level code with no open frame is
|
|
* unattributed and therefore never a violation, matching "if you cannot
|
|
* identify one" in the design brief).
|
|
*
|
|
* TRANSITIVE ladder attribution is what makes `cmdStateSnapshot` (whose
|
|
* OWN top-level statements never spell the ladder themselves — only its
|
|
* nested `fmScalar` closure does) still register as ladder-bearing: when
|
|
* LADDER_RE matches a line, EVERY frame currently open on the stack at that
|
|
* point — not just the innermost — is marked ladder-bearing, because a
|
|
* nested closure's body is lexically part of every one of its enclosing
|
|
* functions' own bodies. `stateExtractField(` calls are attributed to the
|
|
* INNERMOST frame only (no transitivity needed there: the call already sits
|
|
* directly inside whichever frame is innermost at that point).
|
|
*
|
|
* BRACE-DEPTH COUNTING runs over `scanCode`'s per-line, cross-file output
|
|
* (see that function's own header for the full rationale and the concrete
|
|
* bug its cross-line comment/template tracking fixes) — comments and
|
|
* quoted/backtick string and template literal CONTENTS are already removed
|
|
* before a single brace is counted, escape-aware and threaded across line
|
|
* boundaries, so neither a brace inside a string (`{ label: '{' }`) nor one
|
|
* inside a multi-line block comment or template literal perturbs the depth
|
|
* count. It does NOT specially recognise regex literals (a `{` inside a
|
|
* `/.../ ` quantifier, e.g. `/x{2,3}/`, is counted as a plain character);
|
|
* see `scanCode`'s header for why every such literal actually present in
|
|
* `src/` today is harmless (balanced on its own line).
|
|
*
|
|
* MUST-NOT-FLAG case verified by running the guard (the earlier "case
|
|
* variant chain" exemption for `src/state.cts:1488` was WRONG and is
|
|
* SUPERSEDED — see the header of the guard's initial version in git history
|
|
* for the retracted reasoning; `cmdStateSnapshot` is ladder-bearing, so
|
|
* *every* `stateExtractField(` call inside it, including line 1488, is
|
|
* correctly a violation now):
|
|
* - `src/smart-entry.cts`'s `fmScalarKey` (lines 152-158): its own
|
|
* `typeof v === 'number' || typeof v === 'boolean'` ladder (line 156)
|
|
* reads a value out of a NESTED frontmatter object and never calls
|
|
* `stateExtractField(` anywhere in its own body — it is ladder-bearing
|
|
* but call-free, so it is correctly never flagged. This is the live
|
|
* control case proving the guard still distinguishes "has a ladder" from
|
|
* "re-derives the fallback chain": a ladder alone, with no body
|
|
* fallback call in the same function, is a different question (reading
|
|
* a nested frontmatter object, full stop) and stays silent.
|
|
* - Any ladder or `stateExtractField(` call appearing inside a `//` line
|
|
* comment or a `/* *\/`-style block comment (single- or multi-line).
|
|
* `scanCode` blanks comment text — cross-line-aware — before either
|
|
* regex runs, so prose describing this derivation is never mistaken for
|
|
* a copy of it (ADR-3180 Amendment 3's "trains readers to reflexively
|
|
* exempt documentation" note).
|
|
*
|
|
* FUNCTION-SCOPED EXEMPTIONS (per ADR-3180 Decision 4(a): NEVER a bare
|
|
* whole-file allowlist — Decision 4(d) records that a whole-file exemption
|
|
* on the owner is precisely how `getMilestoneInfo` stayed invisible to an
|
|
* earlier guard). `src/state-document.cts` is the owner of this grammar;
|
|
* its `stateFieldValue` (added for issue #3187 Phase 5, landed in this same
|
|
* working tree while this guard was being authored — see the guard's commit
|
|
* history / PR for the exact sequencing) IS the canonical
|
|
* frontmatter-scalar-then-body-field chain, not a copy of it, so it is the
|
|
* ONLY entry in FUNCTION_SCOPED_EXEMPTIONS. Every other function in
|
|
* `state-document.cts` — including any future re-derivation added anywhere
|
|
* else in that file — is still scanned and still flagged (mirrors
|
|
* `lint-completion-ratio-drift.cjs`'s `FUNCTION_SCOPED_EXEMPTIONS` for
|
|
* `clampPercent`/`clampPercentFromFraction`).
|
|
*
|
|
* Every regex below is small, bounded, and has no nested/overlapping
|
|
* quantifiers; the string-literal and brace scans are plain escape-aware
|
|
* character loops, not regexes, so there is nothing for a backtracking
|
|
* engine to explore. `npm run lint:ci` runs CodeQL js/redos over this repo;
|
|
* mirrors the ReDoS discipline of the sibling drift guards.
|
|
*
|
|
* The tree-walk / root-confinement / sanitizer machinery is SHARED with the
|
|
* sibling drift guards via `scripts/lib/drift-scan.cjs` (ADR-3180 Decision
|
|
* 4). This guard's detection shape needs no regex-literal extraction, so it
|
|
* does not use `readRegexLiteralAt`; the reported fragment is simply the
|
|
* trimmed source line, bounded to MAX_REGEX_LITERAL_LEN characters.
|
|
*
|
|
* LADDER DETECTION DETAIL (evasion history). An earlier version of this
|
|
* guard's ladder pattern was a single regex, `typeof (\w+) === 'number' \|\|
|
|
* typeof \1 === 'boolean'`, tested against ONE line. An isolated adversarial
|
|
* review found three live-shaped ways past it, all verified to produce ZERO
|
|
* violations against a real re-derivation:
|
|
* (a) a member-expression or computed operand — `typeof fm.key ===
|
|
* 'number' || typeof fm.key === 'boolean'` — never matched, because
|
|
* the bare-identifier backreference (`\w+` then `\1`) cannot match
|
|
* `fm.key`/`fm[key]` at all.
|
|
* (b) the tiers in the opposite order — `typeof v === 'boolean' ||
|
|
* typeof v === 'number'` — never matched, because the pattern
|
|
* hardcoded 'number' before 'boolean' with no alternative ordering.
|
|
* This one is plausible from an ordinary code-review reformat, not
|
|
* deliberate evasion.
|
|
* (c) the ladder split across two lines, or written as two separate `if`
|
|
* statements instead of one `||` chain — never matched, because
|
|
* detection ran per-line with no tolerance for the pair spanning more
|
|
* than one line.
|
|
* The fix: OPERAND_SOURCE (identifier / dotted member / computed access,
|
|
* compared as captured TEXT rather than a bare-identifier backreference),
|
|
* TYPEOF_TIER_CLAUSE_RE (one clause at a time, tier-order-independent,
|
|
* collected via a `Map<operand, Set<tier>>`), and a small bounded
|
|
* LADDER_WINDOW_LINES sliding window so the two required clauses need only
|
|
* sit within a few lines of each other, not on the identical line. All
|
|
* three evasion shapes above are now covered by
|
|
* `tests/state-field-drift.test.cjs` (D3c/D3d/D3e).
|
|
*
|
|
* KNOWN, ACCEPTED limits of this scan (honest, not exhaustive by
|
|
* construction — this is a bounded regex-based scan, not a parser):
|
|
* - Cross-function: a re-derivation whose ladder and fallback call sit in
|
|
* two DIFFERENT named functions with no shared enclosing scope (e.g. a
|
|
* ladder in one file-level helper, consumed by a caller in another
|
|
* function that itself calls `stateExtractField(` for an unrelated
|
|
* field) is not caught — this guard's unit is "one named function's own
|
|
* body, including its nested closures", not the whole call graph. That
|
|
* is left to code review, not this regex. This co-occurrence check is
|
|
* deliberately FUNCTION-SCOPED with NO line-distance window at all (see
|
|
* the module-level "DETECTION" note above) — do not confuse this with
|
|
* LADDER_WINDOW_LINES, which bounds a different, much tighter pairing
|
|
* (the ladder's own two clauses, which are always part of the SAME
|
|
* conditional expression by construction, not an arbitrary call
|
|
* anywhere later in the function).
|
|
* - Ladder clauses further apart than LADDER_WINDOW_LINES lines: a ladder
|
|
* whose 'number' and 'boolean' clauses are more than
|
|
* LADDER_WINDOW_LINES lines apart (e.g. separated by an unrelated
|
|
* intervening block of code, not just the couple of lines a single
|
|
* conditional or two adjacent `if`s span) is not caught. No such shape
|
|
* has been observed in this codebase; if one appears, raise
|
|
* LADDER_WINDOW_LINES rather than silently accepting the miss.
|
|
* - Non-`typeof`-shaped coercion checks: a ladder rewritten through a
|
|
* `switch (typeof v)`, a helper function abstracting the check (e.g.
|
|
* `isNumberOrBoolean(v)`), or any comparison operator other than
|
|
* `===` (e.g. `typeof v == 'number'`) is not recognised — the pattern
|
|
* is `typeof OPERAND === 'TIER'` literally, not "any type-coercion
|
|
* test with equivalent runtime behaviour."
|
|
* - Nested computed access: `a[b[c]]` is not modelled as a single
|
|
* operand — the computed-content class excludes `]`, so nested
|
|
* brackets truncate the captured operand at the first `]` rather than
|
|
* matching the whole expression. Not observed in this codebase's
|
|
* ladders today.
|
|
* - Whitespace-sensitive operand identity: `fm[key]` and `fm[ key ]` are
|
|
* compared as distinct operand TEXT (no normalisation), so a ladder
|
|
* whose two clauses format the same computed access differently could
|
|
* under-detect. Not observed in this codebase's ladders today.
|
|
*/
|
|
|
|
const path = require('node:path');
|
|
const driftScan = require('./lib/drift-scan.cjs');
|
|
const { MAX_REGEX_LITERAL_LEN, sanitizeForReport, scanTree } = driftScan;
|
|
|
|
// A ladder operand: a bare identifier (`v`), a dotted member expression
|
|
// (`fm.key`, `fm.a.b`), or a computed/bracket access (`fm[key]`), repeated
|
|
// via a single bounded alternation. No nested/overlapping quantifiers: the
|
|
// bracket-content class (`[^\]\r\n]{1,80}`) is one bounded character class,
|
|
// not a quantifier nested inside another quantifier, so there is nothing
|
|
// for a backtracking engine to explore. The captured TEXT (not a bare-
|
|
// identifier backreference) is what two clauses are compared against for
|
|
// "same operand" — see TYPEOF_TIER_CLAUSE_RE below and buildFunctionInfo's
|
|
// ladder-window accumulation.
|
|
const OPERAND_SOURCE = String.raw`[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[[^\]\r\n]{1,80}\])*`;
|
|
|
|
// One ladder clause: `typeof OPERAND === 'TIER'`, TIER being one of the
|
|
// three coercion tiers this derivation ever compares against. Run with the
|
|
// `g` flag over a single line's `detect` text so every clause that line
|
|
// carries is collected (a single-line `||` chain carries two or three; a
|
|
// clause split onto its own line, or written as a standalone `if`, carries
|
|
// one). No trailing `\b` after the closing quote: the quote itself is
|
|
// already an unambiguous, non-word boundary — see the historical note this
|
|
// replaced for why a trailing `\b` there silently matches nothing at all.
|
|
const TYPEOF_TIER_CLAUSE_RE = new RegExp(String.raw`\btypeof\s+(${OPERAND_SOURCE})\s*===\s*'(number|boolean|string)'`, 'g');
|
|
|
|
// A ladder is CONFIRMED when the SAME operand carries both a 'number' clause
|
|
// and a 'boolean' clause (in either order; an additional 'string' clause
|
|
// anywhere among them does not prevent this) within this many consecutive
|
|
// lines of each other. Small and bounded, and DELIBERATELY NOT the same
|
|
// concept as the ladder-to-`stateExtractField(` co-occurrence check, which
|
|
// stays function-scoped with no window at all (see the module header's
|
|
// "DETECTION" and "LADDER DETECTION DETAIL" notes for why these two
|
|
// pairings are not interchangeable).
|
|
const LADDER_WINDOW_LINES = 6;
|
|
|
|
// The body-fallback call whose presence, inside a ladder-bearing function,
|
|
// makes that function a re-derivation of the "frontmatter-else-body" grammar.
|
|
const STATE_EXTRACT_FIELD_CALL_RE = /\bstateExtractField\(/;
|
|
|
|
// Named function scope openers. Each requires its own opening `{` on the
|
|
// SAME line as the signature — see the header's "FUNCTION ATTRIBUTION"
|
|
// paragraph for the documented limitation and why it does not affect either
|
|
// real copy this guard was written against.
|
|
// - `function NAME(...) {` — top-level OR nested, any indentation.
|
|
const FUNCTION_DECL_RE = /\bfunction\s+([A-Za-z_$][\w$]*)\s*\(/;
|
|
// - `const NAME = (...): ReturnType => {` — block-bodied arrow assigned to
|
|
// a const. `[^)]*` and `[^=]*` are bounded, single-purpose character
|
|
// classes (no nested/overlapping quantifiers): the former stops at the
|
|
// parameter list's closing paren, the latter at the arrow itself.
|
|
const ARROW_CONST_RE = /\bconst\s+([A-Za-z_$][\w$]*)\s*=\s*\([^)]*\)\s*(?::\s*[^=]+)?=>\s*\{/;
|
|
|
|
// ADR-3180 Decision 4(d): a guard's scan surface is every AUTHORED surface
|
|
// that can EXPRESS the derivation, not `src/` — reading "whole-repo scan" as
|
|
// "the whole `src/` tree" is itself the forbidden allowlist, one directory
|
|
// wide. This derivation is expressed in TWO languages: TypeScript under
|
|
// `src/` (the ladder + `stateExtractField(` shape PASS 1/2 above detect), and
|
|
// PROSE in the workflow/command/agent/skill markdown that ships to every
|
|
// runtime — `gsd-core/workflows/smart-entry.md`'s "Extract: `status`
|
|
// (frontmatter `status:` or body `**Status:**`)" is exactly this chain,
|
|
// hand-described rather than called. `SCAN_DIRS` therefore covers both;
|
|
// `findPromptFieldDrift` (below `findStateFieldDrift`) is the markdown-side
|
|
// detector, dispatched by extension in `scanRepo`'s `onFile`. Mirrors
|
|
// `lint-planning-prompt-drift.cjs`'s own prompt-layer surface exactly
|
|
// (`gsd-core/workflows`, `commands`, `agents`, `skills`).
|
|
const SCAN_DIRS = ['src', 'gsd-core/workflows', 'commands', 'agents', 'skills'];
|
|
const SCAN_EXT = new Set(['.cts', '.ts', '.mts', '.md']);
|
|
|
|
// The designated owner (issue #3187 Phase 5, ADR-3180 §7.7).
|
|
const OWNER_FILE = path.join('src', 'state-document.cts');
|
|
|
|
// Per ADR-3180 Decision 4(a): function-scoped, NEVER a bare file allowlist —
|
|
// Decision 4(d) records that a whole-file exemption on the owner is
|
|
// precisely how `getMilestoneInfo` stayed invisible to an earlier guard.
|
|
// Only `stateFieldValue` itself is exempt: it IS the canonical
|
|
// frontmatter-scalar-then-body-field chain (holds the ladder AND calls
|
|
// `stateExtractField(` by construction, in its own body — that is its whole
|
|
// job), not a copy of it. Every OTHER function in this same file — including
|
|
// any future re-derivation added anywhere else in state-document.cts — is
|
|
// still scanned and still flagged; nothing else in this file is exempt.
|
|
const FUNCTION_SCOPED_EXEMPTIONS = new Map([[OWNER_FILE, new Set(['stateFieldValue'])]]);
|
|
|
|
/**
|
|
* Tokenize the WHOLE file into TWO parallel per-line views, from ONE
|
|
* single-pass, escape-aware character scan (not a regex — no backtracking
|
|
* cost to bound):
|
|
* - `detect[i]`: comments stripped, but string/template literal contents
|
|
* KEPT VERBATIM (quotes included). This is what the detection regexes
|
|
* (LADDER_RE / STATE_EXTRACT_FIELD_CALL_RE / FUNCTION_DECL_RE /
|
|
* ARROW_CONST_RE) run against — they need the literal quoted text
|
|
* `'number'` / `'boolean'` to still be present.
|
|
* - `braces[i]`: comments AND string/template literal CONTENTS stripped,
|
|
* used only for brace-depth counting, so a brace character written
|
|
* inside a string (e.g. `{ label: '{' }`) never perturbs the count.
|
|
*
|
|
* This REPLACES two earlier, narrower designs in turn:
|
|
* 1. Two independent per-line helpers (`stripComments` + a separate
|
|
* `stripStringLiterals`), each with no cross-line state. That design
|
|
* missed a `/* ... *\/`-style block comment whose CLOSING line also
|
|
* carries trailing real code (e.g. a `catch { /* comment` opener
|
|
* followed by ` * more comment. *\/ }` on a later line): the old
|
|
* per-line `stripComments` heuristic ("a line whose trimmed text starts
|
|
* with `*` is entirely comment") blanked that closing line WHOLESALE,
|
|
* silently dropping the real `}` it also carried, which left a
|
|
* function's brace-depth frame permanently open and made every LATER
|
|
* `stateExtractField(` call in the file — inside unrelated functions —
|
|
* inherit that stale frame's ladder-bearing status (`src/state.cts`'s
|
|
* `cmdStateValidate`, which has no ladder of its own, was falsely
|
|
* flagged this way).
|
|
* 2. A single merged output that dropped string CONTENTS unconditionally.
|
|
* That fixed (1) but broke detection outright: LADDER_RE needs the
|
|
* literal text `'number'`/`'boolean'` (with quotes) to match, and a
|
|
* merged output that strips quote contents for brace-safety also
|
|
* erases the very tokens the ladder regex looks for — every ladder
|
|
* line silently stopped matching. Two views, not one, is what lets
|
|
* each consumer see what it actually needs.
|
|
*
|
|
* `inBlockComment` and `inTemplate` are threaded ACROSS lines so a block
|
|
* comment or a multi-line backtick template literal spanning several source
|
|
* lines is tracked correctly regardless of what trails its closing
|
|
* delimiter. Regex literals are NOT specially recognised (documented,
|
|
* narrow, known limitation, same as the sibling drift guards'
|
|
* `readRegexLiteralAt`-free scans): a `/` is only ever treated as a comment
|
|
* opener when immediately followed by another `/` or `*`, so an ordinary
|
|
* regex literal's slashes pass through as plain characters and any brace
|
|
* inside one (e.g. `/x{2,3}/`) is counted like any other character —
|
|
* verified harmless for every regex literal actually present in `src/`
|
|
* today because each is balanced (equal opens/closes) on its own line, so it
|
|
* never desyncs the running depth total even though the individual
|
|
* characters are not "understood" as a literal.
|
|
*/
|
|
function scanCode(lines) {
|
|
const detect = new Array(lines.length);
|
|
const braces = new Array(lines.length);
|
|
let inBlockComment = false;
|
|
let inTemplate = false;
|
|
for (let li = 0; li < lines.length; li++) {
|
|
const line = lines[li];
|
|
let outDetect = '';
|
|
let outBraces = '';
|
|
let i = 0;
|
|
if (inTemplate) {
|
|
const start = i;
|
|
while (i < line.length) {
|
|
if (line[i] === '\\') {
|
|
i += 2;
|
|
continue;
|
|
}
|
|
if (line[i] === '`') {
|
|
i++;
|
|
inTemplate = false;
|
|
break;
|
|
}
|
|
i++;
|
|
}
|
|
outDetect += line.slice(start, i); // template contents kept verbatim for detect
|
|
if (inTemplate) {
|
|
detect[li] = outDetect;
|
|
braces[li] = ''; // whole line still inside the unterminated template
|
|
continue;
|
|
}
|
|
}
|
|
while (i < line.length) {
|
|
if (inBlockComment) {
|
|
const close = line.indexOf('*/', i);
|
|
if (close === -1) {
|
|
i = line.length;
|
|
break;
|
|
}
|
|
i = close + 2;
|
|
inBlockComment = false;
|
|
continue;
|
|
}
|
|
const ch = line[i];
|
|
if (ch === '/' && line[i + 1] === '/') {
|
|
i = line.length; // rest of line is a line comment
|
|
break;
|
|
}
|
|
if (ch === '/' && line[i + 1] === '*') {
|
|
inBlockComment = true;
|
|
i += 2;
|
|
continue;
|
|
}
|
|
if (ch === "'" || ch === '"') {
|
|
const quote = ch;
|
|
const start = i;
|
|
let j = i + 1;
|
|
while (j < line.length) {
|
|
if (line[j] === '\\') {
|
|
j += 2; // escape consumes the next character, whatever it is
|
|
continue;
|
|
}
|
|
if (line[j] === quote) {
|
|
j++;
|
|
break;
|
|
}
|
|
j++;
|
|
}
|
|
outDetect += line.slice(start, j); // string kept verbatim for detect
|
|
// (nothing appended to outBraces — string contents excluded from depth counting)
|
|
i = j;
|
|
continue;
|
|
}
|
|
if (ch === '`') {
|
|
const start = i;
|
|
let j = i + 1;
|
|
let closed = false;
|
|
while (j < line.length) {
|
|
if (line[j] === '\\') {
|
|
j += 2;
|
|
continue;
|
|
}
|
|
if (line[j] === '`') {
|
|
j++;
|
|
closed = true;
|
|
break;
|
|
}
|
|
j++;
|
|
}
|
|
if (!closed) {
|
|
outDetect += line.slice(start); // rest of line kept verbatim for detect
|
|
inTemplate = true;
|
|
i = line.length;
|
|
break;
|
|
}
|
|
outDetect += line.slice(start, j); // template kept verbatim for detect
|
|
i = j;
|
|
continue;
|
|
}
|
|
outDetect += ch;
|
|
outBraces += ch;
|
|
i++;
|
|
}
|
|
detect[li] = outDetect;
|
|
braces[li] = outBraces;
|
|
}
|
|
return { detect, braces };
|
|
}
|
|
|
|
/**
|
|
* Pure: find every unsanctioned STATE.md field-extraction fallback-chain
|
|
* re-derivation in `text`. `relPath` is the repo-relative path, used both to
|
|
* report file:line and to apply the function-scoped owner exemptions above.
|
|
*
|
|
* Two passes over the same line array:
|
|
* PASS 1 walks the file once, maintaining a brace-depth stack of open
|
|
* named function frames, and records which function NAMES are
|
|
* ladder-bearing (transitively — see the header's "TRANSITIVE ladder
|
|
* attribution" paragraph) and, per source line, which frame is
|
|
* INNERMOST at that line (for call attribution) — {@link buildFunctionInfo}.
|
|
* PASS 2 walks the lines again; any `stateExtractField(` call whose
|
|
* innermost enclosing function is ladder-bearing (and not
|
|
* function-scoped-exempt) is a violation.
|
|
* Returns [{ line, found }].
|
|
*/
|
|
function buildFunctionInfo(lines) {
|
|
const { detect, braces } = scanCode(lines);
|
|
const ladderBearing = new Set();
|
|
const innermostAt = new Array(lines.length).fill(null);
|
|
const stack = []; // { name, openDepth }
|
|
let depth = 0;
|
|
// A `function NAME(` match whose own line carries no `{` (a multi-line
|
|
// signature — parameters and/or a return-type annotation spilling onto
|
|
// later lines before the body opens) — awaiting the line that actually
|
|
// carries its opening brace. See the header's "FUNCTION ATTRIBUTION"
|
|
// paragraph and this function's own doc comment for why this cannot be
|
|
// pushed onto `stack` immediately: pushing it with `openDepth` recorded
|
|
// BEFORE its own `{` is counted produces a frame whose `openDepth` is the
|
|
// ENCLOSING scope's depth, not its own — for a top-level function that is
|
|
// `openDepth: 0`, and since real code never reaches negative depth, a
|
|
// frame pushed with `openDepth: 0` can NEVER pop (`depth < 0` never
|
|
// fires): it would sit at the bottom of `stack` for the rest of the file,
|
|
// and every subsequent line with no OTHER open frame would be
|
|
// misattributed to it. `ARROW_CONST_RE` never needs this deferral — its
|
|
// pattern requires `=>\s*\{` literally, so it only ever matches on a line
|
|
// that already carries the brace.
|
|
let pendingDeclName = null;
|
|
// LADDER WINDOW: a rolling buffer of every `typeof OPERAND === 'TIER'`
|
|
// clause seen in the last LADDER_WINDOW_LINES lines, oldest-first. On
|
|
// each line, new clauses from that line are appended, then entries older
|
|
// than the window are pruned from the front. A ladder is confirmed the
|
|
// moment the SAME operand has accumulated both a 'number' and a
|
|
// 'boolean' entry inside the buffer — order-independent, and regardless
|
|
// of whether the two clauses came from one `||` chain split across
|
|
// lines, one single-line chain, or two entirely separate `if`
|
|
// statements. See LADDER_WINDOW_LINES's own comment for why this is
|
|
// bounded small and is NOT the same concept as the (unbounded,
|
|
// function-scoped) ladder-to-`stateExtractField(` co-occurrence below.
|
|
const recentClauses = []; // { line, operand, tier }
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const detectCode = detect[i];
|
|
const braceCode = braces[i];
|
|
|
|
let immediateName = null;
|
|
if (detectCode.trim()) {
|
|
const arrowMatch = ARROW_CONST_RE.exec(detectCode);
|
|
if (arrowMatch) {
|
|
immediateName = arrowMatch[1];
|
|
} else {
|
|
const declMatch = FUNCTION_DECL_RE.exec(detectCode);
|
|
// A NEW `function NAME(` match replaces any still-pending name
|
|
// rather than stacking deferrals — this scanner tracks at most one
|
|
// pending declaration at a time (two `function` keywords on
|
|
// consecutive lines with neither closing its signature first is not
|
|
// a shape that occurs in this codebase's style).
|
|
if (declMatch) pendingDeclName = declMatch[1];
|
|
}
|
|
}
|
|
|
|
const opens = (braceCode.match(/\{/g) || []).length;
|
|
const closes = (braceCode.match(/\}/g) || []).length;
|
|
depth += opens - closes;
|
|
|
|
if (immediateName) stack.push({ name: immediateName, openDepth: depth });
|
|
|
|
if (pendingDeclName) {
|
|
if (opens > 0) {
|
|
// First line whose brace count actually increases — its `{` is
|
|
// counted in THIS line's `opens`, so `depth` here correctly
|
|
// reflects "inside the function", matching every other frame's
|
|
// convention (push AFTER updating depth for the pushing line).
|
|
stack.push({ name: pendingDeclName, openDepth: depth });
|
|
pendingDeclName = null;
|
|
} else if (detectCode.includes(';')) {
|
|
// The statement terminated before any `{` appeared — a type-only
|
|
// declaration, an ambient `declare function`, or an overload
|
|
// signature, none of which open a function body. Abandon the
|
|
// pending name rather than stranding it to match a `{` that
|
|
// belongs to unrelated later code. (A `;` inside a string literal
|
|
// on this line would also trigger this — accepted, narrow, known
|
|
// limitation: a default-parameter string containing `;` on the
|
|
// SAME line as an unterminated multi-line signature has not been
|
|
// observed in this codebase.)
|
|
pendingDeclName = null;
|
|
}
|
|
}
|
|
|
|
while (stack.length > 0 && depth < stack[stack.length - 1].openDepth) stack.pop();
|
|
|
|
innermostAt[i] = stack.length > 0 ? stack[stack.length - 1].name : null;
|
|
|
|
if (detectCode.trim()) {
|
|
TYPEOF_TIER_CLAUSE_RE.lastIndex = 0;
|
|
let clauseMatch;
|
|
while ((clauseMatch = TYPEOF_TIER_CLAUSE_RE.exec(detectCode)) !== null) {
|
|
recentClauses.push({ line: i, operand: clauseMatch[1], tier: clauseMatch[2] });
|
|
}
|
|
}
|
|
// Prune clauses that have fallen outside the small bounded ladder window.
|
|
while (recentClauses.length > 0 && recentClauses[0].line <= i - LADDER_WINDOW_LINES) {
|
|
recentClauses.shift();
|
|
}
|
|
|
|
let ladderConfirmed = false;
|
|
if (recentClauses.length > 0) {
|
|
const tiersByOperand = new Map();
|
|
for (const clause of recentClauses) {
|
|
let tiers = tiersByOperand.get(clause.operand);
|
|
if (!tiers) {
|
|
tiers = new Set();
|
|
tiersByOperand.set(clause.operand, tiers);
|
|
}
|
|
tiers.add(clause.tier);
|
|
}
|
|
for (const tiers of tiersByOperand.values()) {
|
|
if (tiers.has('number') && tiers.has('boolean')) {
|
|
ladderConfirmed = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ladderConfirmed) {
|
|
// Transitive: every frame currently open (not just the innermost) is
|
|
// ladder-bearing, because a nested closure's body is lexically part of
|
|
// every one of its enclosing functions' own bodies.
|
|
for (const frame of stack) ladderBearing.add(frame.name);
|
|
}
|
|
}
|
|
return { ladderBearing, innermostAt, detect };
|
|
}
|
|
|
|
// ─── PROMPT-LAYER PROSE DETECTION (ADR-3180 Decision 4(d)) ─────────────────
|
|
//
|
|
// The SAME #1760 fallback chain — "prefer the frontmatter scalar, else fall
|
|
// back to the body field" — expressed as MARKDOWN PROSE describing the
|
|
// derivation to an agent, rather than TypeScript re-deriving it. Detection is
|
|
// intentionally narrow, mirroring `lint-planning-prompt-drift.cjs`'s own
|
|
// precedent: a markdown line is a prose re-derivation only when it carries
|
|
// ALL FOUR, on the SAME line:
|
|
// (a) a backtick-quoted YAML-style frontmatter key token — a lowercase
|
|
// identifier immediately followed by a colon, inside backticks, e.g.
|
|
// `` `status:` ``. `FRONTMATTER_KEY_TOKEN_RE`.
|
|
// (b) a backtick-quoted Markdown BOLD body-field token, e.g.
|
|
// `` `**Status:**` ``. `BODY_BOLD_TOKEN_RE`.
|
|
// (c) the word "frontmatter" (case-insensitive, word-bounded) anywhere on
|
|
// the line.
|
|
// (d) the word "body" (case-insensitive, word-bounded) anywhere on the
|
|
// line.
|
|
// (c) and (d) are what distinguish a line DESCRIBING an fm-then-body
|
|
// PRECEDENCE from a line that merely happens to carry two backtick-quoted
|
|
// tokens shaped like (a) and (b) for unrelated reasons — both words are
|
|
// required so an incidental co-occurrence (e.g. a table row naming an
|
|
// unrelated frontmatter key and, several columns over, an unrelated bold
|
|
// body label) cannot false-positive on tokens alone.
|
|
// All four regexes are small, bounded, single fixed character classes with
|
|
// no nested/overlapping quantifiers — nothing for a backtracking engine to
|
|
// explore (`npm run lint:ci` runs CodeQL js/redos over this repo, the same
|
|
// discipline `findStateFieldDrift`'s own regexes document above).
|
|
const FRONTMATTER_KEY_TOKEN_RE = /`[a-z][a-z0-9_]*:`/;
|
|
const BODY_BOLD_TOKEN_RE = /`\*\*[^*`]{1,80}\*\*`/;
|
|
const FRONTMATTER_WORD_RE = /\bfrontmatter\b/i;
|
|
const BODY_WORD_RE = /\bbody\b/i;
|
|
|
|
// Per ADR-3180 Decision 4(d)/(e): `gsd-core/workflows/smart-entry.md`'s
|
|
// Fallback step 1 ("`gsd-tools` itself is broken") is a PERMANENT, by-
|
|
// construction exemption — NOT debt with an owner, and therefore NOT
|
|
// modelled as `lint-planning-prompt-drift.cjs`'s shrink-only ratchet
|
|
// baseline (which exists specifically to acknowledge sites with a removal
|
|
// issue, per Decision 4(e)). This site can never be migrated onto
|
|
// `src/state-document.cjs`'s `stateFieldValue`: the surrounding step exists
|
|
// PRECISELY for when the CLI itself cannot run (`Cannot find module ...` /
|
|
// Node crash — probed one line above this one), so by construction it cannot
|
|
// call the canonical owner it is standing in for. Fabricating a "removal
|
|
// issue" for something that can never be removed would misrepresent it as
|
|
// ratchetable debt it is not.
|
|
//
|
|
// Keyed on (file, TRIMMED source text) — never a line number, which churns
|
|
// on any unrelated edit to the same file — mirroring the ratchet's own key
|
|
// shape (`lint-planning-prompt-drift.cjs`) even though this exemption is not
|
|
// itself a ratchet. `file` uses the SAME native-separator relPath shape as
|
|
// `OWNER_FILE`/`FUNCTION_SCOPED_EXEMPTIONS` above (this guard does not
|
|
// POSIX-normalize elsewhere, so this exemption does not either, for
|
|
// consistency within the one file).
|
|
const PROMPT_LAYER_EXEMPTIONS = new Map([
|
|
[
|
|
path.join('gsd-core', 'workflows', 'smart-entry.md'),
|
|
new Map([
|
|
[
|
|
"- Read `.planning/STATE.md` (frontmatter + body) with the Read tool. Extract: `status` (frontmatter `status:` or body `**Status:**`), `Phase:` from the body, `total_phases`/`percent` from a nested `progress:` frontmatter object if present, and any `## Blockers` items.",
|
|
'gsd-tools-down fallback (smart-entry.md Fallback step 1): runs only when gsd-tools itself cannot run, so it cannot call the canonical owner it substitutes for — permanent by construction, not removable debt.',
|
|
],
|
|
]),
|
|
],
|
|
]);
|
|
|
|
/**
|
|
* Pure: find every prose re-derivation of the STATE.md field-extraction
|
|
* fallback chain in a markdown file's `text`. Returns `[{ line, found }]` for
|
|
* every UNEXEMPTED match — `relPath` (native-separator, matching
|
|
* `PROMPT_LAYER_EXEMPTIONS`'s keys) is consulted only for the exemption
|
|
* lookup, exactly as `findStateFieldDrift` consults it for
|
|
* `FUNCTION_SCOPED_EXEMPTIONS`.
|
|
*/
|
|
function findPromptFieldDrift(text, relPath) {
|
|
const out = [];
|
|
const lines = text.split('\n');
|
|
const exemptTexts = PROMPT_LAYER_EXEMPTIONS.get(relPath) || null;
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
if (!FRONTMATTER_KEY_TOKEN_RE.test(line)) continue;
|
|
if (!BODY_BOLD_TOKEN_RE.test(line)) continue;
|
|
if (!FRONTMATTER_WORD_RE.test(line)) continue;
|
|
if (!BODY_WORD_RE.test(line)) continue;
|
|
const trimmed = line.trim();
|
|
if (exemptTexts && exemptTexts.has(trimmed)) continue;
|
|
out.push({ line: i + 1, found: trimmed.slice(0, MAX_REGEX_LITERAL_LEN) });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function findStateFieldDrift(text, relPath) {
|
|
const out = [];
|
|
const lines = text.split('\n');
|
|
const exemptFunctions = FUNCTION_SCOPED_EXEMPTIONS.get(relPath) || null;
|
|
const { ladderBearing, innermostAt, detect } = buildFunctionInfo(lines);
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const detectCode = detect[i];
|
|
if (!detectCode.trim()) continue;
|
|
if (!STATE_EXTRACT_FIELD_CALL_RE.test(detectCode)) continue;
|
|
|
|
const fn = innermostAt[i];
|
|
if (!fn || !ladderBearing.has(fn)) continue;
|
|
if (exemptFunctions && exemptFunctions.has(fn)) continue;
|
|
|
|
out.push({ line: i + 1, found: lines[i].trim().slice(0, MAX_REGEX_LITERAL_LEN) });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Scan the authored source tree (TypeScript under `src/`, prose in the
|
|
* prompt-layer markdown — ADR-3180 Decision 4(d)) and return every
|
|
* unsanctioned re-derivation, each annotated with the repo-relative file
|
|
* path. Dispatches by extension: `.md` files run the prose detector
|
|
* (`findPromptFieldDrift`), everything else (`.cts`/`.ts`/`.mts`) runs the
|
|
* code detector (`findStateFieldDrift`) — the two derivations are expressed
|
|
* in different languages and need different detection shapes over the same
|
|
* scan surface.
|
|
*/
|
|
function scanRepo(root) {
|
|
return scanTree({
|
|
root,
|
|
scanDirs: SCAN_DIRS,
|
|
scanExt: SCAN_EXT,
|
|
onFile(rel, text) {
|
|
// `rel` is already the REAL (canonical) path (scanTree resolves
|
|
// symlinks before calling onFile), so this — and
|
|
// FUNCTION_SCOPED_EXEMPTIONS/PROMPT_LAYER_EXEMPTIONS above, also keyed
|
|
// on `rel` — match consistently regardless of which symlink reached
|
|
// the file.
|
|
const finder = path.extname(rel) === '.md' ? findPromptFieldDrift : findStateFieldDrift;
|
|
return finder(text, rel).map((d) => ({ file: rel, ...d }));
|
|
},
|
|
});
|
|
}
|
|
|
|
function main() {
|
|
const root = path.join(__dirname, '..');
|
|
const violations = scanRepo(root);
|
|
if (violations.length === 0) {
|
|
process.stdout.write('ok state-field-drift: no unsanctioned STATE.md field-extraction fallback-chain re-derivations found\n');
|
|
return;
|
|
}
|
|
process.stderr.write('state-field-drift: independent re-derivation(s) of the STATE.md frontmatter-else-body field\n');
|
|
process.stderr.write('fallback chain found. Route these call sites through src/state-document.cjs\n');
|
|
process.stderr.write('`stateFieldValue` (issue #3187, ADR-3180 §7.7) instead of re-deriving the ladder:\n');
|
|
for (const d of violations) {
|
|
// `d.file` is exactly as attacker-controlled as `d.found`: a repo can
|
|
// legally track a filename containing control bytes / bidi overrides,
|
|
// and it is a fork-PR-authored value reaching a CI log the same way the
|
|
// matched line text does — sanitize it at the same reporting boundary.
|
|
process.stderr.write(` ${sanitizeForReport(d.file)}:${d.line} ${sanitizeForReport(d.found)}\n`);
|
|
}
|
|
process.exitCode = 1;
|
|
}
|
|
|
|
if (require.main === module) main();
|
|
|
|
module.exports = {
|
|
findStateFieldDrift,
|
|
findPromptFieldDrift,
|
|
buildFunctionInfo,
|
|
scanRepo,
|
|
OPERAND_SOURCE,
|
|
TYPEOF_TIER_CLAUSE_RE,
|
|
LADDER_WINDOW_LINES,
|
|
STATE_EXTRACT_FIELD_CALL_RE,
|
|
FUNCTION_DECL_RE,
|
|
ARROW_CONST_RE,
|
|
OWNER_FILE,
|
|
FUNCTION_SCOPED_EXEMPTIONS,
|
|
FRONTMATTER_KEY_TOKEN_RE,
|
|
BODY_BOLD_TOKEN_RE,
|
|
PROMPT_LAYER_EXEMPTIONS,
|
|
SCAN_DIRS,
|
|
SCAN_EXT,
|
|
scanCode,
|
|
MAX_REGEX_LITERAL_LEN,
|
|
};
|