border-customs 0.1.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +256 -40
- package/dist/assets/residue-src/crates.ts +228 -0
- package/dist/assets/residue-src/npm.ts +294 -0
- package/dist/assets/residue-src/pypi.ts +333 -0
- package/dist/assets/residue-src/residue.ts +348 -0
- package/dist/assets/residue-src/residueGem.ts +371 -0
- package/dist/assets/residue-src/residueMatchers.ts +417 -0
- package/dist/assets/residue-src/residuePy.ts +26 -0
- package/dist/assets/residue-src/residueRust.ts +252 -0
- package/dist/assets/residue-src/rubygems.ts +278 -0
- package/dist/index.js +5126 -2825
- package/package.json +6 -2
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
// provenance: RESIDUE-CONTRACT.md §1.6 (T1 signature), §4 (family tables + argv-token lesson),
|
|
2
|
+
// §5.3 (marker grammar) — .omo/plans/border-residue-gate.md R2, plan "Must have" signature
|
|
3
|
+
// table. THIS FILE IS THE ONLY home of the residue-* rule ids and pattern families (grep AC 5);
|
|
4
|
+
// classifier logic lives in src/artifacts/residue.ts. Closed lists by design: config can never
|
|
5
|
+
// widen them (plan Must-NOT). Every pattern here has at least one hittable fixture proven by
|
|
6
|
+
// R1 §4 (test/fixtures/residue/) — do not add families without one.
|
|
7
|
+
import type { Severity } from "../findings.ts";
|
|
8
|
+
|
|
9
|
+
// ------------------------------------------- rule ids (single closed table)
|
|
10
|
+
|
|
11
|
+
export const RESIDUE_T1_RULE = "residue-in-tree-hook";
|
|
12
|
+
export const RESIDUE_T2_RULE = "residue-install-download";
|
|
13
|
+
export const RESIDUE_T3_RULE = "residue-out-of-tree-write";
|
|
14
|
+
export const RESIDUE_XMGR_RULE = "residue-cross-manager-write";
|
|
15
|
+
export const RESIDUE_PAIRING_RULE = "residue-pairing-missing";
|
|
16
|
+
export const RESIDUE_T4_RULE = "residue-persistence-primitive";
|
|
17
|
+
// R3b (plan R3b AC "unmatchable extension build ⇒ exactly one MEDIUM note, never silent clean"):
|
|
18
|
+
// no existing id carries this semantics (T1 is the in-tree-hook badge, PAIRING the B2 marker lane),
|
|
19
|
+
// so the plan-intent id is appended here — the single home — severity MEDIUM per the plan tier table.
|
|
20
|
+
export const RESIDUE_GEM_UNMATCHED_EXT_RULE = "residue-gem-unmatched-extension";
|
|
21
|
+
|
|
22
|
+
export const RESIDUE_SEVERITIES: Readonly<Record<string, Severity>> = {
|
|
23
|
+
[RESIDUE_T1_RULE]: "MEDIUM",
|
|
24
|
+
[RESIDUE_T2_RULE]: "HIGH",
|
|
25
|
+
[RESIDUE_T3_RULE]: "HIGH",
|
|
26
|
+
[RESIDUE_XMGR_RULE]: "HIGH",
|
|
27
|
+
[RESIDUE_PAIRING_RULE]: "HIGH",
|
|
28
|
+
[RESIDUE_T4_RULE]: "CRITICAL",
|
|
29
|
+
[RESIDUE_GEM_UNMATCHED_EXT_RULE]: "MEDIUM",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// ------------------------------------------- T1: closed signature #1 (contract §1.6)
|
|
33
|
+
|
|
34
|
+
export const RESIDUE_T1_SIGNATURE_ID = "opencode-in-tree-v1";
|
|
35
|
+
|
|
36
|
+
/** Clause 1 — hook grammar: EXACTLY `node ./<name>.mjs` (adjudication #4: ESM + ./ only;
|
|
37
|
+
* CJS `node install.js` stays resolvable-for-scan but is NEVER a T1 lane, §6-4). */
|
|
38
|
+
export const T1_HOOK_GRAMMAR = /^node \.\/([A-Za-z0-9._-]+\.mjs)$/;
|
|
39
|
+
|
|
40
|
+
/** Resolvable-hook grammar for the per-file family scan (plan "the classifier reads ≤2 files
|
|
41
|
+
* per hook"): single command, optional .\, known interpreter, path-safe token. Anything else
|
|
42
|
+
* (shell metachars, multi-hop, bare package specifiers) is UNKNOWN ⇒ caller keeps CRITICAL. */
|
|
43
|
+
export const RESOLVABLE_HOOK = /^(?:node|bash|sh) \.\/?([A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*\.(?:m?js|cjs|sh))$/;
|
|
44
|
+
/** Shell metachars void resolvability — a hook string carrying any of these is UNKNOWN. */
|
|
45
|
+
const HOOK_METACHARS = /[;&|`$><\n"'\\()]/;
|
|
46
|
+
export function isPlainHookString(hook: string): boolean {
|
|
47
|
+
return !HOOK_METACHARS.test(hook);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Clause 2 — closed import allowlist (exact specifiers; `node:` prefix stripped).
|
|
51
|
+
* `url` added per adjudication #2 / §6-2; child_process bounded by clauses 4+6 instead. */
|
|
52
|
+
export const T1_ALLOWED_MODULES: readonly string[] = ["child_process", "fs", "module", "os", "path", "url"];
|
|
53
|
+
|
|
54
|
+
/** Clause 8 — cap: the hook target is one file, ≤300 lines (R1 threshold from §1.2). */
|
|
55
|
+
export const T1_LINE_CAP = 300;
|
|
56
|
+
|
|
57
|
+
/** Clause 3/5/6/7 — frozen literals the opencode anatomy carries (whitespace-normalized
|
|
58
|
+
* substrings; adjudication #1: the network-fallback argv and the verify-binary spawn are
|
|
59
|
+
* embedded here as REQUIRED literals — a future opencode that drops them exits T1). */
|
|
60
|
+
export const T1_REQUIRED_LITERALS: readonly string[] = [
|
|
61
|
+
// __dirname anchor + CJS interop + host probes
|
|
62
|
+
'const __dirname = path.dirname(fileURLToPath(import.meta.url))',
|
|
63
|
+
'createRequire(import.meta.url)',
|
|
64
|
+
'"/proc/cpuinfo"',
|
|
65
|
+
'"/etc/alpine-release"',
|
|
66
|
+
// platform/arch maps + package naming (§1.6-7)
|
|
67
|
+
'darwin: "darwin"',
|
|
68
|
+
'linux: "linux"',
|
|
69
|
+
'win32: "windows"',
|
|
70
|
+
'x64: "x64"',
|
|
71
|
+
'arm64: "arm64"',
|
|
72
|
+
'opencode-${platform}-${arch}',
|
|
73
|
+
'-baseline',
|
|
74
|
+
'-musl',
|
|
75
|
+
// in-tree write roots + hardlink trio (§1.6-5/6)
|
|
76
|
+
'path.join(__dirname, "bin", "opencode.exe")',
|
|
77
|
+
'fs.linkSync(source, target)',
|
|
78
|
+
'fs.copyFileSync(source, target)',
|
|
79
|
+
'fs.chmodSync(target, 0o755)',
|
|
80
|
+
// fallback temp root: created under os.tmpdir(), deleted in finally (§1.6-5)
|
|
81
|
+
'fs.mkdtempSync(path.join(os.tmpdir(), "opencode-install-"))',
|
|
82
|
+
'fs.rmSync(temp, { recursive: true, force: true })',
|
|
83
|
+
// frozen spawn argv (§1.6-4, adjudication #1/#3)
|
|
84
|
+
'spawnSync("sysctl", ["-n", "hw.optional.avx2_0"]',
|
|
85
|
+
'spawnSync("ldd", ["--version"]',
|
|
86
|
+
'["powershell.exe", "pwsh.exe", "pwsh", "powershell"]',
|
|
87
|
+
'["-NoProfile", "-NonInteractive", "-Command"',
|
|
88
|
+
'IsProcessorFeaturePresent(40)',
|
|
89
|
+
'"npm", ["install", "--ignore-scripts", "--no-save", "--loglevel=error", "--prefix"',
|
|
90
|
+
'spawnSync(targetBinary, ["--version"]',
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
/** §1.6-4 — spawn argv[0] closed set: string literals the anatomy may spawn… */
|
|
94
|
+
export const T1_SPAWN_ARGV0_LITERALS: readonly string[] = [
|
|
95
|
+
"ldd",
|
|
96
|
+
"npm",
|
|
97
|
+
"powershell",
|
|
98
|
+
"powershell.exe",
|
|
99
|
+
"pwsh",
|
|
100
|
+
"pwsh.exe",
|
|
101
|
+
"sysctl",
|
|
102
|
+
];
|
|
103
|
+
/** …plus identifier-bound spawns, each tied to the frozen definition that proves its target. */
|
|
104
|
+
export const T1_SPAWN_ARGV0_IDENTIFIERS: ReadonlyArray<{ readonly ident: string; readonly proof: string }> = [
|
|
105
|
+
{ ident: "targetBinary", proof: 'path.join(__dirname, "bin", "opencode.exe")' },
|
|
106
|
+
{ ident: "executable", proof: '["powershell.exe", "pwsh.exe", "pwsh", "powershell"]' },
|
|
107
|
+
];
|
|
108
|
+
/** §1.6-4 spawn call shapes to enumerate argv0 positions (regex over normalized text).
|
|
109
|
+
* NB3 (VERIFIER-REPORT-R2-FIX3): alternation = the FULL child_process surface, longer names first.
|
|
110
|
+
* The argv0 capture also WIDENS to any-quoted string content: the old narrow class
|
|
111
|
+
* [A-Za-z0-9._-]+ silently skipped quoted commands containing spaces or `/` —
|
|
112
|
+
* `exec("echo pwned > ../../escape.txt")` never reached the allowlist check at all.
|
|
113
|
+
* No leading `\b` on purpose: 'myexecSync("evil")' still presents its argv0 to the check. */
|
|
114
|
+
export const T1_SPAWN_CALL = /(?:spawnSync|spawn|execFileSync|execFile|execSync|exec|fork)\(\s*(?:["'`]([^"'`]*)["'`]|([A-Za-z_$][\w$]*))/g;
|
|
115
|
+
/** NB3 (§1.6-9): spawn verbs join the call-position doctrine — `const ex = cp.exec; ex(…)`
|
|
116
|
+
* removes the verb from call shape; word-bounded so 'executable'/'opencode.exe'/'spawns' never match. */
|
|
117
|
+
export const T1_SPAWN_NAME = /\b(?:spawnSync|spawn|execFileSync|execFile|execSync|exec|fork)\b/g;
|
|
118
|
+
/** Import/require specifier shapes for clause 2 (literal specifiers only — the template-literal
|
|
119
|
+
* require.resolve of own optionalDependencies is §1.6-3 and invisible here by design). */
|
|
120
|
+
export const T1_MODULE_REF = /(?:import|export)[^;\n]*?\bfrom\s+["']([^"']+)["']|\brequire\(\s*["']([^"']+)["']\s*\)|(?:^|[;\n])\s*import\s+["']([^"']+)["']/g;
|
|
121
|
+
/** §1.6-2 clause 2 is EXHAUSTIVE ("nothing else"): B1 fix — dynamic `import(...)` / `require(...)`
|
|
122
|
+
* call shapes in ANY quoting form (backticks included) must also be enumerated. `require.resolve(`
|
|
123
|
+
* and `import.meta` never match (the `\(` anchor excludes the dot), keeping §1.6-3 one-hop
|
|
124
|
+
* resolution invisible here by design. evaluateT1 voids T1 unless the captured specifier is a
|
|
125
|
+
* plain '…'/"…" string literal naming an allowlisted module — templates, identifiers, and
|
|
126
|
+
* concatenations are all unrecognized shapes ⇒ fail closed. */
|
|
127
|
+
export const T1_DYNAMIC_REF = /\b(?:import|require)\s*\(([^)]*)\)/g;
|
|
128
|
+
/** §1.6-5 (B2 fix): EVERY write-family call site in a T1 candidate is enumerated — write/append/
|
|
129
|
+
* mkdir/unlink/rm/copyFile/link/chmod/rename/symlink, sync or async form, with or without an
|
|
130
|
+
* `fs.`/`fs.promises.` receiver (`\b` stops 'confirm(' but matches '.rm('). `mkdtemp` is
|
|
131
|
+
* deliberately absent: its only legal occurrence is pinned in T1_REQUIRED_LITERALS.
|
|
132
|
+
* Round 4: `(?:Sync)?` — the old `Sync?` quantified only 'c', so bare async forms
|
|
133
|
+
* (fs.writeFile/fs.rm) escaped this enumeration entirely (R2-M11; golden has zero bare tokens). */
|
|
134
|
+
export const T1_WRITE_CALL = /\b(?:writeFile|appendFile|mkdir|unlink|rm|copyFile|link|chmod|rename|symlink)(?:Sync)?\s*\(/g;
|
|
135
|
+
/** Closed first-argument anatomy observed at the frozen fixture's write sites — the copyBinary
|
|
136
|
+
* (source, target) parameters, the finally-cleaned mkdtemp handle, and the dirname-of-target
|
|
137
|
+
* mkdir parent. Anything else (path.join escapes like (__dirname,'..','..'), unbound identifiers)
|
|
138
|
+
* is an unrecognized shape ⇒ evaluateT1 returns null. Whitespace is stripped before comparison. */
|
|
139
|
+
export const T1_WRITE_ARGV0_FORMS: readonly string[] = ["source", "target", "temp", "path.dirname(target)"];
|
|
140
|
+
/** NB2 (§1.6-9): every WRITE-FAMILY NAME OCCURRENCE — not just call sites — enumerated.
|
|
141
|
+
* The name set can over-match comments/strings (verifier nit N3): prose `rm(` or a quoted
|
|
142
|
+
* "link" costs the T1 badge, never safety — voiding is the fail-CLOSED direction by design. */
|
|
143
|
+
export const T1_WRITE_NAME = /\b(?:writeFile|appendFile|mkdir|unlink|rm|copyFile|link|chmod|rename|symlink)(?:Sync)?\b/g;
|
|
144
|
+
/** NB2+NB3 (§1.6-9): alias binding (`const w = fs.writeFileSync; w(…)`, `const ex = cp.exec`) hides a
|
|
145
|
+
* verb from call-site enumeration, so the stricter posture holds for BOTH name sets: EVERY occurrence
|
|
146
|
+
* in the normalized text must be IMMEDIATELY followed by `(` (a direct call). Alias bindings (`;`),
|
|
147
|
+
* destructured inits (`}`), arg-passing (`,`/`)`), and property/`[`-string refs all void T1.
|
|
148
|
+
* Blessed anatomy verified: 6 write tokens + 5 spawnSync tokens, each in direct call position,
|
|
149
|
+
* zero bare forms, zero \bexec\b/\bfork\b ⇒ zero cost. */
|
|
150
|
+
export function verbsAllCalled(namePattern: RegExp, norm: string): boolean {
|
|
151
|
+
for (const m of norm.matchAll(namePattern)) {
|
|
152
|
+
if (!norm.startsWith("(", (m.index ?? 0) + m[0].length)) return false;
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
/** NB1 (§1.6-9): textual matchers cannot decide eval-equivalence — `eval("im"+"port")("node:net")`
|
|
157
|
+
* never presents an import( token to the clause-2 enumeration. The blessed anatomy contains ZERO
|
|
158
|
+
* metaprogramming (verifier-measured), so any of these three marks voids T1 at no golden cost;
|
|
159
|
+
* the residual obfuscation families are stated honestly in the contract, not papered over. */
|
|
160
|
+
export const T1_METAPROGRAMMING = /\beval\s*\(|\bnew\s+Function\b|\bglobalThis\s*\[/;
|
|
161
|
+
/** §1.6-5 — literals that would place a path outside the package root. The blessed anatomy
|
|
162
|
+
* contains NONE; probe paths are exact literals, everything else starting with `/` voids T1. */
|
|
163
|
+
export const T1_ABS_PATH_LITERAL = /"\/([^"]*)"/g;
|
|
164
|
+
export const T1_ALLOWED_ABS_PATHS: readonly string[] = ["proc/cpuinfo", "etc/alpine-release"];
|
|
165
|
+
export const T1_FORBIDDEN_USER_PATH_MARKS: readonly string[] = [
|
|
166
|
+
"os.homedir(",
|
|
167
|
+
"process.env.HOME",
|
|
168
|
+
"USERPROFILE",
|
|
169
|
+
"APPDATA",
|
|
170
|
+
"XDG_",
|
|
171
|
+
"process.cwd(", // NB2 (verifier nit N5): cwd-relative escapes like path.join(process.cwd(),"..",…)
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
// ------------------------------------------- T2/T3/T4/cross-manager families (contract §4)
|
|
175
|
+
|
|
176
|
+
export type FamilyPattern = { readonly id: string; readonly rule: string; readonly pattern: RegExp };
|
|
177
|
+
|
|
178
|
+
/** §4 lesson: EVERY command family carries both the shell-text and the argv-token form
|
|
179
|
+
* (the first t3-git-global draft scored 0 on `["config", "--global", …]` — never again). */
|
|
180
|
+
export const FAMILY_PATTERNS: readonly FamilyPattern[] = [
|
|
181
|
+
// T2 — install-time network download / fetch of remote bytes
|
|
182
|
+
// B1 fix (b): the static arm above never sees `import("node:net")` or backtick specifiers; the
|
|
183
|
+
// dynamic arm catches (require|import)( + any of " ' ` + an unambiguous net-module token or a
|
|
184
|
+
// `${` interpolation (unknown computed specifier — fail closed).
|
|
185
|
+
{ id: "t2-js-net-module", rule: RESIDUE_T2_RULE, pattern: /(?:require|import)[^;\n]*\bfrom?\s*\(?\s*["'](?:node:)?(?:https?|net|dns)["']|\b(?:import|require)\s*\(\s*["'`](?:node:)?(?:https?|net|dns|\$\{)/ },
|
|
186
|
+
{ id: "t2-js-fetch", rule: RESIDUE_T2_RULE, pattern: /\bfetch\s*\(/ },
|
|
187
|
+
{ id: "t2-curl-wget", rule: RESIDUE_T2_RULE, pattern: /\b(?:curl|wget)(?:\s+|["']\s*,)["' \-]*[^\n]*(?:http:\/\/|https:\/\/|-o\b)/ },
|
|
188
|
+
{ id: "t2-pwsh-download", rule: RESIDUE_T2_RULE, pattern: /Invoke-WebRequest|Invoke-RestMethod/i },
|
|
189
|
+
{ id: "t2-py-net", rule: RESIDUE_T2_RULE, pattern: /\b(?:urllib\.request|urllib\.urlopen|requests\.(?:get|post)|httpx)\b/ },
|
|
190
|
+
{ id: "t2-rs-reqwest", rule: RESIDUE_T2_RULE, pattern: /\b(?:reqwest|ureq)\b/ },
|
|
191
|
+
// R3a: a Cargo.toml DEPENDENCY DECLARATION (`name = "ver"`) — only ever run over an
|
|
192
|
+
// extracted [..build-dependencies] block, never over whole manifests (runtime deps are T0).
|
|
193
|
+
{ id: "t2-rs-builddep", rule: RESIDUE_T2_RULE, pattern: /^\s*(?:reqwest|ureq|curl)\s*=/m },
|
|
194
|
+
{ id: "t2-rb-net", rule: RESIDUE_T2_RULE, pattern: /Net::HTTP|open-uri/ },
|
|
195
|
+
// T3 — writes outside the package root
|
|
196
|
+
{ id: "t3-dotfile", rule: RESIDUE_T3_RULE, pattern: /\.(?:bashrc|zshrc|zshenv|profile)(?![-\w])|\/etc\/profile\.d|authorized_keys|\.gitconfig/ },
|
|
197
|
+
{ id: "t3-git-global", rule: RESIDUE_T3_RULE, pattern: /git\s+config\s+--global|--global["']\s*,|["']\.gitconfig["']/ },
|
|
198
|
+
{ id: "t3-path-export", rule: RESIDUE_T3_RULE, pattern: /export\s+PATH\s*=|setx\s+(?:\/F\s+)?PATH\b|"setx"[^)\n]*"PATH"|SetEnvironmentVariable\s*\(|HKCU:?[\\]{1,2}Environment/ },
|
|
199
|
+
{ id: "t3-win-user-tree", rule: RESIDUE_T3_RULE, pattern: /%APPDATA%|\$env:USERPROFILE|USERPROFILE|\\AppData\\/ },
|
|
200
|
+
// R3a (plan §57): a setup.py that resolves user-home paths is out-of-tree posture — no T1 lane forgives it.
|
|
201
|
+
{ id: "t3-py-expanduser", rule: RESIDUE_T3_RULE, pattern: /\b(?:os\.path\.expanduser|Path\.home)\s*\(|\.expanduser\s*\(/ },
|
|
202
|
+
// T4 — OS persistence primitives, artifact-wide, ANY reference (executed or not)
|
|
203
|
+
{ id: "t4-cron", rule: RESIDUE_T4_RULE, pattern: /crontab/ },
|
|
204
|
+
{ id: "t4-systemd-user", rule: RESIDUE_T4_RULE, pattern: /systemctl(?:\s+|["']\s*,\s*["'])--user|systemd\/user/ },
|
|
205
|
+
{ id: "t4-mac", rule: RESIDUE_T4_RULE, pattern: /LaunchAgents|launchctl/ },
|
|
206
|
+
{ id: "t4-autostart", rule: RESIDUE_T4_RULE, pattern: /\.config\/autostart/ },
|
|
207
|
+
{ id: "t4-win", rule: RESIDUE_T4_RULE, pattern: /schtasks|CurrentVersion\\+Run/i },
|
|
208
|
+
// cross-manager — spawning a FOREIGN package manager at install time (aihr incident shape).
|
|
209
|
+
// npm itself is NOT foreign here; a global-scope npm write is (ledger re-convergence).
|
|
210
|
+
{ id: "xmgr-pip", rule: RESIDUE_XMGR_RULE, pattern: /\bpip install\b|["']pip["']\s*[,\]]|easy_install/ },
|
|
211
|
+
{ id: "xmgr-python-pip", rule: RESIDUE_XMGR_RULE, pattern: /python[\w.-]*\s+-m\s+pip|"python[3]?",\s*\[\s*"pip"/ },
|
|
212
|
+
{ id: "xmgr-npm-global", rule: RESIDUE_XMGR_RULE, pattern: /npm (?:install|i|add)\s+-g\b|npm\s+install\s+--global|"npm"[^)\n]*"(?:-g|--global)"/ },
|
|
213
|
+
{ id: "xmgr-yarn", rule: RESIDUE_XMGR_RULE, pattern: /yarn\s+global|["']yarn["']\s*,\s*["'](?:global|add)["']/ },
|
|
214
|
+
{ id: "xmgr-pnpm", rule: RESIDUE_XMGR_RULE, pattern: /pnpm\s+add\s+-g|["']pnpm["']\s*,\s*\[[^\]]*"add"/ },
|
|
215
|
+
{ id: "xmgr-gem", rule: RESIDUE_XMGR_RULE, pattern: /\bgem install\b|["']gem["']\s*,\s*\[[^\]]*"install"/ },
|
|
216
|
+
{ id: "xmgr-cargo", rule: RESIDUE_XMGR_RULE, pattern: /\bcargo install\b|["']cargo["']\s*,\s*\[[^\]]*"install"/ },
|
|
217
|
+
{ id: "xmgr-uv", rule: RESIDUE_XMGR_RULE, pattern: /\buv pip\b|["']uv["']\s*,\s*\[[^\]]*"pip"/ },
|
|
218
|
+
// R3a: Rust's argv-token manager form — `std::process::Command::new("pip")` etc. in a build script.
|
|
219
|
+
{ id: "xmgr-rs-command", rule: RESIDUE_XMGR_RULE, pattern: /Command::new\s*\(\s*["'](?:pip|pip3|conda|npm|npx|yarn|pnpm|gem|cargo|uv)["']/ },
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
/** Manager labels named in the residue-cross-manager-write message (plan: "the message names
|
|
223
|
+
* the foreign ledger"). Keyed by the t2/xmgr family ids above. */
|
|
224
|
+
export const XMGR_LABELS: Readonly<Record<string, string>> = {
|
|
225
|
+
"xmgr-pip": "pip",
|
|
226
|
+
"xmgr-python-pip": "python -m pip",
|
|
227
|
+
"xmgr-npm-global": "npm -g (global install)",
|
|
228
|
+
"xmgr-yarn": "yarn global",
|
|
229
|
+
"xmgr-pnpm": "pnpm add -g",
|
|
230
|
+
"xmgr-gem": "gem install",
|
|
231
|
+
"xmgr-cargo": "cargo install",
|
|
232
|
+
"xmgr-uv": "uv pip",
|
|
233
|
+
"xmgr-rs-command": "foreign manager via Rust Command::new",
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// ------------------------------------------- R3a pypi/crates legs (plan §57: closed per-language tables)
|
|
237
|
+
|
|
238
|
+
/** Files whose packed presence marks a pypi build hook (setup.py + inline cmdclass bodies live there). */
|
|
239
|
+
export const PY_BUILD_HOOK_BASENAMES: readonly string[] = ["setup.py"];
|
|
240
|
+
/** Cargo runs build.rs on every consumer build — the crates analogue of an npm lifecycle hook. */
|
|
241
|
+
export const RS_BUILD_HOOK_BASENAMES: readonly string[] = ["build.rs"];
|
|
242
|
+
/** TOML section headers whose bodies are install-time code: pyproject's backend declaration… */
|
|
243
|
+
export const PY_BUILD_SYSTEM_SECTION = /^\[build-system\]\s*$/;
|
|
244
|
+
/** …and cargo's build-dependency sections ([build-dependencies], [target.*.build-dependencies], [dev-*] excluded). */
|
|
245
|
+
export const RS_BUILD_DEPS_SECTION = /^\[(?:build-dependencies|target\.[^\]]+\.build-dependencies)\]\s*$/;
|
|
246
|
+
/** §57 names exactly these two write verbs; anything else is out of the closed set (documented limitation). */
|
|
247
|
+
export const RS_WRITE_CALL = /\b(?:fs::write|File::create)\s*\(/g;
|
|
248
|
+
/** A build.rs write target must be confined to these cargo-provided roots (directly or via one `let` hop). */
|
|
249
|
+
export const RS_CONFINEMENT_ENV_TOKENS: readonly string[] = ["OUT_DIR", "CARGO_TARGET_TMPDIR"];
|
|
250
|
+
|
|
251
|
+
// ------------------------------------------- B2 marker grammar (contract §5.3, amendment D3)
|
|
252
|
+
|
|
253
|
+
/** id-path := token (SP token){0,3}, token = [A-Za-z0-9][A-Za-z0-9._-]{0,63}; the parenthetical
|
|
254
|
+
* is a provenance note, NOT part of the id. Matching pairs on the id-path BYTES. */
|
|
255
|
+
export const MARKER_ID_TOKEN = String.raw`[A-Za-z0-9][A-Za-z0-9._-]{0,63}`;
|
|
256
|
+
export const MARKER_BEGIN = new RegExp(String.raw`# BEGIN (${MARKER_ID_TOKEN}(?: ${MARKER_ID_TOKEN}){0,3})(?: \([^)]{0,56}\))?`);
|
|
257
|
+
export const MARKER_END = new RegExp(String.raw`# END (${MARKER_ID_TOKEN}(?: ${MARKER_ID_TOKEN}){0,3})(?: \([^)]{0,56}\))?`);
|
|
258
|
+
|
|
259
|
+
/** §5.3(i) write-path: a marker-bearing file that APPENDS or string-CONCATENATES into a write
|
|
260
|
+
* call. `write_text(text.replace(...))` (a removal) deliberately does NOT match: [^)]*\+ fails
|
|
261
|
+
* before the inner close-paren. Remove-orphan files contain neither form. */
|
|
262
|
+
export const MARKER_WRITE_SHAPE = /\.open\(\s*[^)]*["']a["']|appendFile|Add-Content|\.write(?:_text|File|Sync)?\(\s*[^)]*\+|>>\s*\S/;
|
|
263
|
+
/** §5.3(ii) remove-path: deletion/restore verbs per the hr anatomy (replace/unlink/DeleteValue). */
|
|
264
|
+
export const MARKER_REMOVE_SHAPE = /\.replace\(|\bunlink\(|DeleteValue|Remove-Item/;
|
|
265
|
+
/** §5.3(iii) CLI inverse surface reachable from the package bin (depth ≤2). Tightened so the
|
|
266
|
+
* word "remove" in prose/comments cannot satisfy it: verb flags and function-call shapes only. */
|
|
267
|
+
export const MARKER_INVERSE_SURFACE = /--uninstall|\buninstall\b|\.remove_\w+\(|remove_path\s*\(|removePath\s*\(/;
|
|
268
|
+
|
|
269
|
+
// ------------------------------------------- R3b rubygems leg (plan §57 gem clause + R3b AC; closed per-family tables)
|
|
270
|
+
|
|
271
|
+
/** RubyGems plugin registry: a file at ANY path in the gem data named exactly this is auto-loaded by
|
|
272
|
+
* RubyGems itself on EVERY later `gem`/`ruby` process after install (site_ruby plugin scan; gemspec
|
|
273
|
+
* `files` shape measured in gem 3.6.7 probe 2026-09-09). Gem-world analog of the npm lifecycle hook
|
|
274
|
+
* and strictly worse (it never stops running) ⇒ residue-persistence-primitive CRITICAL, plan R3b (b). */
|
|
275
|
+
export const GEM_PERSISTENCE_BASENAMES: readonly string[] = ["rubygems_plugin.rb"];
|
|
276
|
+
/** Hook-registration class methods on the `Gem` singleton (installer callback registry). Closed
|
|
277
|
+
* literal trio from plan R3b (b); code-position regex, receiver must be `Gem` — dynamic dispatch
|
|
278
|
+
* (`Gem.public_send("post_" + x)`) is the §1.6-9 obfuscation lane, named in §8, never widened here.
|
|
279
|
+
* BLK-1 (VERIFIER-REPORT-R3B): `::` is the static-call spelling and registers a LIVE hook on box
|
|
280
|
+
* ruby 3.3.8 (Gem.post_install_hooks 0→1, probe 2026-09-09) — NOT dynamic dispatch, so both `.` and
|
|
281
|
+
* `::` separators are in the closed grammar; the send/const_get lane stays invisible and §8-named. */
|
|
282
|
+
export const GEM_HOOK_CALL = /(?<![\w.])Gem\s*(?:\.|::)\s*(?:post_install|post_uninstall|pre_install)\b/;
|
|
283
|
+
/** extconf.rb is EXECUTED by the installer during native build (rubygems MakeMaker phase) — the
|
|
284
|
+
* install-time reachability root for the T2/T3/xmgr families and the write-confinement lane. */
|
|
285
|
+
export const GEM_EXTCONF_BASENAME = "extconf.rb";
|
|
286
|
+
/** Write/exec verbs whose first argument (or whole argv span) must stay inside the extension build
|
|
287
|
+
* tree. Closed set motivated by real extconf bodies (mkmf-based gems, e.g. nokogiri/bootsnap family
|
|
288
|
+
* shapes) plus the plan R3b (a)/(d) enumerations: File.*, FileUtils.*, Dir.mkdir, IO.popen, Open3.*,
|
|
289
|
+
* bare system/spawn/exec, backtick and %x() command execution. Read-only mkmf hints (dir_config,
|
|
290
|
+
* have_header, have_library, create_makefile) are deliberately NOT in the set — they probe or edit
|
|
291
|
+
* the in-tree Makefile only (probe 2026-09-09: create_makefile("acme/ghost") writes no path arg).
|
|
292
|
+
* BLK-4a (VERIFIER-REPORT-R3B): `%x` escaped the old `%x\(`-only spelling.
|
|
293
|
+
* BLK-6 (VERIFIER-REPORT-R3B-FIX): box ruby 3.3.8 EXECUTES any non-alphanumeric, non-space %x
|
|
294
|
+
* delimiter — `%x!cmd!`, `%x~cmd~`, `%x"cmd"`, `%x%cmd%` all ran live commands (probe 2026-09-09) —
|
|
295
|
+
* so the enumerated six-class was replaced by the GENERIC `%x[^A-Za-z0-9\s]` arm, mirroring
|
|
296
|
+
* GEM_PERCENT_LITERAL's delimiter-agnostic approach; the closer is resolved generically in
|
|
297
|
+
* residueGem's gemVerbSpan (pairs for ( [ { <, otherwise the opener char itself).
|
|
298
|
+
* BLK-5 (VERIFIER-REPORT-R3B-FIX): every dotted alternative carries `(?:\.|::)` — `File::write`,
|
|
299
|
+
* `FileUtils::cp`, `Dir::mkdir`, `IO::popen`, `Open3::pipeline_rw` are the static-call spellings and
|
|
300
|
+
* execute identically (box-verified), so the ::-form costs a row exactly like the dot-form; the bare
|
|
301
|
+
* system/spawn/exec/popen arm deliberately excludes `:` from its lookbehind, which is the DOCUMENTED
|
|
302
|
+
* design by which `Kernel::system` rows (BLK-1 precedent: :: is a live spelling, not obfuscation).
|
|
303
|
+
* BLK-R3bF2-B (VERIFIER-REPORT-R3B-FIX2): the dotted-canonical `Process.spawn("…")` matched NOTHING —
|
|
304
|
+
* the bare arm's lookbehind (?<![\w.]) refuses the `.`, and there was no Process receiver arm — the most
|
|
305
|
+
* canonical spawn spelling in Ruby silent-passed (box ruby 3.3.8 executes it live, probe 2026-09-09).
|
|
306
|
+
* An explicit `Process\s*(?:\.|::)\s*spawn` arm now precedes the bare verbs, so both separators pay
|
|
307
|
+
* EXACTLY one row each (the receiver match consumes the colon form before the bare-spawn arm can
|
|
308
|
+
* re-fire on it; pinned R3b-C9d/C9e).
|
|
309
|
+
* BLK-R3bF4-B (VERIFIER-REPORT-R3B-FIX3): the bare arm's operand class gained `%` — box ruby 3.3.8
|
|
310
|
+
* executes `system %q{/usr/local/bin/x}`, `exec %q{…}` and `system %{touch /tmp/y}` live (payloads
|
|
311
|
+
* i4_pctq/i4b_pctbrace, liveness re-proved 2026-09-09) yet ( [ " ' matched none ⇒ silent escape. The
|
|
312
|
+
* lookbehind (?<![\w.]) still refuses `system%` inside an identifier; gemVerbSpan's new percent
|
|
313
|
+
* branch feeds the paren-less payload through the SAME expansion path as the parened form, and a
|
|
314
|
+
* RELATIVE operand (`system %q{make clean}`) stays zero-row (pinned R3b-C12a…C12c).
|
|
315
|
+
* BLK-R3bF5-A (VERIFIER-REPORT-R3B-FIX4): the grammar is UNCHANGED this round — `system %x|…|`,
|
|
316
|
+
* `system %x!…!`, `%x%…%`, `%x@…@` bare-verb forms all live-execute on box ruby 3.3.8 yet silent-
|
|
317
|
+
* passed because the percent branch consumed the `%` (the dedicated %x arm could not re-fire on that
|
|
318
|
+
* text) while its span still carried the `%x<delim>` prefix past the ROOTED `^`-anchor; the fix
|
|
319
|
+
* normalizes that branch's span to the quoted payload inside gemVerbSpan (residueGem.ts), covering
|
|
320
|
+
* every non-q/Q designator and the designator-less `%|…|` spelling through the ONE existing path
|
|
321
|
+
* (pinned R3b-C13a…C13e).
|
|
322
|
+
* ROUND 8 / BLK-7 (VERIFIER-REPORT-R3B-FIX6): per-finding naming was not convergence — the whole
|
|
323
|
+
* write-primitive call-form ROSTER was box-swept ONCE on ruby 3.3.8 (probe 2026-09-09, scratch-safe)
|
|
324
|
+
* and closed in this one table. The File arm gains sysopen, the metadata verbs (chown/lchmod/
|
|
325
|
+
* lchown/utime), truncate, mkfifo and the deletion verbs unlink/delete; Dir gains rmdir; the IO arm
|
|
326
|
+
* becomes popen|sysopen|write|binwrite|copy_stream; and a NEW fixed-receiver reopen arm
|
|
327
|
+
* (`$stdout`/`$stderr`/`$stdin`/STDOUT/STDERR/STDIN + .reopen, both separators) covers the box-live
|
|
328
|
+
* fd-hijack writer that silent-passed UNNAMED. Deletion is COVERED by adjudication: the
|
|
329
|
+
* FileUtils.rm_r/remove wildcard already priced deletion, so File.unlink/Dir.rmdir join the same
|
|
330
|
+
* lane (pinned R3b-C15a…C15f). Readers (IO.read etc.), File.new, bare open and arbitrary-receiver
|
|
331
|
+
* `f.reopen(x)` stay the NAMED §8 invisible lanes, never silent ones.
|
|
332
|
+
* ROUND 10 / BLK-R3bF9-A/B/C (VERIFIER-REPORT-R3B-FIX7): the reopen arm was NOT closed at six — the
|
|
333
|
+
* M0 machine-derived alias table (.omo/evidence/residue-spike/ruby-alias-table-3.3.8.txt, box ruby
|
|
334
|
+
* 3.3.8) shows `$>` is a $stdout alias (`$>.equal?($stdout)` true, reopen materialized the rooted
|
|
335
|
+
* target): added to the fixed receivers (pinned C15a-bis; `$<`/ARGF has NO reopen — box-DEAD, §8).
|
|
336
|
+
* Dir gains `delete|unlink` (C15f 3→5) and `chroot` — the confinement break itself, priced only on a
|
|
337
|
+
* rooted operand (C15j). File gains `lutime` (C15e 8→9; `futime` is absent on 3.3.8 — §8 DEAD row).
|
|
338
|
+
* `binwrite|write|sysopen` moved from the File arm into a shared `(?:File|IO)` arm that also picks up
|
|
339
|
+
* File.popen/File.copy_stream — IO singletons inherited by File (respond_to?-proved, C15i). The bare
|
|
340
|
+
* arm gains the mkmf top-level verbs `rm_rf|rm_f|xsystem|xpopen` (MakeMakefile module_functions the
|
|
341
|
+
* mkmf.rb:2890-2894 include-splice promotes to extconf privates; dotted FileUtils.rm_rf stays ONE
|
|
342
|
+
* row through the [\w.] lookbehind, re-pinned in C15i). */
|
|
343
|
+
export const GEM_WRITE_CALL = /(?<![\w.?])(?:File\s*(?:\.|::)\s*(?:open|touch|link|symlink|rename|unlink|delete|truncate|chmod|lchmod|chown|lchown|utime|lutime|mkfifo)|FileUtils\s*(?:\.|::)\s*\w+|(?:File|IO)\s*(?:\.|::)\s*(?:popen|sysopen|write|binwrite|copy_stream)|Dir\s*(?:\.|::)\s*(?:mkdir|rmdir|delete|unlink|chroot)|Open3\s*(?:\.|::)\s*\w+|Process\s*(?:\.|::)\s*spawn|(?:\$(?:stdout|stderr|stdin|>)|STDOUT|STDERR|STDIN)\s*(?:\.|::)\s*reopen|(?<![\w.])(?:system|spawn|exec|popen|rm_rf|rm_f|xsystem|xpopen)\s*(?:\(|["']|%)|%x[^A-Za-z0-9\s]|`)/g;
|
|
344
|
+
/** A write/exec span containing ANY of these marks escapes the build tree ⇒ T3 HIGH (plan R3b (a):
|
|
345
|
+
* $HOME, user-home resolution, ENV-driven targets, `~`-rooted paths). BLK-R3bF2-A
|
|
346
|
+
* (VERIFIER-REPORT-R3B-FIX2): the plain-substring table was DOT-ONLY, so the live colon spellings
|
|
347
|
+
* `Dir::home` (== Dir.home, box ruby 3.3.8) and `ENV::[]("HOME")` / `ENV::fetch` (both read $HOME,
|
|
348
|
+
* probe 2026-09-09) silent-passed — the `::`-normalized-everywhere meta-lesson. The table is now
|
|
349
|
+
* RegExp: `Dir\s*(?:\.|::)\s*home` covers spaced forms, `ENV\s*(?:\[|\.|::)` covers every ENV lookup
|
|
350
|
+
* spelling (and makes ENV::fetch row via THIS lane, killing the accidental dependency on the npm
|
|
351
|
+
* fetch( pattern; pinned R3b-C9a…C9c). No /g flag — .test() on a stateful regex would latch. */
|
|
352
|
+
export const GEM_OUT_OF_TREE_MARKS: readonly RegExp[] = [/\$HOME/u, /\bDir\s*(?:\.|::)\s*home/u, /\bENV\s*(?:\[|\.|::)/u, /~\//u];
|
|
353
|
+
/** …and a string literal ROOTED at a slash (`"/usr/local/…"` — the plan's "/usr/local, /opt,
|
|
354
|
+
* absolute paths" writes). Root-slash only: a URL inside a curl argv ("https://host/x") is T2
|
|
355
|
+
* territory, not a path write, so the quote-content-slash spelling stays confinement-clean. */
|
|
356
|
+
export const GEM_ABSOLUTE_LITERAL = /["']\s*\//;
|
|
357
|
+
/** BLK-4a (VERIFIER-REPORT-R3B): a slash-rooted BARE token in a write/exec span (`%x|install … /usr/…|`,
|
|
358
|
+
* shell argv without quotes) — boundary then `/`. URLs stay clean (the slash follows `:`).
|
|
359
|
+
* BLK-R3bF2-C (VERIFIER-REPORT-R3B-FIX2, adjudicated WIDEN): the space-or-start boundary missed every
|
|
360
|
+
* OPENING-BRACKET adjacency — `File.write(%(/usr/local/bin/x), "d")` and `system(%w[/usr/local/bin/x])`
|
|
361
|
+
* (box ruby 3.3.8 executes the %w argv verbatim, probe 2026-09-09) silent-passed. Accepting `[({<`
|
|
362
|
+
* adjacency costs FPs on bracket-adjacent rooted argv hints (`%w[install … /usr/bin/make]`-style
|
|
363
|
+
* extconf shapes now row) — the verifier's own note: naming the %w argv class silent instead would
|
|
364
|
+
* gut the exec-confinement lane, so the exotic-operand FP cost is ACCEPTED (pinned R3b-C10a…C10c).
|
|
365
|
+
* No %w-item-join is added: the boundary widening alone catches every rooted %w item. Lookahead (not
|
|
366
|
+
* \S) so `/`-then-structure (`//` comments, `/*`) doesn't newly row; /m for line-start spans, no /g. */
|
|
367
|
+
export const GEM_ROOTED_SLASH_ARG = /(?:^|[\s(\[{<])\/(?=[A-Za-z0-9._~-])/m;
|
|
368
|
+
/** NIT-1 (VERIFIER-REPORT-R3B): the former GEM_CONFINED_MARKS table was exported but consumed by no
|
|
369
|
+
* predicate — $extout/DISTDIR staging is confinement-legitimate through the ABSENCE-of-marks rule
|
|
370
|
+
* itself (they carry no mark), so the table was deleted, not wired; §8 states this. */
|
|
371
|
+
/** BLK-4c: Dir.chdir MOVES the extconf CWD — every later relative write follows it out of the build
|
|
372
|
+
* tree, so any occurrence in a reachable extconf is itself a confinement break (HIGH), one row per
|
|
373
|
+
* file regardless of argument; false positives (a chdir into an in-tree subdir) are the accepted
|
|
374
|
+
* exotic-operand cost. BLK-5 (R3B-FIX): `Dir::chdir("/tmp")` is the same live call on box ruby 3.3.8
|
|
375
|
+
* (probe 2026-09-09), so the chdir lane is separator-agnostic like the verb table. */
|
|
376
|
+
export const GEM_CHDIR_CALL = /(?<![\w.])Dir\s*(?:\.|::)\s*chdir\b/;
|
|
377
|
+
/** BLK-2: Ruby literal-text percent arrays `%q{…}` / `%Q(…)` hide paths from the quote-then-slash
|
|
378
|
+
* test (P12: File.open(%q{/etc/evil}, "w") silent-passed). The opener char class + bracket pairs are
|
|
379
|
+
* here; residueGem extracts the literal text and re-feeds it through the same mark tests. */
|
|
380
|
+
export const GEM_PERCENT_LITERAL = /%[qQ]([^A-Za-z0-9\s])/;
|
|
381
|
+
export const GEM_PERCENT_PAIRS: Readonly<Record<string, string>> = { "(": ")", "[": "]", "{": "}", "<": ">" };
|
|
382
|
+
/** BLK-2 ALL-bindings doctrine (crates §745-747 neighbour, plan exotic-operand rule): every single-`=`
|
|
383
|
+
* assignment line `id = <rhs>` is captured (==, =~, +=, <= excluded by shape); a bare-identifier write
|
|
384
|
+
* operand resolves through these bindings, transitively, cycle ⇒ null ⇒ fail-closed arm.
|
|
385
|
+
* BLK-8 (VERIFIER-REPORT-R3B-FIX): the LHS grammar covers `@ivar`, `$global` and `CONSTANT` names —
|
|
386
|
+
* `@o = "/usr/local/bin/x"; File.write(@o, "y")` writes through a live box-verified binding (probe
|
|
387
|
+
* 2026-09-09), so those names are binding keys too; the map/cycle machinery in residueGem is shared. */
|
|
388
|
+
export const GEM_ASSIGNMENT = /^\s*([@$]?[A-Za-z_][A-Za-z0-9_]*[?!]?)\s*=(?![=~])\s*(.+)$/;
|
|
389
|
+
/** A write operand that is exactly one bare Ruby identifier/method word (no quote, no call, no `/`).
|
|
390
|
+
* BLK-8: same `[@$]?…[?!]?` shape as the assignment LHS so ivar/global/CONST operands enter the
|
|
391
|
+
* binding lane instead of silently bypassing it.
|
|
392
|
+
* BLK-R3bF2-D (VERIFIER-REPORT-R3B-FIX2): the grammar stopped at single-`@`/letter-`$`, so `@@cvar`
|
|
393
|
+
* operands and `$`-builtin globals (`$0`, `$!`, `$:`, `$-x` — all assignable, box ruby 3.3.8 live)
|
|
394
|
+
* BYPASSED the operand lane entirely and silent-passed (`@@ov = "/usr/…"; File.write(@@ov, "y")` → 0).
|
|
395
|
+
* Doctrine-consistent route: these heads are NOT added to the binding LHS grammar (no per-name lists,
|
|
396
|
+
* `@@`/special-global binding semantics vary by scope), only to the ACCEPTANCE grammar — an operand
|
|
397
|
+
* that matches here but resolves to nothing falls into the EXISTING unresolvable-fail-close lane
|
|
398
|
+
* (pinned R3b-C11a/C11b). */
|
|
399
|
+
export const GEM_BARE_IDENTIFIER = /^(?:(?:@{1,2}|\$)?[A-Za-z_][A-Za-z0-9_]*[?!]?|\$[0-9]+|\$-[a-zA-Z0-9]|\$[-!@#$%*?'"&():;,.<>=~^|+\/])$/u;
|
|
400
|
+
/** metadata.gz YAML key whose value is the gem's declared build-extension list (gem 3.6.7 measured:
|
|
401
|
+
* `extensions: []` when pure-ruby, `extensions:` + block seq `- ext/foo/extconf.rb` otherwise; flow
|
|
402
|
+
* `extensions: ["a", "b"]` tolerated as the YAML-equal spelling). BLK-7 (VERIFIER-REPORT-R3B-FIX):
|
|
403
|
+
* the regex is GLOBAL (/gm) so residueGem counts EVERY top-level occurrence — psych/rubygems resolve
|
|
404
|
+
* duplicate keys LAST-wins (box-measured 2026-09-09) while a first-wins read silently certifies a
|
|
405
|
+
* crafted `extensions: []` + real block-seq container clean, so >1 occurrence THROWS fail-closed. */
|
|
406
|
+
export const GEM_EXTENSIONS_KEYS = /^extensions:[ \t]*(.*)$/gm;
|
|
407
|
+
/** One block-sequence item line under extensions:. */
|
|
408
|
+
export const GEM_YAML_SEQ_ITEM = /^[ \t]*-[ \t]*["']?([^"'\n]+?)["']?[ \t]*$/;
|
|
409
|
+
/** BLK-3 (VERIFIER-REPORT-R3B, plan-AC violation): YAML comments do NOT terminate a block sequence —
|
|
410
|
+
* `extensions:\n# packaged by tool\n- ext/ghost/extconf.rb` silently declared NOTHING under the old
|
|
411
|
+
* break-on-unrecognized loop. Comment lines are skipped and collection CONTINUES. */
|
|
412
|
+
export const GEM_YAML_COMMENT = /^[ \t]*#/;
|
|
413
|
+
/** …and the sequence ends legitimately only at the next top-level mapping key (column-0 `key:`).
|
|
414
|
+
* Anything else non-blank inside the block region is an unrecognized declaration ⇒ THROW (the gate
|
|
415
|
+
* stops via EngineRunError; never a silent null). */
|
|
416
|
+
export const GEM_YAML_KEY_TERM = /^[^\s#][^\n]*:(\s|$)/;
|
|
417
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// provenance: R3a pypi leg (plan border-residue-gate §57, contract §4 T2-py family).
|
|
2
|
+
// NO T1 pass-lane for pypi — setuptools executes arbitrary build code, so plan §57 keeps the
|
|
3
|
+
// leg detection-only: the closed FAMILY table flags T2/T3/xmgr shapes inside build hooks, plus
|
|
4
|
+
// the artifact-wide T4 sweep. Unknown/unparseable simply produces no residue row; the pre-R3a
|
|
5
|
+
// pypi findings (sdist-unexpected-file / twine-check / engine rows) pass through untouched.
|
|
6
|
+
import { PY_BUILD_HOOK_BASENAMES, PY_BUILD_SYSTEM_SECTION } from "../rules/residueMatchers.ts";
|
|
7
|
+
import { FAMILY_WORDING, familyHits, readText, sectionBlock, t4HitFor, type ResidueHit } from "./residue.ts";
|
|
8
|
+
|
|
9
|
+
/** Residue hits for one extracted pypi tree (sdist or wheel), paths wrapper-stripped inner rels. */
|
|
10
|
+
export function residuePypiHits(pkgDir: string, innerRels: readonly string[]): ResidueHit[] {
|
|
11
|
+
const hits: ResidueHit[] = [];
|
|
12
|
+
for (const rel of innerRels) {
|
|
13
|
+
const text = readText(pkgDir, rel);
|
|
14
|
+
if (text === null) continue;
|
|
15
|
+
const name = rel.split("/").pop() ?? "";
|
|
16
|
+
// §57: setup.py body whole; pyproject only its [build-system] section — [project].dependencies
|
|
17
|
+
// is runtime metadata, out of install-time scope (false-positive wall, tests pin it silent).
|
|
18
|
+
const buildSystem = name === "pyproject.toml" ? sectionBlock(text, PY_BUILD_SYSTEM_SECTION) : "";
|
|
19
|
+
if (PY_BUILD_HOOK_BASENAMES.includes(name) || buildSystem !== "") {
|
|
20
|
+
hits.push(...familyHits(rel, name === "pyproject.toml" ? buildSystem : text, FAMILY_WORDING.pypi));
|
|
21
|
+
}
|
|
22
|
+
const t4 = t4HitFor(rel, text);
|
|
23
|
+
if (t4 !== null) hits.push(t4);
|
|
24
|
+
}
|
|
25
|
+
return hits;
|
|
26
|
+
}
|