@ryuenn3123/agentic-senior-core 4.0.2 → 4.1.0
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/.agent-context/rules/api-docs.md +14 -0
- package/.agent-context/rules/api-versioning.md +93 -0
- package/.agent-context/rules/background-jobs.md +93 -0
- package/.agent-context/rules/config-and-flags.md +79 -0
- package/.agent-context/rules/database-design.md +32 -0
- package/.agent-context/rules/frontend-architecture.md +35 -0
- package/.agent-context/rules/migrations.md +84 -0
- package/.agent-context/rules/observability.md +69 -0
- package/.agent-context/rules/resilience.md +78 -0
- package/.agent-context/rules/security.md +28 -0
- package/AGENTS.md +8 -8
- package/README.md +42 -9
- package/bin/agentic-senior-core.js +6 -0
- package/lib/cli/audits/typography-palette-anti-repeat/color-utils.mjs +156 -0
- package/lib/cli/audits/typography-palette-anti-repeat/file-scanner.mjs +103 -0
- package/lib/cli/audits/typography-palette-anti-repeat/typography-utils.mjs +70 -0
- package/lib/cli/audits/typography-palette-anti-repeat-audit.mjs +255 -0
- package/lib/cli/commands/audit-design-anti-repeat.mjs +198 -0
- package/lib/cli/commands/upgrade.mjs +1 -0
- package/lib/cli/utils.mjs +1 -0
- package/package.json +4 -4
- package/scripts/audit-cache-layer-contract.mjs +5 -0
- package/scripts/audit-caching-scope-hygiene.mjs +5 -0
- package/scripts/audit-typography-palette-anti-repeat.mjs +120 -0
- package/scripts/clean-local-artifacts.mjs +0 -1
- package/scripts/frontend-usability-audit.mjs +5 -8
- package/scripts/release-gate/static-checks.mjs +7 -7
- package/scripts/validate/config.mjs +0 -2
- package/scripts/validate/coverage-checks.mjs +1 -42
- package/scripts/validate.mjs +42 -7
- package/scripts/migrate-rule-format/id-prefix-table.mjs +0 -37
- package/scripts/migrate-rule-format/parse-legacy.mjs +0 -180
- package/scripts/migrate-rule-format/render-new.mjs +0 -169
- package/scripts/migrate-rule-format/roundtrip-validate.mjs +0 -89
- package/scripts/migrate-rule-format.mjs +0 -192
- package/scripts/v3-purge-audit.mjs +0 -236
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Roundtrip substance validator.
|
|
5
|
-
*
|
|
6
|
-
* After rendering the new format, we extract the substantial-word set from
|
|
7
|
-
* both the original v3 file and the rendered v4 file, then compute set overlap.
|
|
8
|
-
* Drop in overlap below the threshold means the migration almost certainly
|
|
9
|
-
* lost real content; the helper surfaces the lost words so the human migrator
|
|
10
|
-
* can decide whether the loss is intentional (renamed terms) or a bug.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const STOPWORD_SET = new Set([
|
|
14
|
-
'the', 'and', 'for', 'with', 'that', 'this', 'from', 'into', 'are', 'was',
|
|
15
|
-
'were', 'has', 'have', 'had', 'not', 'but', 'can', 'will', 'must', 'use',
|
|
16
|
-
'used', 'using', 'when', 'then', 'than', 'they', 'their', 'them', 'who',
|
|
17
|
-
'what', 'why', 'how', 'all', 'any', 'one', 'two', 'three', 'four', 'five',
|
|
18
|
-
'six', 'seven', 'eight', 'nine', 'ten', 'each', 'such', 'some', 'most',
|
|
19
|
-
'more', 'less', 'only', 'also', 'just', 'over', 'under', 'between', 'across',
|
|
20
|
-
'before', 'after', 'because', 'while', 'until', 'unless', 'within',
|
|
21
|
-
'without', 'inside', 'outside', 'about', 'around', 'against', 'through',
|
|
22
|
-
'throughout', 'during', 'including', 'include', 'includes', 'see', 'note',
|
|
23
|
-
'rule', 'rules', 'agent', 'agents', 'project', 'repo', 'code', 'file',
|
|
24
|
-
'files', 'item', 'items', 'list', 'lists', 'thing', 'things', 'value',
|
|
25
|
-
'values', 'should', 'shall', 'may', 'might', 'could', 'would', 'does',
|
|
26
|
-
'doing', 'done', 'make', 'makes', 'making', 'made', 'set', 'sets',
|
|
27
|
-
]);
|
|
28
|
-
|
|
29
|
-
function tokenize(text) {
|
|
30
|
-
const lowered = text.toLowerCase();
|
|
31
|
-
// Strip fenced code blocks first (multi-line ``` ... ``` spans).
|
|
32
|
-
const noFenced = lowered.replace(/```[\s\S]*?```/g, ' ');
|
|
33
|
-
// Strip inline code spans, but only within a single line so that an
|
|
34
|
-
// unmatched backtick on a code-heavy line cannot eat the rest of the file.
|
|
35
|
-
const noInline = noFenced.replace(/`[^`\n]+`/g, ' ');
|
|
36
|
-
const words = noInline.match(/[a-z][a-z0-9]+(?:-[a-z0-9]+)*/g) ?? [];
|
|
37
|
-
return words.filter((word) => word.length >= 4 && !STOPWORD_SET.has(word));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @param {string} originalSourceText
|
|
42
|
-
* @param {string} renderedSourceText
|
|
43
|
-
* @param {{ minimumOverlapPercent?: number }} [options]
|
|
44
|
-
* @returns {{
|
|
45
|
-
* passed: boolean,
|
|
46
|
-
* originalSubstantialWordCount: number,
|
|
47
|
-
* renderedSubstantialWordCount: number,
|
|
48
|
-
* overlapPercent: number,
|
|
49
|
-
* lostWords: string[],
|
|
50
|
-
* newWords: string[],
|
|
51
|
-
* minimumRequired: number,
|
|
52
|
-
* }}
|
|
53
|
-
*/
|
|
54
|
-
export function roundtripSubstanceCheck(originalSourceText, renderedSourceText, options = {}) {
|
|
55
|
-
const minimumOverlapPercent = options.minimumOverlapPercent ?? 95;
|
|
56
|
-
const originalWordCounts = new Map();
|
|
57
|
-
for (const word of tokenize(originalSourceText)) {
|
|
58
|
-
originalWordCounts.set(word, (originalWordCounts.get(word) || 0) + 1);
|
|
59
|
-
}
|
|
60
|
-
const renderedWordSet = new Set(tokenize(renderedSourceText));
|
|
61
|
-
|
|
62
|
-
const lostWords = [];
|
|
63
|
-
let preservedDistinctWordCount = 0;
|
|
64
|
-
for (const [word, count] of originalWordCounts.entries()) {
|
|
65
|
-
if (renderedWordSet.has(word)) {
|
|
66
|
-
preservedDistinctWordCount += 1;
|
|
67
|
-
} else {
|
|
68
|
-
lostWords.push(`${word} (x${count})`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const originalDistinctCount = originalWordCounts.size;
|
|
73
|
-
const overlapPercent = originalDistinctCount > 0
|
|
74
|
-
? (preservedDistinctWordCount / originalDistinctCount) * 100
|
|
75
|
-
: 100;
|
|
76
|
-
|
|
77
|
-
const originalWordSet = new Set(originalWordCounts.keys());
|
|
78
|
-
const newWords = [...renderedWordSet].filter((word) => !originalWordSet.has(word));
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
passed: overlapPercent >= minimumOverlapPercent,
|
|
82
|
-
originalSubstantialWordCount: originalDistinctCount,
|
|
83
|
-
renderedSubstantialWordCount: renderedWordSet.size,
|
|
84
|
-
overlapPercent: Math.round(overlapPercent * 100) / 100,
|
|
85
|
-
lostWords: lostWords.sort().slice(0, 50),
|
|
86
|
-
newWords: newWords.sort().slice(0, 50),
|
|
87
|
-
minimumRequired: minimumOverlapPercent,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// @ts-check
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* scripts/migrate-rule-format.mjs
|
|
6
|
-
*
|
|
7
|
-
* Phase 1 Task 1.2 migration helper. Converts a legacy v3 rule file into the
|
|
8
|
-
* canonical v4 format defined in docs/architecture/format-spec.md. Output is written
|
|
9
|
-
* to a `.candidate.md` sibling so the human migrator can review the diff
|
|
10
|
-
* before replacing the original.
|
|
11
|
-
*
|
|
12
|
-
* Usage:
|
|
13
|
-
* node scripts/migrate-rule-format.mjs <path-to-rule-file>
|
|
14
|
-
* node scripts/migrate-rule-format.mjs <path> --json
|
|
15
|
-
* node scripts/migrate-rule-format.mjs <path> --apply
|
|
16
|
-
*
|
|
17
|
-
* Flags:
|
|
18
|
-
* --json Print the structured report to stdout (JSON only, no human prose).
|
|
19
|
-
* --apply Overwrite the source file with the rendered v4 content. Default
|
|
20
|
-
* behavior writes a .candidate.md file and leaves the source alone.
|
|
21
|
-
*
|
|
22
|
-
* The helper does not commit, does not stage, and does not call any network.
|
|
23
|
-
*
|
|
24
|
-
* Exit codes:
|
|
25
|
-
* 0 — rendered cleanly + roundtrip overlap >= 95%
|
|
26
|
-
* 1 — roundtrip overlap below threshold OR parser warnings present
|
|
27
|
-
* 2 — input path missing or filename not in the locked prefix table
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
31
|
-
import { basename, dirname, resolve } from 'node:path';
|
|
32
|
-
import { fileURLToPath } from 'node:url';
|
|
33
|
-
|
|
34
|
-
import { countTokens } from '../benchmarks/token-usage/lib/token-counter.mjs';
|
|
35
|
-
import { getPrefixEntry } from './migrate-rule-format/id-prefix-table.mjs';
|
|
36
|
-
import { parseLegacyRuleFile } from './migrate-rule-format/parse-legacy.mjs';
|
|
37
|
-
import { renderNewFormat } from './migrate-rule-format/render-new.mjs';
|
|
38
|
-
import { roundtripSubstanceCheck } from './migrate-rule-format/roundtrip-validate.mjs';
|
|
39
|
-
|
|
40
|
-
const SCRIPT_FILE_PATH = fileURLToPath(import.meta.url);
|
|
41
|
-
const REPOSITORY_ROOT = resolve(dirname(SCRIPT_FILE_PATH), '..');
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @param {string} ruleFileAbsolutePath
|
|
45
|
-
* @returns {Promise<{
|
|
46
|
-
* sourcePath: string,
|
|
47
|
-
* filename: string,
|
|
48
|
-
* prefix: string,
|
|
49
|
-
* sectionAssignments: Array<{ sectionTitle: string, sectionId: string, itemCount: number }>,
|
|
50
|
-
* warnings: string[],
|
|
51
|
-
* roundtrip: ReturnType<typeof roundtripSubstanceCheck>,
|
|
52
|
-
* tokenSavings: { original: number, rendered: number, deltaPercent: number },
|
|
53
|
-
* candidatePath: string,
|
|
54
|
-
* rendered: string,
|
|
55
|
-
* }>}
|
|
56
|
-
*/
|
|
57
|
-
export async function migrateOneRuleFile(ruleFileAbsolutePath) {
|
|
58
|
-
const filename = basename(ruleFileAbsolutePath);
|
|
59
|
-
const prefixEntry = getPrefixEntry(filename);
|
|
60
|
-
const originalSource = readFileSync(ruleFileAbsolutePath, 'utf8');
|
|
61
|
-
const parsed = parseLegacyRuleFile(originalSource);
|
|
62
|
-
const { rendered, sectionAssignments, warnings } = renderNewFormat(prefixEntry, parsed);
|
|
63
|
-
const roundtrip = roundtripSubstanceCheck(originalSource, rendered);
|
|
64
|
-
|
|
65
|
-
const originalTokenCount = await countTokens(originalSource, 'openai', 'gpt-4o-2024-08-06');
|
|
66
|
-
const renderedTokenCount = await countTokens(rendered, 'openai', 'gpt-4o-2024-08-06');
|
|
67
|
-
const tokenSavings = {
|
|
68
|
-
original: originalTokenCount.token_count,
|
|
69
|
-
rendered: renderedTokenCount.token_count,
|
|
70
|
-
deltaPercent: Math.round(
|
|
71
|
-
((renderedTokenCount.token_count - originalTokenCount.token_count) / originalTokenCount.token_count) * 10000,
|
|
72
|
-
) / 100,
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const candidatePath = ruleFileAbsolutePath.replace(/\.md$/, '.candidate.md');
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
sourcePath: ruleFileAbsolutePath,
|
|
79
|
-
filename,
|
|
80
|
-
prefix: prefixEntry.prefix,
|
|
81
|
-
sectionAssignments,
|
|
82
|
-
warnings,
|
|
83
|
-
roundtrip,
|
|
84
|
-
tokenSavings,
|
|
85
|
-
candidatePath,
|
|
86
|
-
rendered,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function formatHumanReport(report) {
|
|
91
|
-
const lines = [];
|
|
92
|
-
lines.push('=================================================');
|
|
93
|
-
lines.push(` migrate-rule-format: ${report.filename}`);
|
|
94
|
-
lines.push('=================================================');
|
|
95
|
-
lines.push(` Prefix : ${report.prefix}`);
|
|
96
|
-
lines.push(` Section count : ${report.sectionAssignments.length}`);
|
|
97
|
-
lines.push(` Token (orig) : ${report.tokenSavings.original}`);
|
|
98
|
-
lines.push(` Token (new) : ${report.tokenSavings.rendered}`);
|
|
99
|
-
const sign = report.tokenSavings.deltaPercent <= 0 ? '' : '+';
|
|
100
|
-
lines.push(` Token delta : ${sign}${report.tokenSavings.deltaPercent}%`);
|
|
101
|
-
lines.push(` Roundtrip : ${report.roundtrip.passed ? 'PASS' : 'FAIL'} (${report.roundtrip.overlapPercent}% overlap, min ${report.roundtrip.minimumRequired}%)`);
|
|
102
|
-
lines.push('');
|
|
103
|
-
|
|
104
|
-
lines.push(' Sections assigned:');
|
|
105
|
-
for (const assignment of report.sectionAssignments) {
|
|
106
|
-
lines.push(` ${assignment.sectionId.padEnd(12)} ${assignment.itemCount} items ${assignment.sectionTitle}`);
|
|
107
|
-
}
|
|
108
|
-
lines.push('');
|
|
109
|
-
|
|
110
|
-
if (report.warnings.length > 0) {
|
|
111
|
-
lines.push(' Warnings:');
|
|
112
|
-
for (const warning of report.warnings) {
|
|
113
|
-
lines.push(` ! ${warning}`);
|
|
114
|
-
}
|
|
115
|
-
lines.push('');
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (report.roundtrip.lostWords.length > 0) {
|
|
119
|
-
lines.push(` Substantial words present in original, missing in rendered (${report.roundtrip.lostWords.length} of distinct):`);
|
|
120
|
-
for (const word of report.roundtrip.lostWords.slice(0, 30)) {
|
|
121
|
-
lines.push(` - ${word}`);
|
|
122
|
-
}
|
|
123
|
-
if (report.roundtrip.lostWords.length > 30) {
|
|
124
|
-
lines.push(` ... ${report.roundtrip.lostWords.length - 30} more (see JSON report)`);
|
|
125
|
-
}
|
|
126
|
-
lines.push('');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return lines.join('\n');
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async function runCli() {
|
|
133
|
-
const argv = process.argv.slice(2);
|
|
134
|
-
if (argv.length === 0) {
|
|
135
|
-
console.error('usage: node scripts/migrate-rule-format.mjs <path-to-rule-file> [--json] [--apply]');
|
|
136
|
-
process.exit(2);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const inputPath = argv[0];
|
|
140
|
-
const jsonMode = argv.includes('--json');
|
|
141
|
-
const applyMode = argv.includes('--apply');
|
|
142
|
-
const ruleFileAbsolutePath = resolve(REPOSITORY_ROOT, inputPath);
|
|
143
|
-
|
|
144
|
-
let report;
|
|
145
|
-
try {
|
|
146
|
-
report = await migrateOneRuleFile(ruleFileAbsolutePath);
|
|
147
|
-
} catch (migrationError) {
|
|
148
|
-
if (jsonMode) {
|
|
149
|
-
process.stdout.write(`${JSON.stringify({ passed: false, error: migrationError.message }, null, 2)}\n`);
|
|
150
|
-
} else {
|
|
151
|
-
console.error(`migrate-rule-format failed: ${migrationError.message}`);
|
|
152
|
-
}
|
|
153
|
-
process.exit(2);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (applyMode) {
|
|
157
|
-
writeFileSync(ruleFileAbsolutePath, report.rendered, 'utf8');
|
|
158
|
-
} else {
|
|
159
|
-
writeFileSync(report.candidatePath, report.rendered, 'utf8');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const exitCode = report.roundtrip.passed && report.warnings.length === 0 ? 0 : 1;
|
|
163
|
-
|
|
164
|
-
if (jsonMode) {
|
|
165
|
-
process.stdout.write(`${JSON.stringify({
|
|
166
|
-
passed: report.roundtrip.passed,
|
|
167
|
-
filename: report.filename,
|
|
168
|
-
prefix: report.prefix,
|
|
169
|
-
sectionCount: report.sectionAssignments.length,
|
|
170
|
-
sectionAssignments: report.sectionAssignments,
|
|
171
|
-
warnings: report.warnings,
|
|
172
|
-
roundtrip: report.roundtrip,
|
|
173
|
-
tokenSavings: report.tokenSavings,
|
|
174
|
-
candidatePath: applyMode ? report.sourcePath : report.candidatePath,
|
|
175
|
-
mode: applyMode ? 'apply' : 'candidate',
|
|
176
|
-
}, null, 2)}\n`);
|
|
177
|
-
process.exit(exitCode);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
console.log(formatHumanReport(report));
|
|
181
|
-
if (applyMode) {
|
|
182
|
-
console.log(` APPLIED in place: ${report.sourcePath}`);
|
|
183
|
-
} else {
|
|
184
|
-
console.log(` Candidate written: ${report.candidatePath}`);
|
|
185
|
-
console.log(' Review the diff, then re-run with --apply when satisfied.');
|
|
186
|
-
}
|
|
187
|
-
process.exit(exitCode);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}` || process.argv[1].endsWith('migrate-rule-format.mjs')) {
|
|
191
|
-
runCli();
|
|
192
|
-
}
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { readdir, readFile, stat, writeFile, mkdir } from 'node:fs/promises';
|
|
4
|
-
import { dirname, extname, join, relative, resolve } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
|
|
7
|
-
const SCRIPT_FILE_PATH = fileURLToPath(import.meta.url);
|
|
8
|
-
const ROOT_DIR = resolve(dirname(SCRIPT_FILE_PATH), '..');
|
|
9
|
-
const STATE_OUTPUT_PATH = join(ROOT_DIR, '.agent-context', 'state', 'v3-purge-audit.json');
|
|
10
|
-
|
|
11
|
-
const STATIC_DIRECTORIES = [
|
|
12
|
-
'.agent-context/stacks',
|
|
13
|
-
'.agent-context/blueprints',
|
|
14
|
-
'.agent-context/profiles',
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
const REFERENCE_TOKENS = [
|
|
18
|
-
'.agent-context/stacks/',
|
|
19
|
-
'.agent-context/blueprints/',
|
|
20
|
-
'.agent-context/profiles/',
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
const NON_BLOCKING_REFERENCE_FILE_PATHS = new Set([
|
|
24
|
-
'.agent-context/state/v3-purge-audit.json',
|
|
25
|
-
'scripts/v3-purge-audit.mjs',
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
|
-
const RUNTIME_BLOCKING_PATH_PATTERNS = [
|
|
29
|
-
/^AGENTS\.md$/,
|
|
30
|
-
/^CLAUDE\.md$/,
|
|
31
|
-
/^GEMINI\.md$/,
|
|
32
|
-
/^\.instructions\.md$/,
|
|
33
|
-
/^\.github\/copilot-instructions\.md$/,
|
|
34
|
-
/^\.github\/instructions\/agentic-senior-core\.instructions\.md$/,
|
|
35
|
-
/^\.gemini\/instructions\.md$/,
|
|
36
|
-
/^\.cursor\/rules\/agentic-senior-core\.mdc$/,
|
|
37
|
-
/^\.windsurf\/rules\/agentic-senior-core\.md$/,
|
|
38
|
-
/^mcp\.json$/,
|
|
39
|
-
/^lib\//,
|
|
40
|
-
/^scripts\//,
|
|
41
|
-
/^tests\//,
|
|
42
|
-
/^\.agent-context\/prompts\//,
|
|
43
|
-
/^\.agents\/workflows\//,
|
|
44
|
-
/^\.agent-context\/review-checklists\//,
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
const SKIP_DIRECTORY_NAMES = new Set([
|
|
48
|
-
'.git',
|
|
49
|
-
'node_modules',
|
|
50
|
-
'.benchmarks',
|
|
51
|
-
'.agentic-backup',
|
|
52
|
-
]);
|
|
53
|
-
|
|
54
|
-
const TEXT_EXTENSIONS = new Set([
|
|
55
|
-
'.md',
|
|
56
|
-
'.mjs',
|
|
57
|
-
'.js',
|
|
58
|
-
'.json',
|
|
59
|
-
'.yml',
|
|
60
|
-
'.yaml',
|
|
61
|
-
'.txt',
|
|
62
|
-
'.ts',
|
|
63
|
-
'.tsx',
|
|
64
|
-
'.cjs',
|
|
65
|
-
'.sh',
|
|
66
|
-
'.ps1',
|
|
67
|
-
]);
|
|
68
|
-
|
|
69
|
-
function isTextCandidate(fileName) {
|
|
70
|
-
const extension = extname(fileName).toLowerCase();
|
|
71
|
-
return TEXT_EXTENSIONS.has(extension);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function pathStatOrNull(targetPath) {
|
|
75
|
-
try {
|
|
76
|
-
return await stat(targetPath);
|
|
77
|
-
} catch {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function listAllFiles(directoryPath) {
|
|
83
|
-
const discoveredFilePaths = [];
|
|
84
|
-
|
|
85
|
-
async function walk(currentPath) {
|
|
86
|
-
const directoryEntries = await readdir(currentPath, { withFileTypes: true });
|
|
87
|
-
|
|
88
|
-
for (const entry of directoryEntries) {
|
|
89
|
-
if (entry.isDirectory() && SKIP_DIRECTORY_NAMES.has(entry.name)) {
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const entryPath = join(currentPath, entry.name);
|
|
94
|
-
|
|
95
|
-
if (entry.isDirectory()) {
|
|
96
|
-
await walk(entryPath);
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (!isTextCandidate(entry.name)) {
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
discoveredFilePaths.push(entryPath);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
await walk(directoryPath);
|
|
109
|
-
return discoveredFilePaths;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function collectDirectoryEntryCount(absoluteDirectoryPath) {
|
|
113
|
-
const directoryStats = await pathStatOrNull(absoluteDirectoryPath);
|
|
114
|
-
if (!directoryStats || !directoryStats.isDirectory()) {
|
|
115
|
-
return 0;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
let fileCount = 0;
|
|
119
|
-
|
|
120
|
-
async function walk(currentPath) {
|
|
121
|
-
const directoryEntries = await readdir(currentPath, { withFileTypes: true });
|
|
122
|
-
|
|
123
|
-
for (const entry of directoryEntries) {
|
|
124
|
-
const entryPath = join(currentPath, entry.name);
|
|
125
|
-
if (entry.isDirectory()) {
|
|
126
|
-
await walk(entryPath);
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
fileCount += 1;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
await walk(absoluteDirectoryPath);
|
|
135
|
-
return fileCount;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function runAudit() {
|
|
139
|
-
const allTextFilePaths = await listAllFiles(ROOT_DIR);
|
|
140
|
-
const matchesByFile = [];
|
|
141
|
-
const tokenMatchCounts = Object.fromEntries(REFERENCE_TOKENS.map((token) => [token, 0]));
|
|
142
|
-
let runtimeBlockingFileCount = 0;
|
|
143
|
-
let documentationReferenceFileCount = 0;
|
|
144
|
-
|
|
145
|
-
function isRuntimeBlockingFile(relativeFilePath) {
|
|
146
|
-
return RUNTIME_BLOCKING_PATH_PATTERNS.some((pattern) => pattern.test(relativeFilePath));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
for (const absoluteFilePath of allTextFilePaths) {
|
|
150
|
-
const relativeFilePath = relative(ROOT_DIR, absoluteFilePath).replace(/\\/g, '/');
|
|
151
|
-
|
|
152
|
-
if (NON_BLOCKING_REFERENCE_FILE_PATHS.has(relativeFilePath)) {
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const fileContent = await readFile(absoluteFilePath, 'utf8');
|
|
157
|
-
|
|
158
|
-
const matchedTokens = REFERENCE_TOKENS.filter((token) => fileContent.includes(token));
|
|
159
|
-
if (matchedTokens.length === 0) {
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
for (const matchedToken of matchedTokens) {
|
|
164
|
-
tokenMatchCounts[matchedToken] += 1;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const classification = isRuntimeBlockingFile(relativeFilePath)
|
|
168
|
-
? 'runtime-blocking'
|
|
169
|
-
: 'documentation-reference';
|
|
170
|
-
|
|
171
|
-
if (classification === 'runtime-blocking') {
|
|
172
|
-
runtimeBlockingFileCount += 1;
|
|
173
|
-
} else {
|
|
174
|
-
documentationReferenceFileCount += 1;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
matchesByFile.push({
|
|
178
|
-
filePath: relativeFilePath,
|
|
179
|
-
matchedTokens,
|
|
180
|
-
classification,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const directoryAudit = [];
|
|
185
|
-
for (const relativeDirectoryPath of STATIC_DIRECTORIES) {
|
|
186
|
-
const absoluteDirectoryPath = join(ROOT_DIR, relativeDirectoryPath);
|
|
187
|
-
const directoryStat = await pathStatOrNull(absoluteDirectoryPath);
|
|
188
|
-
|
|
189
|
-
directoryAudit.push({
|
|
190
|
-
path: relativeDirectoryPath,
|
|
191
|
-
exists: Boolean(directoryStat && directoryStat.isDirectory()),
|
|
192
|
-
fileCount: await collectDirectoryEntryCount(absoluteDirectoryPath),
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const report = {
|
|
197
|
-
generatedAt: new Date().toISOString(),
|
|
198
|
-
auditName: 'v3-purge-audit',
|
|
199
|
-
source: 'working-tree',
|
|
200
|
-
deletionCandidates: directoryAudit,
|
|
201
|
-
referenceSummary: {
|
|
202
|
-
scannedTextFileCount: allTextFilePaths.length,
|
|
203
|
-
blockingFileCount: matchesByFile.length,
|
|
204
|
-
runtimeBlockingFileCount,
|
|
205
|
-
documentationReferenceFileCount,
|
|
206
|
-
tokenMatchCounts,
|
|
207
|
-
},
|
|
208
|
-
blockingReferences: matchesByFile,
|
|
209
|
-
readyForMassDeletion: runtimeBlockingFileCount === 0,
|
|
210
|
-
nextActions: runtimeBlockingFileCount === 0
|
|
211
|
-
? [
|
|
212
|
-
documentationReferenceFileCount === 0
|
|
213
|
-
? 'No runtime blockers detected. Mass deletion can proceed after explicit user confirmation.'
|
|
214
|
-
: 'No runtime blockers detected. Optional docs cleanup can be done after explicit user confirmation for mass deletion.',
|
|
215
|
-
]
|
|
216
|
-
: [
|
|
217
|
-
'Refactor blocking references before removing static directories.',
|
|
218
|
-
'Rerun `npm run audit:v3-purge` and require readyForMassDeletion=true before deletion.',
|
|
219
|
-
],
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
await mkdir(dirname(STATE_OUTPUT_PATH), { recursive: true });
|
|
223
|
-
await writeFile(STATE_OUTPUT_PATH, JSON.stringify(report, null, 2) + '\n', 'utf8');
|
|
224
|
-
|
|
225
|
-
console.log(JSON.stringify(report, null, 2));
|
|
226
|
-
|
|
227
|
-
if (process.argv.includes('--strict') && !report.readyForMassDeletion) {
|
|
228
|
-
process.exitCode = 1;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
runAudit().catch((error) => {
|
|
233
|
-
console.error('[FATAL] v3-purge-audit failed');
|
|
234
|
-
console.error(error instanceof Error ? error.stack : String(error));
|
|
235
|
-
process.exitCode = 1;
|
|
236
|
-
});
|