@readystack/payroll-deduction-code-audit 1.0.2
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/LICENSE.txt +16 -0
- package/README.md +52 -0
- package/README.md.bak_s158refund +52 -0
- package/cli.js +161 -0
- package/engine.js +135 -0
- package/license.js +83 -0
- package/package.json +41 -0
- package/rules.json +62 -0
- package/strings.json +10 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Payroll Deduction Code Audit 2026 — Licence
|
|
2
|
+
|
|
3
|
+
Free scope
|
|
4
|
+
Every deduction code in the open map is audited against twelve rules and each broken line is named with its line number. is free for any use, personal or commercial, for as long as you keep the
|
|
5
|
+
extension installed, and the online check at the product page is free without registration.
|
|
6
|
+
|
|
7
|
+
Paid scope
|
|
8
|
+
Audits every deduction map in the workspace at once and writes a dated exposure report you keep as the payroll audit trail. is the paid part. It asks for a licence key issued at purchase (one seat, one key;
|
|
9
|
+
7-day full refund, no questions). The key is checked against the payment provider and
|
|
10
|
+
cached locally for 30 days so it keeps working offline.
|
|
11
|
+
|
|
12
|
+
Redistribution
|
|
13
|
+
You may not resell, re-host or bundle this extension or its rule set. The rule set and the
|
|
14
|
+
engine are provided as-is; check results are advisory and do not constitute legal advice.
|
|
15
|
+
|
|
16
|
+
(c) ReadyStack — https://getreadystack.com
|
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Payroll Deduction Code Audit 2026
|
|
2
|
+
|
|
3
|
+
Twelve rules over the deduction-code map, before the January 31 W-2 filing date
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npx @readystack/payroll-deduction-code-audit file
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Node 18+. The same 12 rules as the VS Code extension, from a terminal or CI.
|
|
12
|
+
|
|
13
|
+
## Free
|
|
14
|
+
|
|
15
|
+
- Every deduction code in the open map is audited against twelve rules and each broken line is named with its line number.
|
|
16
|
+
- `--rules` lists every rule
|
|
17
|
+
|
|
18
|
+
## With a licence ($29 once)
|
|
19
|
+
|
|
20
|
+
- Audits every deduction map in the workspace at once and writes a dated exposure report you keep as the payroll audit trail.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
@readystack/payroll-deduction-code-audit --dir ./templates --report html --out report.html
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Hand-checking one deduction map against the IRS Box 12 letter list is about half a day of a payroll analyst's time, every plan year
|
|
27
|
+
|
|
28
|
+
## Use from an AI agent (MCP)
|
|
29
|
+
|
|
30
|
+
Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{ "mcpServers": { "payroll-deduction-code-audit": { "command": "npx", "args": ["-y", "@readystack/payroll-deduction-code-audit", "--mcp"] } } }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Tools: `check_text` and `check_file` (free) · `check_dir` (licence). The agent gets every finding with the line number.
|
|
37
|
+
|
|
38
|
+
## Use in CI
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
- name: Payroll Deduction Code Audit 2026
|
|
42
|
+
run: npx -y @readystack/payroll-deduction-code-audit --dir . --ci
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
(container: `docker run --rm -v "$PWD:/work" getreadystack/payroll-deduction-code-audit --dir /work --ci`)
|
|
46
|
+
|
|
47
|
+
The folder sweep, reports and CI mode need one licence — one payment, no subscription. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
|
|
48
|
+
|
|
49
|
+
[Get a licence](https://buy.polar.sh/polar_cl_nAgRlEvSuB50Wdx5AxIH3jJ7eqwgOTIAwUFRk26gtYC)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
<!-- payroll deduction code audit -->
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Payroll Deduction Code Audit 2026
|
|
2
|
+
|
|
3
|
+
Twelve rules over the deduction-code map, before the January 31 W-2 filing date
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npx @readystack/payroll-deduction-code-audit file
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Node 18+. The same 12 rules as the VS Code extension, from a terminal or CI.
|
|
12
|
+
|
|
13
|
+
## Free
|
|
14
|
+
|
|
15
|
+
- Every deduction code in the open map is audited against twelve rules and each broken line is named with its line number.
|
|
16
|
+
- `--rules` lists every rule
|
|
17
|
+
|
|
18
|
+
## With a licence ($29 once)
|
|
19
|
+
|
|
20
|
+
- Audits every deduction map in the workspace at once and writes a dated exposure report you keep as the payroll audit trail.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
@readystack/payroll-deduction-code-audit --dir ./templates --report html --out report.html
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Hand-checking one deduction map against the IRS Box 12 letter list is about half a day of a payroll analyst's time, every plan year
|
|
27
|
+
|
|
28
|
+
## Use from an AI agent (MCP)
|
|
29
|
+
|
|
30
|
+
Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{ "mcpServers": { "payroll-deduction-code-audit": { "command": "npx", "args": ["-y", "@readystack/payroll-deduction-code-audit", "--mcp"] } } }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Tools: `check_text` and `check_file` (free) · `check_dir` (licence; the full sweep is free for 7 days). The agent gets every finding with the line number.
|
|
37
|
+
|
|
38
|
+
## Use in CI
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
- name: Payroll Deduction Code Audit 2026
|
|
42
|
+
run: npx -y @readystack/payroll-deduction-code-audit --dir . --ci
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
(container: `docker run --rm -v "$PWD:/work" getreadystack/payroll-deduction-code-audit --dir /work --ci`)
|
|
46
|
+
|
|
47
|
+
Try the full run free for 7 days — no key needed. Then one licence, 7-day refund, no questions. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
|
|
48
|
+
|
|
49
|
+
[Get a licence](https://buy.polar.sh/polar_cl_nAgRlEvSuB50Wdx5AxIH3jJ7eqwgOTIAwUFRk26gtYC)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
<!-- payroll deduction code audit -->
|
package/cli.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cli.js — the same rules as the VS Code extension, run from a terminal or a container. Generated by adapters.py.
|
|
3
|
+
'use strict';
|
|
4
|
+
const fs = require('fs'), path = require('path');
|
|
5
|
+
const RULES = require('./rules.json');
|
|
6
|
+
const S = require('./strings.json');
|
|
7
|
+
const lic = require('./license.js');
|
|
8
|
+
const ENGINE = require('./engine.js'); // s144 — scaffold products: the same engine the VS Code extension runs
|
|
9
|
+
const RULE_LIST = Array.isArray(ENGINE.RULES) ? ENGINE.RULES : (Array.isArray(RULES) ? RULES : []);
|
|
10
|
+
const RULE_N = ENGINE.RULE_COUNT || RULE_LIST.length;
|
|
11
|
+
function scan(text, file) {
|
|
12
|
+
const r = ENGINE.engine.check(String(text), { today: new Date().toISOString().slice(0, 10), path: file || '' });
|
|
13
|
+
return (r && r.findings || []).map((f) => ({ line: parseInt(f.line, 10) || 1, msg: String(f.msg || f.message || f.check || ''), fix: f.fix || null, sev: (String(f.sev || 'error').toLowerCase().startsWith('err') ? 'error' : 'warn') }));
|
|
14
|
+
}
|
|
15
|
+
function isText(p) { try { const b = fs.readFileSync(p); if (b.length > 2 * 1024 * 1024) return false; const s = b.subarray(0, 4096); for (const x of s) if (x === 0) return false; return true; } catch (e) { return false; } }
|
|
16
|
+
function walk(dir, exts, out) {
|
|
17
|
+
let ents = []; try { ents = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { return out; }
|
|
18
|
+
for (const e of ents) {
|
|
19
|
+
if (['node_modules', '.git', '.hg', '.svn', '.venv', 'venv', '.tox', '.cache', '.next', '.nuxt', '.terraform', '__pycache__'].includes(e.name)) continue; // s158 — .github · .gitlab-ci.yml · .env · .well-known 은 본다 (옛 줄은 점으로 시작하는 것을 전부 건너뛰어 워크플로 검사기가 .github/workflows 를 못 봤다)
|
|
20
|
+
const p = path.join(dir, e.name);
|
|
21
|
+
if (e.isDirectory()) walk(p, exts, out);
|
|
22
|
+
else if ((!exts.length || exts.includes(path.extname(e.name).toLowerCase())) && isText(p)) out.push(p);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
function esc(s) { return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
27
|
+
function csvq(s) { return '"' + String(s == null ? '' : s).replace(/"/g, '""') + '"'; }
|
|
28
|
+
function render(rows, fmt) {
|
|
29
|
+
if (fmt === 'json') return JSON.stringify({ tool: S.name, rules: RULE_N, files: rows }, null, 1);
|
|
30
|
+
if (fmt === 'csv') { const o = ['file,line,severity,message,fix']; for (const r of rows) for (const h of r.hits) o.push([csvq(r.file), h.line, h.sev, csvq(h.msg), csvq(h.fix)].join(',')); return o.join('\n') + '\n'; }
|
|
31
|
+
if (fmt === 'html') {
|
|
32
|
+
const n = rows.reduce((a, r) => a + r.hits.length, 0);
|
|
33
|
+
let o = '<!doctype html><meta charset="utf-8"><title>' + esc(S.name) + ' report</title><style>body{font:14px system-ui;margin:24px}table{border-collapse:collapse}td,th{border:1px solid #ddd;padding:4px 8px;font-size:13px}code{font-family:ui-monospace,monospace}</style>';
|
|
34
|
+
o += '<h1>' + esc(S.name) + '</h1><p>' + rows.length + ' files · ' + n + ' findings · ' + RULE_N + ' rules</p><table><tr><th>file</th><th>line</th><th>sev</th><th>message</th><th>fix</th></tr>';
|
|
35
|
+
for (const r of rows) for (const h of r.hits) o += '<tr><td><code>' + esc(r.file) + '</code></td><td>' + h.line + '</td><td>' + esc(h.sev) + '</td><td>' + esc(h.msg) + '</td><td>' + esc(h.fix || '') + '</td></tr>';
|
|
36
|
+
return o + '</table>';
|
|
37
|
+
}
|
|
38
|
+
let o = ''; for (const r of rows) { o += r.file + '\n'; for (const h of r.hits) o += ' ' + String(h.line).padStart(5) + ' ' + h.sev.padEnd(5) + ' ' + h.msg + (h.fix ? '\n -> ' + h.fix : '') + '\n'; if (!r.hits.length) o += ' (no findings)\n'; }
|
|
39
|
+
return o;
|
|
40
|
+
}
|
|
41
|
+
function trialState() {
|
|
42
|
+
// s158 — ⚑7일 무료 없음: 새 체험은 ⛔열지 않는다 · 이미 시작된 체험(trial.json)만 끝까지 지킨다 (읽기 전용)
|
|
43
|
+
if (process.env.READYSTACK_NO_TRIAL) return { active: false };
|
|
44
|
+
try {
|
|
45
|
+
const p = path.join(path.dirname(lic.storePath()), S.bin + '.trial.json');
|
|
46
|
+
const t = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
47
|
+
return { active: !!(t && t.until) && new Date(t.until + 'T23:59:59Z').getTime() > Date.now(), until: t && t.until };
|
|
48
|
+
} catch (e) { return { active: false }; }
|
|
49
|
+
}
|
|
50
|
+
// ★s158 2026-09-23 — the free run SPEAKS about the rest of the folder (vsix: auto.js · crx: welcome tab).
|
|
51
|
+
// The answer for the named files is complete and free; the next step is shown with the user's OWN numbers:
|
|
52
|
+
// "this folder has N more matching files - M issues in K of them" + the one command that sweeps them.
|
|
53
|
+
// Never blocks, never starts a trial (s158: no free trial - the free answer on the named files is the "try it"), human seat only, 400 files / 1.5 s cap.
|
|
54
|
+
function folderHint(done) {
|
|
55
|
+
if (!process.stderr.isTTY || process.env.CI || process.env.READYSTACK_NO_HINT || !done.length) return;
|
|
56
|
+
const base = path.dirname(path.resolve(done[0]));
|
|
57
|
+
const seen = new Set(done.map((f) => path.resolve(f)));
|
|
58
|
+
const all = walk(base, S.exts || [], []).filter((f) => !seen.has(path.resolve(f)));
|
|
59
|
+
if (!all.length) return;
|
|
60
|
+
const t0 = Date.now(); let issues = 0, hitFiles = 0, scanned = 0;
|
|
61
|
+
for (const f of all.slice(0, 400)) { if (Date.now() - t0 > 1500) break; let t = ''; try { t = fs.readFileSync(f, 'utf8'); } catch (e) { continue; } scanned++; const h = scan(t, f); if (h.length) { issues += h.length; hitFiles++; } }
|
|
62
|
+
const rel = path.relative(process.cwd(), base) || '.';
|
|
63
|
+
const tail = trialState().active ? 'free during your trial' : '$' + S.price + ' once';
|
|
64
|
+
const more = all.length + ' more matching file' + (all.length > 1 ? 's' : '');
|
|
65
|
+
const found = issues ? ' - ' + (scanned < all.length ? 'at least ' : '') + issues + ' issue' + (issues > 1 ? 's' : '') + ' in ' + hitFiles + ' of them' : ' - no issues found in them';
|
|
66
|
+
process.stderr.write('\n' + (rel === '.' ? 'This folder' : rel) + ' has ' + more + found + '.\n' + (issues ? 'Sweep them all: ' + S.bin + ' --dir ' + (/\s/.test(rel) ? JSON.stringify(rel) : rel) + ' (' + tail + ')\n' : ''));
|
|
67
|
+
}
|
|
68
|
+
let _usePinged = false;
|
|
69
|
+
function pingUse() {
|
|
70
|
+
try {
|
|
71
|
+
if (_usePinged) return; _usePinged = true;
|
|
72
|
+
if (process.env.DO_NOT_TRACK === '1' || process.env.READYSTACK_NO_TELEMETRY || process.env.CI) return;
|
|
73
|
+
if (!(process.stdout.isTTY || process.stdin.isTTY)) return; // human seat only (s152)
|
|
74
|
+
const https = require('https');
|
|
75
|
+
const body = JSON.stringify({ t: 'use', slug: S.bin, src: 'cli', why: 'free' });
|
|
76
|
+
const req = https.request({ hostname: 'getreadystack.com', path: '/api/ev', method: 'POST', timeout: 3000,
|
|
77
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body), 'user-agent': 'readystack-cli/' + S.bin } }, function (res) { res.resume(); });
|
|
78
|
+
req.on('timeout', function () { req.destroy(); }); req.on('error', function () {});
|
|
79
|
+
req.write(body); req.end();
|
|
80
|
+
} catch (e) {}
|
|
81
|
+
}
|
|
82
|
+
function mcpServe() {
|
|
83
|
+
process.env.READYSTACK_MCP = '1'; // s152 - 에이전트(Claude Code·Cursor)가 부른 세션은 사람 자리다 · 키 판 핑 src=mcp
|
|
84
|
+
// s144 — MCP server over stdio (newline-delimited JSON-RPC · no dependencies). Free: check_text · check_file. Licence (s158: no free trial): check_dir.
|
|
85
|
+
const rl = require('readline').createInterface({ input: process.stdin });
|
|
86
|
+
const send = (o) => process.stdout.write(JSON.stringify(o) + '\n');
|
|
87
|
+
const tools = [
|
|
88
|
+
{ name: 'check_text', description: S.name + ' - run all ' + RULE_N + ' checks on a text (free)', inputSchema: { type: 'object', properties: { text: { type: 'string', description: 'file contents' }, path: { type: 'string', description: 'optional file name for context' } }, required: ['text'] } },
|
|
89
|
+
{ name: 'check_file', description: S.name + ' - run all checks on one file by path (free)', inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } },
|
|
90
|
+
{ name: 'check_dir', description: S.name + ' - sweep a folder and return every finding (licence; $' + S.price + ' once)', inputSchema: { type: 'object', properties: { dir: { type: 'string' }, ext: { type: 'string', description: 'optional extension filter, e.g. .html' } }, required: ['dir'] } }
|
|
91
|
+
];
|
|
92
|
+
const result = (id, rows) => send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: render(rows, 'text') }], structuredContent: { tool: S.name, rules: RULE_N, files: rows } } });
|
|
93
|
+
const fail = (id, text) => send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }], isError: true } });
|
|
94
|
+
rl.on('line', async (line) => {
|
|
95
|
+
let m; try { m = JSON.parse(line); } catch (e) { return; }
|
|
96
|
+
const id = m.id, method = m.method;
|
|
97
|
+
if (method === 'initialize') return send({ jsonrpc: '2.0', id, result: { protocolVersion: '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: '@readystack/' + S.bin, version: '1.0.0' } } });
|
|
98
|
+
if (method === 'notifications/initialized' || method === 'ping') { if (id !== undefined) send({ jsonrpc: '2.0', id, result: {} }); return; }
|
|
99
|
+
if (method === 'tools/list') return send({ jsonrpc: '2.0', id, result: { tools } });
|
|
100
|
+
if (method === 'tools/call') {
|
|
101
|
+
const name = (m.params || {}).name, args = (m.params || {}).arguments || {};
|
|
102
|
+
try {
|
|
103
|
+
if (name === 'check_text') return result(id, [{ file: args.path || '(text)', hits: scan(String(args.text || ''), args.path || '') }]);
|
|
104
|
+
if (name === 'check_file') return result(id, [{ file: args.path, hits: scan(fs.readFileSync(args.path, 'utf8'), args.path) }]);
|
|
105
|
+
if (name === 'check_dir') {
|
|
106
|
+
const r = await lic.ensure();
|
|
107
|
+
if (!r.ok && !trialState().active) return fail(id, S.need_key + ' Get a licence ($' + S.price + ', once): ' + lic.BUY_URL);
|
|
108
|
+
const files = walk(args.dir, args.ext ? [args.ext] : (S.exts || []), []);
|
|
109
|
+
return result(id, files.map((f) => { let t = ''; try { t = fs.readFileSync(f, 'utf8'); } catch (e) { return { file: f, hits: [], error: String(e.message) }; } return { file: f, hits: scan(t, f) }; }));
|
|
110
|
+
}
|
|
111
|
+
return send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'unknown tool ' + name } });
|
|
112
|
+
} catch (e) { return fail(id, String(e && e.message || e)); }
|
|
113
|
+
}
|
|
114
|
+
if (id !== undefined) send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'method not found: ' + method } });
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function help() {
|
|
118
|
+
return [S.name + ' - ' + S.subtitle, '', 'Usage: ' + S.bin + ' <file> [more files] check the files you name (free, every rule)',
|
|
119
|
+
' ' + S.bin + ' --dir <folder> [--ext .html] scan a whole folder (licence)',
|
|
120
|
+
' ' + S.bin + ' ... --report csv|json|html [--out file] export a report (licence)',
|
|
121
|
+
' ' + S.bin + ' ... --ci exit 1 when an error-level finding exists (licence)',
|
|
122
|
+
' ' + S.bin + ' --license <key> store your licence key (or set READYSTACK_LICENSE)',
|
|
123
|
+
' ' + S.bin + ' --rules list the ' + RULE_N + ' rules',
|
|
124
|
+
' ' + S.bin + ' --mcp run as an MCP server (stdio) for Claude Code / Cursor / Windsurf - free checks, folder sweep needs a licence', '',
|
|
125
|
+
'Free: ' + S.free, 'Licence ($' + S.price + ', once): ' + S.paid, 'Get a licence: ' + lic.BUY_URL, ''].join('\n');
|
|
126
|
+
}
|
|
127
|
+
(async function main() {
|
|
128
|
+
try { const _feed = await lic.pullFeed(); if (_feed && Array.isArray(_feed.rules)) { if (Array.isArray(ENGINE.RULES)) for (const r of _feed.rules) ENGINE.RULES.push(r); } } catch (e) {} // ★s134 구독 피드 병합 (키 있는 손님만)
|
|
129
|
+
const a = process.argv.slice(2);
|
|
130
|
+
const get = (k) => { const i = a.indexOf(k); return i >= 0 ? a[i + 1] : null; };
|
|
131
|
+
if (a.includes('--mcp')) { mcpServe(); return; }
|
|
132
|
+
if (!a.length || a.includes('--help') || a.includes('-h')) { process.stdout.write(help()); return; }
|
|
133
|
+
if (a.includes('--rules')) { process.stdout.write((RULE_LIST.length ? RULE_LIST.map((r, i) => String(i + 1).padStart(3) + ' [' + (r.sev || 'warn') + '] ' + (r.message || r.msg || r.id || '')) : Array.from({ length: RULE_N }, (_, i) => String(i + 1).padStart(3) + ' [engine] check ' + (i + 1))).join('\n') + '\n'); return; }
|
|
134
|
+
if (a.includes('--license')) { const r = await lic.ensure(get('--license')); process.stdout.write(r.ok ? 'Licence stored: ' + lic.storePath() + '\n' : 'Licence not accepted (' + r.why + '). Get one: ' + lic.BUY_URL + '\n'); process.exit(r.ok ? 0 : 2); }
|
|
135
|
+
const dir = get('--dir'), fmt = get('--report'), out = get('--out'), ci = a.includes('--ci');
|
|
136
|
+
const exts = a.includes('--ext') ? [get('--ext')] : (S.exts || []);
|
|
137
|
+
const paid = !!(dir || fmt || ci);
|
|
138
|
+
if (paid) {
|
|
139
|
+
const r = await lic.ensure();
|
|
140
|
+
if (!r.ok) {
|
|
141
|
+
const t = trialState(); // s158 — ⚑7일 무료 없음 · 이미 시작된 체험만 지킨다
|
|
142
|
+
if (t.active) process.stderr.write('Trial: the full run is free until ' + t.until + ' — after that $' + S.price + ' once. Get a licence: ' + lic.BUY_URL + '\n');
|
|
143
|
+
else {
|
|
144
|
+
// s158 — the key is asked WITH the customer's own count (endowment · open loop): how many issues this folder holds.
|
|
145
|
+
let own = '';
|
|
146
|
+
if (dir) { try { const all = walk(dir, exts, []); let n = 0, k = 0, sc = 0; const t0 = Date.now();
|
|
147
|
+
for (const f of all.slice(0, 2000)) { if (Date.now() - t0 > 4000) break; let tx = ''; try { tx = fs.readFileSync(f, 'utf8'); } catch (e) { continue; } sc++; const h = scan(tx, f); if (h.length) { n += h.length; k++; } }
|
|
148
|
+
if (n) own = dir + ': ' + (sc < all.length ? 'at least ' : '') + n + ' issue' + (n > 1 ? 's' : '') + ' in ' + k + ' of ' + all.length + ' files.\n'; } catch (e) {} }
|
|
149
|
+
process.stderr.write(own + S.need_key + '\n set READYSTACK_LICENSE=<key> or ' + S.bin + ' --license <key>\n Get a licence ($' + S.price + ' once): ' + lic.BUY_URL + '\n'); process.exit(2);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const files = dir ? walk(dir, exts, []) : a.filter((x, i) => !x.startsWith('--') && !['--dir', '--report', '--out', '--ext', '--license'].includes(a[i - 1]));
|
|
154
|
+
if (!files.length) { process.stderr.write('No files. ' + S.bin + ' --help\n'); process.exit(2); }
|
|
155
|
+
const rows = files.map((f) => { let t = ''; try { t = fs.readFileSync(f, 'utf8'); } catch (e) { return { file: f, hits: [], error: String(e.message) }; } return { file: f, hits: scan(t) }; });
|
|
156
|
+
const text = render(rows, fmt || 'text');
|
|
157
|
+
if (out) fs.writeFileSync(out, text); else process.stdout.write(text.endsWith('\n') ? text : text + '\n');
|
|
158
|
+
if (!paid) { try { folderHint(files); } catch (e) {} pingUse(); } // s158 — a free run ends with the user's own folder count + one anonymous 'used' count (human seat only)
|
|
159
|
+
const errors = rows.reduce((n, r) => n + r.hits.filter((h) => h.sev === 'error').length, 0);
|
|
160
|
+
if (ci && errors) process.exit(1);
|
|
161
|
+
})().catch((e) => { process.stderr.write(String(e && e.stack || e) + '\n'); process.exit(3); });
|
package/engine.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/* Payroll Deduction Code Audit — one brain, used by the VS Code extension and by the free web page. */
|
|
2
|
+
(function (root) {
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
var RULES = (typeof module !== 'undefined' && module.exports)
|
|
6
|
+
? require('./rules.json')
|
|
7
|
+
: (root.PDCA_RULES || []);
|
|
8
|
+
|
|
9
|
+
var RULE_BY_ID = {};
|
|
10
|
+
for (var r = 0; r < RULES.length; r++) { RULE_BY_ID[RULES[r].id] = RULES[r]; }
|
|
11
|
+
|
|
12
|
+
// IRS W-2 Box 12 code letters.
|
|
13
|
+
var BOX12 = ['A','B','C','D','E','F','G','H','J','K','L','M','N','P','Q','R','S','T','V','W','Y','Z',
|
|
14
|
+
'AA','BB','DD','EE','FF','GG','HH','II'];
|
|
15
|
+
var DEFERRAL_BOX12 = ['D','E','G','S','H','AA','BB','EE'];
|
|
16
|
+
var PLAN_BASIS = ['125','129','132','401K','403B','457B','408P','501C18D'];
|
|
17
|
+
|
|
18
|
+
var RE_ROTH = /roth/i;
|
|
19
|
+
var RE_GARNISH = /garnish|levy|child\s*support|childsup|chld|creditor\s*withhold/i;
|
|
20
|
+
var RE_DEFERRAL = /401\s*\(?k|401k|403\s*\(?b|403b|457\s*\(?b|457b|elective\s*deferral|deferral/i;
|
|
21
|
+
var RE_HSA = /\bhsa\b|health\s*savings/i;
|
|
22
|
+
|
|
23
|
+
function flat(s) { return String(s === undefined || s === null ? '' : s).replace(/\s+/g, ' ').trim(); }
|
|
24
|
+
|
|
25
|
+
function planYear(opts) {
|
|
26
|
+
var t = flat((opts || {}).today);
|
|
27
|
+
var m = t.match(/(\d{4})/); // tolerate 2026-09-19, 2026/09/19, "2026-09-19x"
|
|
28
|
+
var y = m ? parseInt(m[1], 10) : NaN;
|
|
29
|
+
if (!y || y < 1990 || y > 2999) { y = new Date().getFullYear(); }
|
|
30
|
+
return y;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function splitRow(line) {
|
|
34
|
+
var out = [], cur = '', q = false;
|
|
35
|
+
for (var i = 0; i < line.length; i++) {
|
|
36
|
+
var c = line.charAt(i);
|
|
37
|
+
if (c === '"') { if (q && line.charAt(i + 1) === '"') { cur += '"'; i++; } else { q = !q; } }
|
|
38
|
+
else if (c === ',' && !q) { out.push(cur); cur = ''; }
|
|
39
|
+
else { cur += c; }
|
|
40
|
+
}
|
|
41
|
+
out.push(cur);
|
|
42
|
+
for (var j = 0; j < out.length; j++) { out[j] = flat(out[j]); }
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function norm(h) { return h.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, ''); }
|
|
47
|
+
|
|
48
|
+
function check(text, opts) {
|
|
49
|
+
opts = opts || {};
|
|
50
|
+
var findings = [];
|
|
51
|
+
var year = planYear(opts);
|
|
52
|
+
var lines = String(text === undefined || text === null ? '' : text).split(/\r\n|\r|\n/);
|
|
53
|
+
|
|
54
|
+
function add(id, line, extra) {
|
|
55
|
+
var rule = RULE_BY_ID[id] || { id: id, sev: 'med', msg: id };
|
|
56
|
+
findings.push({ check: id, sev: rule.sev, msg: extra ? rule.msg + ' ' + extra : rule.msg, line: line });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 1. locate the header row
|
|
60
|
+
var hIdx = -1, head = null;
|
|
61
|
+
for (var i = 0; i < lines.length; i++) {
|
|
62
|
+
if (!flat(lines[i])) { continue; }
|
|
63
|
+
var cells = splitRow(lines[i]).map(norm);
|
|
64
|
+
if (cells.indexOf('code') >= 0 || cells.indexOf('deduction_code') >= 0) { hIdx = i; head = cells; break; }
|
|
65
|
+
if (hIdx < 0 && i > 4) { break; }
|
|
66
|
+
}
|
|
67
|
+
if (hIdx < 0) { return { findings: findings, rows: 0, ruleCount: RULES.length, planYear: year }; }
|
|
68
|
+
|
|
69
|
+
function col(names) {
|
|
70
|
+
for (var n = 0; n < names.length; n++) { var k = head.indexOf(names[n]); if (k >= 0) { return k; } }
|
|
71
|
+
return -1;
|
|
72
|
+
}
|
|
73
|
+
var cCode = col(['code', 'deduction_code']);
|
|
74
|
+
var cDesc = col(['description', 'name', 'label']);
|
|
75
|
+
var cTax = col(['tax_treatment', 'tax', 'treatment', 'pre_tax']);
|
|
76
|
+
var cPlan = col(['plan_basis', 'plan', 'basis', 'plan_type']);
|
|
77
|
+
var cBox = col(['box12', 'w2_box12', 'box_12']);
|
|
78
|
+
var cEr = col(['employer_amount', 'employer', 'er_amount']);
|
|
79
|
+
var cEe = col(['employee_amount', 'employee', 'ee_amount']);
|
|
80
|
+
var cEff = col(['effective_date', 'effective', 'start_date']);
|
|
81
|
+
var cOrd = col(['order', 'priority', 'withholding_order', 'sequence']);
|
|
82
|
+
|
|
83
|
+
var seen = {}, rows = 0, sawGarnish = false;
|
|
84
|
+
|
|
85
|
+
for (var li = hIdx + 1; li < lines.length; li++) {
|
|
86
|
+
var raw = lines[li];
|
|
87
|
+
if (!flat(raw)) { continue; }
|
|
88
|
+
var f = splitRow(raw);
|
|
89
|
+
var get = function (k) { return k >= 0 && k < f.length ? f[k] : ''; };
|
|
90
|
+
var code = get(cCode);
|
|
91
|
+
if (!code) { continue; }
|
|
92
|
+
rows++;
|
|
93
|
+
var ln = li + 1;
|
|
94
|
+
var desc = get(cDesc);
|
|
95
|
+
var blob = code + ' ' + desc;
|
|
96
|
+
var tax = get(cTax).toLowerCase().replace(/[^a-z]/g, ''); // pre_tax -> pretax
|
|
97
|
+
var isPre = tax === 'pretax' || tax === 'pre' || tax === 'true' || tax === 'yes';
|
|
98
|
+
var plan = get(cPlan).toUpperCase().replace(/[^A-Z0-9]/g, '');
|
|
99
|
+
var box = get(cBox).toUpperCase().replace(/[^A-Z]/g, '');
|
|
100
|
+
var eff = get(cEff);
|
|
101
|
+
var garnish = RE_GARNISH.test(blob);
|
|
102
|
+
if (garnish) { sawGarnish = true; }
|
|
103
|
+
|
|
104
|
+
if (RE_ROTH.test(blob) && isPre) { add('roth_marked_pretax', ln, '(' + code + ')'); }
|
|
105
|
+
if (garnish && isPre) { add('garnishment_pretax', ln, '(' + code + ')'); }
|
|
106
|
+
if (!get(cTax)) { add('missing_tax_treatment', ln, '(' + code + ')'); }
|
|
107
|
+
|
|
108
|
+
var key = code.toUpperCase();
|
|
109
|
+
if (Object.prototype.hasOwnProperty.call(seen, key)) {
|
|
110
|
+
if (seen[key].tax !== tax) { add('duplicate_code_conflict', ln, '(' + code + ' first seen on line ' + seen[key].line + ')'); }
|
|
111
|
+
} else { seen[key] = { tax: tax, line: ln }; }
|
|
112
|
+
|
|
113
|
+
if (isPre && PLAN_BASIS.indexOf(plan) < 0) { add('pretax_without_plan_basis', ln, '(' + code + ')'); }
|
|
114
|
+
if (box && BOX12.indexOf(box) < 0) { add('bad_box12_letter', ln, '(' + code + ' carries ' + box + ')'); }
|
|
115
|
+
if (!box && RE_DEFERRAL.test(blob) && !RE_HSA.test(blob)) { add('deferral_without_box12', ln, '(' + code + ')'); }
|
|
116
|
+
if (RE_HSA.test(blob)) {
|
|
117
|
+
if (box !== 'W') { add('hsa_without_box12_w', ln, '(' + code + ')'); }
|
|
118
|
+
if (!get(cEr)) { add('hsa_no_employer_split', ln, '(' + code + ')'); }
|
|
119
|
+
}
|
|
120
|
+
var ym = eff.match(/(\d{4})/);
|
|
121
|
+
if (!eff) { add('missing_effective_date', ln, '(' + code + ')'); }
|
|
122
|
+
else if (ym && parseInt(ym[1], 10) < year) { add('stale_plan_year', ln, '(' + code + ' is dated ' + eff + ', plan year ' + year + ')'); }
|
|
123
|
+
void cEe; void DEFERRAL_BOX12;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (sawGarnish && cOrd < 0) { add('no_withholding_order', hIdx + 1, '(header row has no order column)'); }
|
|
127
|
+
|
|
128
|
+
findings.sort(function (a, b) { return a.line - b.line; });
|
|
129
|
+
return { findings: findings, rows: rows, ruleCount: RULES.length, planYear: year };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
var api = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length };
|
|
133
|
+
if (typeof module !== 'undefined' && module.exports) { module.exports = api; }
|
|
134
|
+
root.PDCAENGINE = api;
|
|
135
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
package/license.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// license.js — Polar licence check for the CLI / container. Generated by adapters.py; do not edit by hand.
|
|
2
|
+
'use strict';
|
|
3
|
+
const https = require('https'), fs = require('fs'), path = require('path'), os = require('os');
|
|
4
|
+
const ORG_ID = 'a5cdf664-d8e7-4f87-8895-056717aaba17';
|
|
5
|
+
const BENEFIT_ID = '78b648bf-3893-4fa5-927f-930a4c8854bc'; // s142 — this product's Polar benefit (s140 law: ask with benefit_id, or one key opens every product)
|
|
6
|
+
const BUY_URL = 'https://buy.polar.sh/polar_cl_nAgRlEvSuB50Wdx5AxIH3jJ7eqwgOTIAwUFRk26gtYC';
|
|
7
|
+
const SLUG = 'payroll-deduction-code-audit';
|
|
8
|
+
const GRACE_MS = 30 * 24 * 3600 * 1000; // after a successful check, 30 days work offline
|
|
9
|
+
const RECHECK_MS = 7 * 24 * 3600 * 1000; // re-ask Polar every 7 days (refunds / cancellations)
|
|
10
|
+
function storePath() { return path.join(process.env.READYSTACK_HOME || path.join(os.homedir(), '.config', 'readystack'), SLUG + '.json'); }
|
|
11
|
+
function load() { try { return JSON.parse(fs.readFileSync(storePath(), 'utf8')); } catch (e) { return {}; } }
|
|
12
|
+
function save(o) { try { fs.mkdirSync(path.dirname(storePath()), { recursive: true }); fs.writeFileSync(storePath(), JSON.stringify(o)); } catch (e) { /* read-only home: still works for this run */ } }
|
|
13
|
+
const ALL_BENEFIT_ID = '22692551-5203-4467-b1a3-e33cdba6589d'; // s149 2026-09-17 — 팀 키(전 린터 한 키 · Polar benefit) · 상품 benefit 다음에 한 번 더 묻는다
|
|
14
|
+
function validate(key) {
|
|
15
|
+
return validate1(key, BENEFIT_ID).then(function (r) { return (r.ok || r.offline || !/^[0-9a-f-]{36}$/.test(ALL_BENEFIT_ID)) ? r : validate1(key, ALL_BENEFIT_ID); });
|
|
16
|
+
}
|
|
17
|
+
function validate1(key, ben) {
|
|
18
|
+
return new Promise(function (resolve) {
|
|
19
|
+
if (!ORG_ID) return resolve({ ok: false, offline: false });
|
|
20
|
+
const body = JSON.stringify(/^[0-9a-f-]{36}$/.test(ben) ? { key: key, organization_id: ORG_ID, benefit_id: ben } : { key: key, organization_id: ORG_ID });
|
|
21
|
+
const req = https.request({ hostname: 'api.polar.sh', path: '/v1/customer-portal/license-keys/validate', method: 'POST', timeout: 8000,
|
|
22
|
+
headers: { 'content-type': 'application/json', 'polar-version': '2026-04', 'content-length': Buffer.byteLength(body) } }, function (res) {
|
|
23
|
+
let buf = ''; res.on('data', function (d) { buf += d; });
|
|
24
|
+
res.on('end', function () {
|
|
25
|
+
if (res.statusCode !== 200) return resolve({ ok: false, offline: false });
|
|
26
|
+
try { const j = JSON.parse(buf); resolve({ ok: j && (j.status === 'granted' || j.valid === true || !!j.id), offline: false }); }
|
|
27
|
+
catch (e) { resolve({ ok: false, offline: false }); }
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
req.on('timeout', function () { req.destroy(); resolve({ ok: false, offline: true }); });
|
|
31
|
+
req.on('error', function () { resolve({ ok: false, offline: true }); });
|
|
32
|
+
req.write(body); req.end();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// ★s151 2026-09-19 — 키 판(키가 없거나 거절된 순간)을 익명으로 센다 (슬러그·출처·이유만) · DO_NOT_TRACK=1 · READYSTACK_NO_TELEMETRY 면 안 보낸다 · 실패는 조용히 · 프로세스당 한 번.
|
|
36
|
+
let _pinged = false;
|
|
37
|
+
function pingPaywall(why) {
|
|
38
|
+
try {
|
|
39
|
+
if (_pinged) return; _pinged = true;
|
|
40
|
+
if (process.env.DO_NOT_TRACK === '1' || process.env.READYSTACK_NO_TELEMETRY || process.env.CI) return; // s151: CI(깃허브 액션 등)와 우리 빌드는 손님이 아니다
|
|
41
|
+
if (!(process.stdout.isTTY || process.stdin.isTTY || process.env.READYSTACK_MCP === '1')) return; // s152: 사람 자리(터미널·MCP 세션)에서만 센다 - 발행 1분 뒤 남의 실행기(JP · 우리 3대는 US)가 돌린 no_key 13건은 손님이 아니다
|
|
42
|
+
const body = JSON.stringify({ t: 'paywall', slug: SLUG, src: process.env.READYSTACK_MCP === '1' ? 'mcp' : 'cli', why: why || 'no_key' });
|
|
43
|
+
const req = https.request({ hostname: 'getreadystack.com', path: '/api/ev', method: 'POST', timeout: 3000,
|
|
44
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body), 'user-agent': 'readystack-cli/' + SLUG } }, function (res) { res.resume(); });
|
|
45
|
+
req.on('timeout', function () { req.destroy(); }); req.on('error', function () {});
|
|
46
|
+
req.write(body); req.end();
|
|
47
|
+
} catch (e) { /* 세는 것이 실패해도 상품은 돈다 */ }
|
|
48
|
+
}
|
|
49
|
+
async function ensure(explicitKey) {
|
|
50
|
+
const st = load();
|
|
51
|
+
const key = explicitKey || process.env.READYSTACK_LICENSE || st.key;
|
|
52
|
+
if (!key) { pingPaywall('no_key'); return { ok: false, why: 'no_key' }; } // s151
|
|
53
|
+
const age = Date.now() - (st.okAt || 0);
|
|
54
|
+
if (!explicitKey && st.key === key && age < RECHECK_MS) return { ok: true, cached: true };
|
|
55
|
+
const r = await validate(String(key).trim());
|
|
56
|
+
if (r.ok) { save({ key: String(key).trim(), okAt: Date.now() }); return { ok: true }; }
|
|
57
|
+
if (r.offline && st.key === key && age < GRACE_MS) return { ok: true, offline: true };
|
|
58
|
+
if (!r.offline) pingPaywall('invalid'); // s151
|
|
59
|
+
return { ok: false, why: r.offline ? 'offline' : 'invalid' };
|
|
60
|
+
}
|
|
61
|
+
// ★s134 — 구독 규칙 피드(층3 "바뀌면 업데이트"): 키 있는 손님만 · 7일마다 · 오프라인은 캐시. 워커 GET /api/rules/<slug>?key=
|
|
62
|
+
const FEED_URL = 'https://getreadystack.com/api/rules/';
|
|
63
|
+
function pullFeed() {
|
|
64
|
+
const st = load(); const key = process.env.READYSTACK_LICENSE || st.key; const cached = st.feed || null;
|
|
65
|
+
if (!key) return Promise.resolve(cached);
|
|
66
|
+
if (cached && (Date.now() - (st.feedAt || 0)) < RECHECK_MS) return Promise.resolve(cached);
|
|
67
|
+
return new Promise(function (resolve) {
|
|
68
|
+
let req;
|
|
69
|
+
try {
|
|
70
|
+
req = https.get(FEED_URL + encodeURIComponent(SLUG) + '?key=' + encodeURIComponent(key), { timeout: 8000, headers: { 'user-agent': 'readystack-cli' } }, function (res) {
|
|
71
|
+
let buf = ''; res.on('data', function (d) { buf += d; });
|
|
72
|
+
res.on('end', function () {
|
|
73
|
+
if (res.statusCode !== 200) return resolve(cached);
|
|
74
|
+
try { const j = JSON.parse(buf); if (!j || !Array.isArray(j.rules)) return resolve(cached); save(Object.assign(load(), { feed: j, feedAt: Date.now() })); resolve(j); }
|
|
75
|
+
catch (e) { resolve(cached); }
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
} catch (e) { return resolve(cached); }
|
|
79
|
+
req.on('timeout', function () { req.destroy(); resolve(cached); });
|
|
80
|
+
req.on('error', function () { resolve(cached); });
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
module.exports = { ensure, BUY_URL, storePath, pullFeed };
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@readystack/payroll-deduction-code-audit",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Twelve rules over the deduction-code map, before the January 31 W-2 filing date",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"payroll",
|
|
11
|
+
"deduction code",
|
|
12
|
+
"W-2 Box 12",
|
|
13
|
+
"pre-tax",
|
|
14
|
+
"HRIS",
|
|
15
|
+
"csv lint",
|
|
16
|
+
"plan year"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://getreadystack.com",
|
|
19
|
+
"funding": "https://buy.polar.sh/polar_cl_nAgRlEvSuB50Wdx5AxIH3jJ7eqwgOTIAwUFRk26gtYC",
|
|
20
|
+
"bin": {
|
|
21
|
+
"payroll-deduction-code-audit": "cli.js"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"mcpName": "io.github.jmshinhwa/payroll-deduction-code-audit",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/jmshinhwa/readystack-themes.git",
|
|
30
|
+
"directory": "payroll-deduction-code-audit"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"cli.js",
|
|
34
|
+
"license.js",
|
|
35
|
+
"rules.json",
|
|
36
|
+
"strings.json",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE.txt",
|
|
39
|
+
"engine.js"
|
|
40
|
+
]
|
|
41
|
+
}
|
package/rules.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "roth_marked_pretax",
|
|
4
|
+
"sev": "high",
|
|
5
|
+
"msg": "Roth deferrals are always after-tax. This row is flagged pre_tax, so taxable wages come out low."
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"id": "garnishment_pretax",
|
|
9
|
+
"sev": "high",
|
|
10
|
+
"msg": "Garnishments, child support and levies are withheld after tax. This row is flagged pre_tax."
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"id": "missing_tax_treatment",
|
|
14
|
+
"sev": "high",
|
|
15
|
+
"msg": "tax_treatment is empty. Nothing in the map says whether this code reduces taxable wages."
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "duplicate_code_conflict",
|
|
19
|
+
"sev": "high",
|
|
20
|
+
"msg": "This deduction code already appears above with a different tax_treatment."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "pretax_without_plan_basis",
|
|
24
|
+
"sev": "med",
|
|
25
|
+
"msg": "Pre-tax row names no plan_basis (125, 401k, 403b, 457b, 132), so nothing supports the exclusion."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "bad_box12_letter",
|
|
29
|
+
"sev": "high",
|
|
30
|
+
"msg": "This box12 value is not an IRS W-2 Box 12 code letter."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "deferral_without_box12",
|
|
34
|
+
"sev": "med",
|
|
35
|
+
"msg": "Elective-deferral code carries no W-2 Box 12 letter (D, E, G, S, AA, BB, EE)."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "hsa_without_box12_w",
|
|
39
|
+
"sev": "high",
|
|
40
|
+
"msg": "HSA contributions report in W-2 Box 12 under code W. This row does not carry W."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "hsa_no_employer_split",
|
|
44
|
+
"sev": "med",
|
|
45
|
+
"msg": "Box 12 code W is employer plus employee combined. This row has no employer_amount to add."
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "stale_plan_year",
|
|
49
|
+
"sev": "high",
|
|
50
|
+
"msg": "effective_date is older than the plan year being filed. Deduction limits reset every January 1."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "missing_effective_date",
|
|
54
|
+
"sev": "med",
|
|
55
|
+
"msg": "effective_date is empty, so the row cannot be tied to a plan year."
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "no_withholding_order",
|
|
59
|
+
"sev": "med",
|
|
60
|
+
"msg": "A garnishment code is present but the map has no order column to rank it against other deductions."
|
|
61
|
+
}
|
|
62
|
+
]
|
package/strings.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Payroll Deduction Code Audit 2026",
|
|
3
|
+
"subtitle": "Twelve rules over the deduction-code map, before the January 31 W-2 filing date",
|
|
4
|
+
"bin": "payroll-deduction-code-audit",
|
|
5
|
+
"price": 29,
|
|
6
|
+
"free": "Every deduction code in the open map is audited against twelve rules and each broken line is named with its line number.",
|
|
7
|
+
"paid": "Audits every deduction map in the workspace at once and writes a dated exposure report you keep as the payroll audit trail.",
|
|
8
|
+
"need_key": "This option needs a licence (Payroll Deduction Code Audit 2026).",
|
|
9
|
+
"exts": []
|
|
10
|
+
}
|