@readystack/nencho-fuyo-csv-lint-r8 1.0.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/LICENSE.txt +15 -0
- package/README.md +52 -0
- package/cli.js +161 -0
- package/engine.js +106 -0
- package/license.js +83 -0
- package/package.json +42 -0
- package/rules.json +135 -0
- package/strings.json +10 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
年末調整 扶養控除 チェック(令和8年分・62万円) — Licence
|
|
2
|
+
|
|
3
|
+
Free scope
|
|
4
|
+
開いている扶養親族CSV 1ファイルを令和8年分の要件で全行チェックし、控除漏れ・過大控除の金額と根拠条文を行ごとに表示する 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
|
+
チェック結果を従業員ごとの修正一覧レポートとして書き出し、年末調整の確認記録として保存する is the paid part. It asks for a licence key issued at purchase (one seat, one key). The key is checked against the payment provider and
|
|
9
|
+
cached locally for 30 days so it keeps working offline.
|
|
10
|
+
|
|
11
|
+
Redistribution
|
|
12
|
+
You may not resell, re-host or bundle this extension or its rule set. The rule set and the
|
|
13
|
+
engine are provided as-is; check results are advisory and do not constitute legal advice.
|
|
14
|
+
|
|
15
|
+
(c) ReadyStack — https://getreadystack.com
|
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# 年末調整 扶養控除 チェック(令和8年分・62万円)
|
|
2
|
+
|
|
3
|
+
扶養親族CSVを令和8年分の新要件62万円・74万円・特定親族特別控除で19ルール検査
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npx @readystack/nencho-fuyo-csv-lint-r8 file
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Node 18+. The same 19 rules as the VS Code extension, from a terminal or CI.
|
|
12
|
+
|
|
13
|
+
## Free
|
|
14
|
+
|
|
15
|
+
- 開いている扶養親族CSV 1ファイルを19ルールで全行チェックし、控除漏れ・過大控除の金額と根拠条文を行ごとに表示する(サンプル7人で11件の指摘・控除漏れ ¥1,170,000・過大控除 ¥860,000)
|
|
16
|
+
- `--rules` lists every rule
|
|
17
|
+
|
|
18
|
+
## With a licence ($29 once)
|
|
19
|
+
|
|
20
|
+
- チェック結果を従業員ごとの修正一覧レポートとして書き出し、年末調整の確認記録として保存する($29・1回払い・1人またはチーム1席に1キー)
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
@readystack/nencho-fuyo-csv-lint-r8 --dir ./templates --report html --out report.html
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
税理士に年末調整を頼んだ実例:従業員1人あたり2,500円(従業員45名で11万2,500円・税理士ドットコム掲載の事例)
|
|
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": { "nencho-fuyo-csv-lint-r8": { "command": "npx", "args": ["-y", "@readystack/nencho-fuyo-csv-lint-r8", "--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: 年末調整 扶養控除 チェック(令和8年分・62万円)
|
|
42
|
+
run: npx -y @readystack/nencho-fuyo-csv-lint-r8 --dir . --ci
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
(container: `docker run --rm -v "$PWD:/work" getreadystack/nencho-fuyo-csv-lint-r8 --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_QYWqS9lHXsKzdj2d8EtKinHzfk0YQHF5Xxzw311jfI1)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
<!-- 年末調整 扶養控除 -->
|
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,106 @@
|
|
|
1
|
+
/* 年末調整 扶養親族CSV チェック(令和8年分) — VS Code 拡張と無料ウェブ版で同じ判定を使う */
|
|
2
|
+
(function () {
|
|
3
|
+
var RULES = (typeof module !== 'undefined') ? require('./rules.json') : window.NENCHO_RULES;
|
|
4
|
+
var BY_ID = {}; RULES.forEach(function (r) { BY_ID[r.id] = r; });
|
|
5
|
+
var LIMIT = { fuyo: 620000, spouse: 620000, spouseSp: 1330000, tokutei: 1230000 };
|
|
6
|
+
var FUYO_AMT = { '一般扶養': 380000, '特定扶養': 630000, '老人扶養': 480000, '同居老親': 580000 };
|
|
7
|
+
var SPOUSE_SP = [[950000, 380000], [1000000, 360000], [1050000, 310000], [1100000, 260000], [1150000, 210000], [1200000, 160000], [1250000, 110000], [1300000, 60000], [1330000, 30000]];
|
|
8
|
+
var TOKUTEI = [[850000, 630000], [900000, 610000], [950000, 510000], [1000000, 410000], [1050000, 310000], [1100000, 210000], [1150000, 110000], [1200000, 60000], [1230000, 30000]];
|
|
9
|
+
var CLAIMED = ['一般扶養', '特定扶養', '老人扶養', '同居老親', '特定親族', '配偶者控除', '配偶者特別控除'];
|
|
10
|
+
var CHOKKEI = /^(父|母|祖父|祖母|曾祖父|曾祖母|義父|義母|配偶者の父|配偶者の母)$/;
|
|
11
|
+
|
|
12
|
+
function yen(n) { return '¥' + Math.round(n).toLocaleString('en-US'); }
|
|
13
|
+
function money(s) {
|
|
14
|
+
s = String(s == null ? '' : s).replace(/[円,\s"¥]/g, '');
|
|
15
|
+
if (s === '') return 0;
|
|
16
|
+
var m = s.match(/^(\d+(?:\.\d+)?)万$/); if (m) return Math.round(parseFloat(m[1]) * 10000);
|
|
17
|
+
return /^\d+$/.test(s) ? parseInt(s, 10) : NaN;
|
|
18
|
+
}
|
|
19
|
+
function band(tbl, x) { for (var i = 0; i < tbl.length; i++) if (x <= tbl[i][0]) return tbl[i][1]; return 0; }
|
|
20
|
+
// 令和8年分 給与所得(所得税法28条・別表第五の簡略:219.1万円未満は収入−74万円)
|
|
21
|
+
function kyuyoShotoku(r) {
|
|
22
|
+
if (r < 691000) return 0;
|
|
23
|
+
if (r < 2191000) return r - 740000;
|
|
24
|
+
var k = r <= 3600000 ? r * 0.3 + 80000 : r <= 6600000 ? r * 0.2 + 440000 : r <= 8500000 ? r * 0.1 + 1100000 : 1950000;
|
|
25
|
+
return Math.floor(r - k);
|
|
26
|
+
}
|
|
27
|
+
function ageAt(birth, year) {
|
|
28
|
+
var m = String(birth).match(/^(\d{4})[-\/.](\d{1,2})[-\/.](\d{1,2})$/); if (!m) return null;
|
|
29
|
+
var y = +m[1], mo = +m[2], d = +m[3];
|
|
30
|
+
var a = year - y; if (mo === 1 && d === 1) a += 1; // 年齢計算ニ関スル法律:誕生日の前日に加齢
|
|
31
|
+
return { age: a, naive: year - y, jan1: mo === 1 && d === 1 };
|
|
32
|
+
}
|
|
33
|
+
function yes(v) { return /^(有|はい|○|済|y|yes|true|1)$/i.test(String(v || '').trim()); }
|
|
34
|
+
function splitCsv(line) { return line.split(',').map(function (c) { return c.trim().replace(/^"|"$/g, ''); }); }
|
|
35
|
+
function fuyoKind(age, rel, dokyo) {
|
|
36
|
+
if (age < 16) return '年少';
|
|
37
|
+
if (age >= 19 && age < 23) return '特定扶養';
|
|
38
|
+
if (age >= 70) return (CHOKKEI.test(rel) && dokyo) ? '同居老親' : '老人扶養';
|
|
39
|
+
return '一般扶養';
|
|
40
|
+
}
|
|
41
|
+
function correct(p) {
|
|
42
|
+
if (p.rel === '配偶者') {
|
|
43
|
+
if (p.inc <= LIMIT.spouse) return { kind: '配偶者控除', amt: p.age >= 70 ? 480000 : 380000 };
|
|
44
|
+
if (p.inc <= LIMIT.spouseSp) return { kind: '配偶者特別控除', amt: band(SPOUSE_SP, p.inc) };
|
|
45
|
+
return { kind: '対象外', amt: 0 };
|
|
46
|
+
}
|
|
47
|
+
if (p.abroad && p.age >= 30 && p.age < 70 && !p.ryugaku && !p.shogai && p.soukin < 380000) return { kind: '対象外', amt: 0 };
|
|
48
|
+
if (p.inc <= LIMIT.fuyo) { var k = fuyoKind(p.age, p.rel, p.dokyo && !p.abroad); return { kind: k, amt: FUYO_AMT[k] || 0 }; }
|
|
49
|
+
if (p.age >= 19 && p.age < 23 && p.inc <= LIMIT.tokutei) return { kind: '特定親族', amt: band(TOKUTEI, p.inc) };
|
|
50
|
+
return { kind: '対象外', amt: 0 };
|
|
51
|
+
}
|
|
52
|
+
function claimedAmt(p) {
|
|
53
|
+
if (FUYO_AMT[p.cls]) return FUYO_AMT[p.cls];
|
|
54
|
+
if (p.cls === '配偶者控除') return p.age >= 70 ? 480000 : 380000;
|
|
55
|
+
if (p.cls === '配偶者特別控除') return band(SPOUSE_SP, p.inc) || 0;
|
|
56
|
+
if (p.cls === '特定親族') return (p.inc > LIMIT.fuyo ? band(TOKUTEI, p.inc) : 0);
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function check(text, opts) {
|
|
61
|
+
opts = opts || {};
|
|
62
|
+
var year = opts.year || (opts.today ? +String(opts.today).slice(0, 4) : 2026);
|
|
63
|
+
var findings = [], missed = 0, over = 0, rows = 0;
|
|
64
|
+
function add(id, line, extra) { var r = BY_ID[id]; findings.push({ check: id, sev: r.sev, line: line, msg: r.title + (extra ? '(' + extra + ')' : '') + ' → ' + r.fix + ' [' + r.law + ']' }); }
|
|
65
|
+
var lines = String(text || '').replace(/^/, '').split(/\r?\n/);
|
|
66
|
+
var hi = lines.findIndex(function (l) { return /氏名/.test(l) && /区分/.test(l); });
|
|
67
|
+
if (hi < 0) { add('BAD_ROW', 1, '見出し行(氏名・続柄・生年月日・区分・合計所得…)がない'); return { findings: findings, summary: { rows: 0, missed: 0, over: 0 } }; }
|
|
68
|
+
var H = splitCsv(lines[hi]); function col(c, n) { var i = H.indexOf(n); return i < 0 ? '' : (c[i] || ''); }
|
|
69
|
+
for (var i = hi + 1; i < lines.length; i++) {
|
|
70
|
+
if (!lines[i].trim()) continue;
|
|
71
|
+
var c = splitCsv(lines[i]), ln = i + 1, A = ageAt(col(c, '生年月日'), year);
|
|
72
|
+
var p = { name: col(c, '氏名'), rel: col(c, '続柄'), cls: col(c, '区分') || '対象外', inc: money(col(c, '合計所得')), sal: money(col(c, '給与収入')),
|
|
73
|
+
dokyo: yes(col(c, '同居')), abroad: yes(col(c, '国外')), shogai: yes(col(c, '障害者')), ryugaku: yes(col(c, '留学')), soukin: money(col(c, '送金額')),
|
|
74
|
+
docRel: yes(col(c, '親族関係書類')), docSou: yes(col(c, '送金関係書類')) };
|
|
75
|
+
if (!A || isNaN(p.inc) || isNaN(p.sal) || isNaN(p.soukin)) { add('BAD_ROW', ln, p.name || ('行' + ln)); continue; }
|
|
76
|
+
rows++; p.age = A.age; var who = p.name + '・' + year + '/12/31で' + p.age + '歳';
|
|
77
|
+
if (p.sal > 0 && Math.abs(kyuyoShotoku(p.sal) - p.inc) > 1000) add('KYUYO_KOJO_OLD', ln, who + '・給与' + yen(p.sal) + 'なら所得は' + yen(kyuyoShotoku(p.sal)) + '、ファイルは' + yen(p.inc));
|
|
78
|
+
if (p.sal > 0) p.inc = kyuyoShotoku(p.sal);
|
|
79
|
+
var ok = correct(p), got = claimedAmt(p), isFuyo = !!FUYO_AMT[p.cls];
|
|
80
|
+
if (A.jan1 && fuyoKind(A.age, p.rel, true) !== fuyoKind(A.naive, p.rel, true)) add('JAN1_BIRTHDAY', ln, who + '(生年だけで数えると' + A.naive + '歳)');
|
|
81
|
+
if (isFuyo && p.age < 16) add('NENSHO_CLAIMED', ln, who);
|
|
82
|
+
if (p.cls === '特定扶養' && !(p.age >= 19 && p.age < 23)) add('TOKUTEI_AGE', ln, who);
|
|
83
|
+
if ((p.cls === '老人扶養' || p.cls === '同居老親') && p.age < 70) add('ROUJIN_AGE', ln, who);
|
|
84
|
+
if (p.cls === '一般扶養' && p.age >= 70 && p.inc <= LIMIT.fuyo) add('ROUJIN_MISSED', ln, who + '・正しくは' + ok.kind + ' ' + yen(ok.amt));
|
|
85
|
+
if (p.cls === '同居老親' && p.age >= 70 && (!CHOKKEI.test(p.rel) || !p.dokyo || p.abroad)) add('DOUKYO_ROUSHIN_REQ', ln, who + '・続柄' + p.rel + '・同居' + (p.dokyo ? '有' : '無'));
|
|
86
|
+
if (isFuyo && p.inc > LIMIT.fuyo) add('FUYO_INCOME_OVER', ln, who + '・所得' + yen(p.inc));
|
|
87
|
+
if (p.rel !== '配偶者' && (p.cls === '対象外' || p.cls === '') && ok.kind in FUYO_AMT) add('FUYO_MISSED', ln, who + '・所得' + yen(p.inc) + '・' + ok.kind + ' ' + yen(ok.amt));
|
|
88
|
+
if (p.cls !== '特定親族' && ok.kind === '特定親族') add('TOKUTEI_SHINZOKU_MISSED', ln, who + '・所得' + yen(p.inc) + '・控除' + yen(ok.amt));
|
|
89
|
+
if (p.cls === '特定親族' && ok.kind !== '特定親族') add('TOKUTEI_SHINZOKU_RANGE', ln, who + '・所得' + yen(p.inc) + '・正しくは' + ok.kind);
|
|
90
|
+
if (p.cls === '配偶者控除' && p.inc > LIMIT.spouse) add('SPOUSE_KOJO_OVER', ln, who + '・所得' + yen(p.inc) + '・正しくは' + ok.kind + ' ' + yen(ok.amt));
|
|
91
|
+
if (p.cls === '配偶者特別控除' && (p.inc <= LIMIT.spouse || p.inc > LIMIT.spouseSp)) add('SPOUSE_SPECIAL_RANGE', ln, who + '・所得' + yen(p.inc));
|
|
92
|
+
if (p.rel === '配偶者' && (p.cls === '対象外' || p.cls === '') && ok.amt > 0) add('SPOUSE_MISSED', ln, who + '・所得' + yen(p.inc) + '・' + ok.kind + ' ' + yen(ok.amt));
|
|
93
|
+
var claimedAbroad = p.abroad && CLAIMED.indexOf(p.cls) >= 0;
|
|
94
|
+
if (claimedAbroad && p.age >= 30 && p.age < 70 && !p.ryugaku && !p.shogai && p.soukin < 380000) add('NONRES_30_69', ln, who + '・送金' + yen(p.soukin));
|
|
95
|
+
if (claimedAbroad && !p.docRel) add('NONRES_SHINZOKU_DOC', ln, who);
|
|
96
|
+
if (claimedAbroad && !p.docSou) add('NONRES_SOUKIN_DOC', ln, who);
|
|
97
|
+
if (p.abroad && p.ryugaku && p.age >= 30 && p.age < 70) add('NONRES_RYUGAKU_DOC', ln, who);
|
|
98
|
+
if (ok.amt > got) missed += ok.amt - got; else over += got - ok.amt;
|
|
99
|
+
}
|
|
100
|
+
findings.push({ check: 'SUMMARY', sev: 'info', line: hi + 1, msg: year + '年分(令和8年分)で' + rows + '人を判定:控除漏れ ' + yen(missed) + '・過大控除 ' + yen(over) + '(本人の合計所得900万円以下として計算)' });
|
|
101
|
+
return { findings: findings, summary: { rows: rows, missed: missed, over: over } };
|
|
102
|
+
}
|
|
103
|
+
var api = { engine: { check: check, kyuyoShotoku: kyuyoShotoku }, RULES: RULES, RULE_COUNT: RULES.length };
|
|
104
|
+
if (typeof module !== 'undefined') module.exports = api;
|
|
105
|
+
if (typeof window !== 'undefined') window.NENCHO_ENGINE = api;
|
|
106
|
+
})();
|
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 = '9fe0e68e-dab9-4484-92f2-899cfc5842b6'; // 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_QYWqS9lHXsKzdj2d8EtKinHzfk0YQHF5Xxzw311jfI1';
|
|
7
|
+
const SLUG = 'nencho-fuyo-csv-lint-r8';
|
|
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,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@readystack/nencho-fuyo-csv-lint-r8",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "扶養親族CSVを令和8年分の新要件62万円・74万円・特定親族特別控除で19ルール検査",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"年末調整",
|
|
11
|
+
"扶養控除",
|
|
12
|
+
"令和8年分",
|
|
13
|
+
"特定親族特別控除",
|
|
14
|
+
"配偶者特別控除",
|
|
15
|
+
"国外居住親族",
|
|
16
|
+
"給与計算",
|
|
17
|
+
"nenmatsu chosei"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://getreadystack.com",
|
|
20
|
+
"funding": "https://buy.polar.sh/polar_cl_QYWqS9lHXsKzdj2d8EtKinHzfk0YQHF5Xxzw311jfI1",
|
|
21
|
+
"bin": {
|
|
22
|
+
"nencho-fuyo-csv-lint-r8": "cli.js"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"mcpName": "io.github.jmshinhwa/nencho-fuyo-csv-lint-r8",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/jmshinhwa/readystack-themes.git",
|
|
31
|
+
"directory": "nencho-fuyo-csv-lint-r8"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"cli.js",
|
|
35
|
+
"license.js",
|
|
36
|
+
"rules.json",
|
|
37
|
+
"strings.json",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE.txt",
|
|
40
|
+
"engine.js"
|
|
41
|
+
]
|
|
42
|
+
}
|
package/rules.json
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "NENSHO_CLAIMED",
|
|
4
|
+
"sev": "error",
|
|
5
|
+
"law": "所得税法2条1項34号の2",
|
|
6
|
+
"title": "16歳未満を扶養控除に入れている",
|
|
7
|
+
"fix": "区分を「年少」に(所得税の扶養控除なし・住民税の申告欄のみ)"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "TOKUTEI_AGE",
|
|
11
|
+
"sev": "error",
|
|
12
|
+
"law": "所得税法2条1項34号の3",
|
|
13
|
+
"title": "特定扶養の年齢要件(12/31現在19歳以上23歳未満)を満たさない",
|
|
14
|
+
"fix": "年齢に合う区分(一般扶養・老人扶養)に直す"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "ROUJIN_AGE",
|
|
18
|
+
"sev": "error",
|
|
19
|
+
"law": "所得税法2条1項34号の4",
|
|
20
|
+
"title": "老人扶養・同居老親なのに12/31現在70歳未満",
|
|
21
|
+
"fix": "一般扶養(38万円)に直す"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "ROUJIN_MISSED",
|
|
25
|
+
"sev": "warning",
|
|
26
|
+
"law": "所得税法2条1項34号の4",
|
|
27
|
+
"title": "70歳以上なのに一般扶養のまま",
|
|
28
|
+
"fix": "老人扶養(48万円)または同居老親(58万円)に直す"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "DOUKYO_ROUSHIN_REQ",
|
|
32
|
+
"sev": "error",
|
|
33
|
+
"law": "租税特別措置法41条の16",
|
|
34
|
+
"title": "同居老親の要件(直系尊属・同居)を満たさない",
|
|
35
|
+
"fix": "老人扶養(48万円)に直す"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "FUYO_INCOME_OVER",
|
|
39
|
+
"sev": "error",
|
|
40
|
+
"law": "所得税法2条1項34号(令和8年分 62万円)",
|
|
41
|
+
"title": "扶養親族の合計所得が62万円を超えている",
|
|
42
|
+
"fix": "19〜22歳で123万円以下なら特定親族特別控除、それ以外は対象外"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "KYUYO_KOJO_OLD",
|
|
46
|
+
"sev": "warning",
|
|
47
|
+
"law": "所得税法28条3項(令和8年分 最低74万円)",
|
|
48
|
+
"title": "合計所得が令和8年分の給与所得控除(最低74万円)で計算されていない",
|
|
49
|
+
"fix": "給与収入から令和8年分の給与所得控除を引き直す"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "FUYO_MISSED",
|
|
53
|
+
"sev": "warning",
|
|
54
|
+
"law": "所得税法84条",
|
|
55
|
+
"title": "所得62万円以下・16歳以上なのに扶養控除の対象外になっている",
|
|
56
|
+
"fix": "年齢に合う扶養区分を付ける(旧基準48万・58万円で外した可能性)"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "TOKUTEI_SHINZOKU_MISSED",
|
|
60
|
+
"sev": "warning",
|
|
61
|
+
"law": "所得税法84条の2",
|
|
62
|
+
"title": "19〜22歳・所得62万円超123万円以下なのに特定親族特別控除がない",
|
|
63
|
+
"fix": "区分を「特定親族」にし、特定親族特別控除申告書を回収する"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "TOKUTEI_SHINZOKU_RANGE",
|
|
67
|
+
"sev": "error",
|
|
68
|
+
"law": "所得税法84条の2",
|
|
69
|
+
"title": "特定親族特別控除の要件(19〜22歳・所得62万円超123万円以下)を外れている",
|
|
70
|
+
"fix": "62万円以下なら特定扶養(63万円)、123万円超なら対象外"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "SPOUSE_KOJO_OVER",
|
|
74
|
+
"sev": "error",
|
|
75
|
+
"law": "所得税法83条(令和8年分 62万円)",
|
|
76
|
+
"title": "配偶者控除なのに配偶者の合計所得が62万円を超えている",
|
|
77
|
+
"fix": "133万円以下なら配偶者特別控除に直す"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "SPOUSE_SPECIAL_RANGE",
|
|
81
|
+
"sev": "error",
|
|
82
|
+
"law": "所得税法83条の2",
|
|
83
|
+
"title": "配偶者特別控除の範囲(62万円超133万円以下)を外れている",
|
|
84
|
+
"fix": "62万円以下なら配偶者控除、133万円超なら対象外"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"id": "SPOUSE_MISSED",
|
|
88
|
+
"sev": "warning",
|
|
89
|
+
"law": "所得税法83条・83条の2",
|
|
90
|
+
"title": "配偶者の所得133万円以下なのに配偶者の控除がない",
|
|
91
|
+
"fix": "本人の合計所得1,000万円以下なら配偶者控除または配偶者特別控除を付ける"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "NONRES_30_69",
|
|
95
|
+
"sev": "error",
|
|
96
|
+
"law": "所得税法2条1項34号の2ロ",
|
|
97
|
+
"title": "国外居住の30〜69歳で、留学・障害者・送金38万円以上のどれにも当たらない",
|
|
98
|
+
"fix": "対象外にする(令和5年分以後)"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"id": "NONRES_SHINZOKU_DOC",
|
|
102
|
+
"sev": "error",
|
|
103
|
+
"law": "所得税法194条4項",
|
|
104
|
+
"title": "国外居住親族の親族関係書類が未回収",
|
|
105
|
+
"fix": "戸籍の附票の写し等または外国政府発行の書類(訳文付き)を回収する"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"id": "NONRES_SOUKIN_DOC",
|
|
109
|
+
"sev": "error",
|
|
110
|
+
"law": "所得税法190条2号ハ",
|
|
111
|
+
"title": "国外居住親族の送金関係書類が未回収",
|
|
112
|
+
"fix": "年末調整までに送金関係書類(30〜69歳の送金要件なら38万円送金書類)を回収する"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"id": "NONRES_RYUGAKU_DOC",
|
|
116
|
+
"sev": "info",
|
|
117
|
+
"law": "所得税法194条5項",
|
|
118
|
+
"title": "留学で国外居住の30〜69歳には留学ビザ等書類が必要",
|
|
119
|
+
"fix": "外国政府発行の留学の在留資格を証する書類を回収する"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"id": "JAN1_BIRTHDAY",
|
|
123
|
+
"sev": "warning",
|
|
124
|
+
"law": "年齢計算ニ関スル法律",
|
|
125
|
+
"title": "1月1日生まれは12/31に1歳上になり、区分の境目が変わる",
|
|
126
|
+
"fix": "年齢は(2026 − 生年 + 1)で判定する"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"id": "BAD_ROW",
|
|
130
|
+
"sev": "warning",
|
|
131
|
+
"law": "—",
|
|
132
|
+
"title": "列の欠け・日付や金額が読めない行",
|
|
133
|
+
"fix": "生年月日は YYYY-MM-DD、金額は円の数字で書く"
|
|
134
|
+
}
|
|
135
|
+
]
|
package/strings.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "年末調整 扶養控除 チェック(令和8年分・62万円)",
|
|
3
|
+
"subtitle": "扶養親族CSVを令和8年分の新要件62万円・74万円・特定親族特別控除で19ルール検査",
|
|
4
|
+
"bin": "nencho-fuyo-csv-lint-r8",
|
|
5
|
+
"price": 29,
|
|
6
|
+
"free": "開いている扶養親族CSV 1ファイルを19ルールで全行チェックし、控除漏れ・過大控除の金額と根拠条文を行ごとに表示する(サンプル7人で11件の指摘・控除漏れ ¥1,170,000・過大控除 ¥860,000)",
|
|
7
|
+
"paid": "チェック結果を従業員ごとの修正一覧レポートとして書き出し、年末調整の確認記録として保存する($29・1回払い・1人またはチーム1席に1キー)",
|
|
8
|
+
"need_key": "This option needs a licence (年末調整 扶養控除 チェック(令和8年分・62万円)).",
|
|
9
|
+
"exts": []
|
|
10
|
+
}
|