@ryuenn3123/agentic-senior-core 3.0.50 → 4.0.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/review-checklists/pr-checklist.md +1 -0
- package/.agent-context/rules/api-docs.md +63 -47
- package/.agent-context/rules/architecture.md +133 -120
- package/.agent-context/rules/database-design.md +36 -18
- package/.agent-context/rules/docker-runtime.md +66 -43
- package/.agent-context/rules/efficiency-vs-hype.md +38 -17
- package/.agent-context/rules/error-handling.md +35 -16
- package/.agent-context/rules/event-driven.md +35 -18
- package/.agent-context/rules/frontend-architecture.md +103 -76
- package/.agent-context/rules/git-workflow.md +81 -197
- package/.agent-context/rules/microservices.md +42 -41
- package/.agent-context/rules/naming-conv.md +27 -8
- package/.agent-context/rules/performance.md +32 -12
- package/.agent-context/rules/realtime.md +26 -9
- package/.agent-context/rules/security.md +39 -20
- package/.agent-context/rules/testing.md +36 -16
- package/AGENTS.md +9 -9
- package/README.md +10 -1
- package/lib/cli/commands/init.mjs +1 -0
- package/lib/cli/compiler.mjs +1 -0
- package/lib/cli/detector/constants.mjs +135 -0
- package/lib/cli/detector/design-evidence/collector.mjs +256 -0
- package/lib/cli/detector/design-evidence/constants.mjs +39 -0
- package/lib/cli/detector/design-evidence/file-traversal.mjs +83 -0
- package/lib/cli/detector/design-evidence/structured-attribute-evidence.mjs +117 -0
- package/lib/cli/detector/design-evidence/summary.mjs +109 -0
- package/lib/cli/detector/design-evidence/utility-helpers.mjs +122 -0
- package/lib/cli/detector/design-evidence.mjs +25 -610
- package/lib/cli/detector/stack-detection.mjs +243 -0
- package/lib/cli/detector/ui-signals.mjs +150 -0
- package/lib/cli/detector/workspace-scan.mjs +177 -0
- package/lib/cli/detector.mjs +20 -688
- package/lib/cli/memory-continuity.mjs +1 -0
- package/lib/cli/project-scaffolder/design-contract/sections/audits.mjs +96 -0
- package/lib/cli/project-scaffolder/design-contract/sections/conceptual-anchor.mjs +116 -0
- package/lib/cli/project-scaffolder/design-contract/sections/execution-handoff.mjs +211 -0
- package/lib/cli/project-scaffolder/design-contract/seed-signals.mjs +79 -0
- package/lib/cli/project-scaffolder/design-contract/signal-vocab.mjs +64 -0
- package/lib/cli/project-scaffolder/design-contract/validation/anchor-validators.mjs +222 -0
- package/lib/cli/project-scaffolder/design-contract/validation/audit-validators.mjs +117 -0
- package/lib/cli/project-scaffolder/design-contract/validation/completeness.mjs +83 -0
- package/lib/cli/project-scaffolder/design-contract/validation/execution-validators.mjs +328 -0
- package/lib/cli/project-scaffolder/design-contract/validation/helpers.mjs +8 -0
- package/lib/cli/project-scaffolder/design-contract/validation/structural-validators.mjs +79 -0
- package/lib/cli/project-scaffolder/design-contract/validation/system-validators.mjs +256 -0
- package/lib/cli/project-scaffolder/design-contract/validation.mjs +59 -896
- package/lib/cli/project-scaffolder/design-contract.mjs +147 -557
- package/mcp.json +30 -9
- package/package.json +17 -2
- package/scripts/audit-cache-layer-contract.mjs +258 -0
- package/scripts/audit-caching-scope-hygiene.mjs +263 -0
- package/scripts/audit-file-size.mjs +219 -0
- package/scripts/audit-reflection-citations.mjs +163 -0
- package/scripts/audit-release-bundle.mjs +170 -0
- package/scripts/audit-rule-id-uniqueness.mjs +313 -0
- package/scripts/benchmark-evidence-bundle.mjs +1 -0
- package/scripts/build-release-benchmark-bundle.mjs +204 -0
- package/scripts/context-triggered-audit.mjs +1 -0
- package/scripts/documentation-boundary-audit.mjs +1 -0
- package/scripts/explain-on-demand-audit.mjs +2 -1
- package/scripts/frontend-usability-audit.mjs +10 -10
- package/scripts/llm-judge/checklist-loader.mjs +45 -0
- package/scripts/llm-judge/constants.mjs +66 -0
- package/scripts/llm-judge/diff-collection.mjs +74 -0
- package/scripts/llm-judge/prompting.mjs +78 -0
- package/scripts/llm-judge/providers.mjs +111 -0
- package/scripts/llm-judge/verdict.mjs +134 -0
- package/scripts/llm-judge.mjs +21 -482
- package/scripts/mcp-server/tool-registry.mjs +55 -0
- package/scripts/mcp-server/tools.mjs +137 -1
- package/scripts/migrate-rule-format/id-prefix-table.mjs +37 -0
- package/scripts/migrate-rule-format/parse-legacy.mjs +180 -0
- package/scripts/migrate-rule-format/render-new.mjs +169 -0
- package/scripts/migrate-rule-format/roundtrip-validate.mjs +89 -0
- package/scripts/migrate-rule-format.mjs +192 -0
- package/scripts/release-gate/constants.mjs +1 -1
- package/scripts/release-gate/static-checks.mjs +1 -1
- package/scripts/rules-guardian-audit.mjs +5 -2
- package/scripts/single-source-lazy-loading-audit.mjs +2 -1
- package/scripts/ui-design-judge/git-input.mjs +3 -0
- package/scripts/validate/config.mjs +3 -2
- package/scripts/validate/coverage-checks.mjs +1 -1
- package/scripts/validate.mjs +93 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* audit-file-size.mjs
|
|
6
|
+
*
|
|
7
|
+
* Enforces the 500 LOC threshold on production source files. Files that exceed
|
|
8
|
+
* the threshold are hard failures unless they declare a justified exception
|
|
9
|
+
* with a `// @file-size-exception: <reason>` marker in their first 5 lines.
|
|
10
|
+
*
|
|
11
|
+
* Scope:
|
|
12
|
+
* lib/ all .mjs/.js files
|
|
13
|
+
* scripts/ all .mjs/.js files
|
|
14
|
+
* bin/ all .js/.mjs files
|
|
15
|
+
*
|
|
16
|
+
* Excluded:
|
|
17
|
+
* *.test.mjs / *.test.js (test files have different size economics)
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* node scripts/audit-file-size.mjs (human-readable + machine line)
|
|
21
|
+
* node scripts/audit-file-size.mjs --json (JSON only)
|
|
22
|
+
*
|
|
23
|
+
* Exit codes:
|
|
24
|
+
* 0 — all files within budget (or covered by exception)
|
|
25
|
+
* 1 — at least one file exceeded the threshold without an exception marker
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
29
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
|
|
32
|
+
const SCRIPT_FILE_PATH = fileURLToPath(import.meta.url);
|
|
33
|
+
const REPOSITORY_ROOT = resolve(dirname(SCRIPT_FILE_PATH), '..');
|
|
34
|
+
|
|
35
|
+
export const DEFAULT_LOC_THRESHOLD = 500;
|
|
36
|
+
const EXCEPTION_MARKER_PATTERN = /^\s*(?:\/\/|\*)\s*@file-size-exception:\s*(.+?)\s*(?:\*\/\s*)?$/;
|
|
37
|
+
const EXCEPTION_MARKER_LOOKAHEAD_LINES = 5;
|
|
38
|
+
const SCAN_ROOTS = ['lib', 'scripts', 'bin'];
|
|
39
|
+
const SCAN_EXTENSIONS = new Set(['.mjs', '.js']);
|
|
40
|
+
const SKIP_DIRECTORY_NAMES = new Set(['node_modules', '.git', '.agentic-backup', '.benchmarks']);
|
|
41
|
+
|
|
42
|
+
const ARGS = new Set(process.argv.slice(2));
|
|
43
|
+
const JSON_ONLY = ARGS.has('--json');
|
|
44
|
+
|
|
45
|
+
function isTestFile(filePath) {
|
|
46
|
+
const baseName = filePath.split(/[\\/]/).pop();
|
|
47
|
+
return /\.test\.(mjs|js)$/.test(baseName);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function collectSourceFiles(scanRootName) {
|
|
51
|
+
const collected = [];
|
|
52
|
+
const scanRootAbsolutePath = join(REPOSITORY_ROOT, scanRootName);
|
|
53
|
+
|
|
54
|
+
function walk(currentPath) {
|
|
55
|
+
let entries;
|
|
56
|
+
try {
|
|
57
|
+
entries = readdirSync(currentPath, { withFileTypes: true });
|
|
58
|
+
} catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
if (SKIP_DIRECTORY_NAMES.has(entry.name)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const entryAbsolute = join(currentPath, entry.name);
|
|
68
|
+
if (entry.isDirectory()) {
|
|
69
|
+
walk(entryAbsolute);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!entry.isFile()) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const dotIndex = entry.name.lastIndexOf('.');
|
|
78
|
+
const extension = dotIndex >= 0 ? entry.name.slice(dotIndex) : '';
|
|
79
|
+
if (!SCAN_EXTENSIONS.has(extension)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (isTestFile(entryAbsolute)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
collected.push(entryAbsolute);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
statSync(scanRootAbsolutePath);
|
|
93
|
+
} catch {
|
|
94
|
+
return collected;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
walk(scanRootAbsolutePath);
|
|
98
|
+
return collected;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function detectExceptionMarker(sourceLines) {
|
|
102
|
+
const lookahead = sourceLines.slice(0, EXCEPTION_MARKER_LOOKAHEAD_LINES);
|
|
103
|
+
for (const line of lookahead) {
|
|
104
|
+
const match = line.match(EXCEPTION_MARKER_PATTERN);
|
|
105
|
+
if (match) {
|
|
106
|
+
return match[1].trim();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function inspectFile(filePath, threshold) {
|
|
113
|
+
const sourceText = readFileSync(filePath, 'utf8');
|
|
114
|
+
const lines = sourceText.split(/\r?\n/);
|
|
115
|
+
const lineCount = lines.length;
|
|
116
|
+
const overThreshold = lineCount > threshold;
|
|
117
|
+
const exceptionReason = overThreshold ? detectExceptionMarker(lines) : null;
|
|
118
|
+
const passed = !overThreshold || Boolean(exceptionReason);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
filePath: relative(REPOSITORY_ROOT, filePath).replace(/\\/g, '/'),
|
|
122
|
+
lineCount,
|
|
123
|
+
threshold,
|
|
124
|
+
overThreshold,
|
|
125
|
+
exceptionReason,
|
|
126
|
+
passed,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function runAuditFileSize({ threshold = DEFAULT_LOC_THRESHOLD } = {}) {
|
|
131
|
+
const scannedFiles = SCAN_ROOTS.flatMap((scanRootName) => collectSourceFiles(scanRootName));
|
|
132
|
+
const inspections = scannedFiles.map((filePath) => inspectFile(filePath, threshold));
|
|
133
|
+
const violations = inspections.filter((entry) => entry.overThreshold && !entry.exceptionReason);
|
|
134
|
+
const exemptedFiles = inspections.filter((entry) => entry.overThreshold && entry.exceptionReason);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
auditName: 'audit-file-size',
|
|
138
|
+
reportVersion: '1.0.0',
|
|
139
|
+
generatedAt: new Date().toISOString(),
|
|
140
|
+
threshold,
|
|
141
|
+
scannedFileCount: inspections.length,
|
|
142
|
+
overThresholdCount: inspections.filter((entry) => entry.overThreshold).length,
|
|
143
|
+
violationCount: violations.length,
|
|
144
|
+
exemptedCount: exemptedFiles.length,
|
|
145
|
+
violations: violations.map((entry) => ({
|
|
146
|
+
filePath: entry.filePath,
|
|
147
|
+
lineCount: entry.lineCount,
|
|
148
|
+
})),
|
|
149
|
+
exemptedFiles: exemptedFiles.map((entry) => ({
|
|
150
|
+
filePath: entry.filePath,
|
|
151
|
+
lineCount: entry.lineCount,
|
|
152
|
+
reason: entry.exceptionReason,
|
|
153
|
+
})),
|
|
154
|
+
passed: violations.length === 0,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function formatHumanLine(prefix, entry) {
|
|
159
|
+
return ` ${prefix} ${entry.filePath} (${entry.lineCount}/${entry.threshold ?? DEFAULT_LOC_THRESHOLD} LOC)`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function main() {
|
|
163
|
+
const report = runAuditFileSize();
|
|
164
|
+
|
|
165
|
+
if (JSON_ONLY) {
|
|
166
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
167
|
+
process.exit(report.passed ? 0 : 1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log('===============================================');
|
|
171
|
+
console.log(' audit:file-size — 500 LOC enforcement');
|
|
172
|
+
console.log('===============================================');
|
|
173
|
+
console.log(` Threshold: ${report.threshold} LOC`);
|
|
174
|
+
console.log(` Scanned: ${report.scannedFileCount} files`);
|
|
175
|
+
console.log('');
|
|
176
|
+
|
|
177
|
+
if (report.exemptedCount > 0) {
|
|
178
|
+
console.log(' Exempted (declared @file-size-exception):');
|
|
179
|
+
for (const entry of report.exemptedFiles) {
|
|
180
|
+
console.log(` EXEMPT ${entry.filePath} (${entry.lineCount} LOC) — ${entry.reason}`);
|
|
181
|
+
}
|
|
182
|
+
console.log('');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (report.violationCount === 0) {
|
|
186
|
+
console.log(' All files within budget.');
|
|
187
|
+
process.stderr.write(`AUDIT_FILE_SIZE_REPORT: ${JSON.stringify({
|
|
188
|
+
passed: true,
|
|
189
|
+
scannedFileCount: report.scannedFileCount,
|
|
190
|
+
violationCount: 0,
|
|
191
|
+
exemptedCount: report.exemptedCount,
|
|
192
|
+
})}\n`);
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log(' Violations:');
|
|
197
|
+
for (const entry of report.violations) {
|
|
198
|
+
console.log(formatHumanLine('FAIL', { ...entry, threshold: report.threshold }));
|
|
199
|
+
}
|
|
200
|
+
console.log('');
|
|
201
|
+
console.log(` ${report.violationCount} file(s) exceed the ${report.threshold} LOC threshold.`);
|
|
202
|
+
console.log(' Either split the file into focused submodules or, when justified,');
|
|
203
|
+
console.log(' declare an exception in the first 5 lines:');
|
|
204
|
+
console.log(' // @file-size-exception: <reason>');
|
|
205
|
+
console.log('');
|
|
206
|
+
|
|
207
|
+
process.stderr.write(`AUDIT_FILE_SIZE_REPORT: ${JSON.stringify({
|
|
208
|
+
passed: false,
|
|
209
|
+
scannedFileCount: report.scannedFileCount,
|
|
210
|
+
violationCount: report.violationCount,
|
|
211
|
+
exemptedCount: report.exemptedCount,
|
|
212
|
+
violations: report.violations,
|
|
213
|
+
})}\n`);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}` || process.argv[1].endsWith('audit-file-size.mjs')) {
|
|
218
|
+
main();
|
|
219
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { runRuleIdUniquenessAudit } from './audit-rule-id-uniqueness.mjs';
|
|
8
|
+
|
|
9
|
+
const SCRIPT_FILE_PATH = fileURLToPath(import.meta.url);
|
|
10
|
+
const REPOSITORY_ROOT = resolve(dirname(SCRIPT_FILE_PATH), '..');
|
|
11
|
+
const ARGS = new Set(process.argv.slice(2));
|
|
12
|
+
const JSON_ONLY = ARGS.has('--json');
|
|
13
|
+
const RULE_ID_PATTERN = /\b[A-Z]+-\d{3,4}(?:-[A-Z])?\b/g;
|
|
14
|
+
|
|
15
|
+
const REQUIRED_REFLECTION_SURFACES = [
|
|
16
|
+
{
|
|
17
|
+
path: 'AGENTS.md',
|
|
18
|
+
requiredSnippets: [
|
|
19
|
+
'## Bounded Reflection',
|
|
20
|
+
'REFLECTION',
|
|
21
|
+
'Rules:',
|
|
22
|
+
'Risk:',
|
|
23
|
+
'Action:',
|
|
24
|
+
'valid rule IDs',
|
|
25
|
+
'hidden chain-of-thought',
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
path: '.agent-context/review-checklists/pr-checklist.md',
|
|
30
|
+
requiredSnippets: [
|
|
31
|
+
'Bounded Reflection',
|
|
32
|
+
'valid rule IDs',
|
|
33
|
+
'hidden chain-of-thought',
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
function readSource(rootDir, relativePath, sourceOverrides) {
|
|
39
|
+
if (sourceOverrides && Object.prototype.hasOwnProperty.call(sourceOverrides, relativePath)) {
|
|
40
|
+
return String(sourceOverrides[relativePath]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const absolutePath = join(rootDir, relativePath);
|
|
44
|
+
if (!existsSync(absolutePath)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return readFileSync(absolutePath, 'utf8');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function collectKnownRuleIds() {
|
|
52
|
+
const ruleIdAudit = runRuleIdUniquenessAudit();
|
|
53
|
+
const knownRuleIds = new Set();
|
|
54
|
+
|
|
55
|
+
for (const fileEntry of ruleIdAudit.perFile || []) {
|
|
56
|
+
for (const ruleId of fileEntry.knownSectionIdsInFile || []) {
|
|
57
|
+
knownRuleIds.add(ruleId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
ruleIdAudit,
|
|
63
|
+
knownRuleIds,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function collectRuleIdsFromSource(sourceText) {
|
|
68
|
+
return Array.from(new Set(sourceText.match(RULE_ID_PATTERN) || [])).sort();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function runReflectionCitationAudit(options = {}) {
|
|
72
|
+
const rootDir = options.rootDir ? resolve(String(options.rootDir)) : REPOSITORY_ROOT;
|
|
73
|
+
const sourceOverrides = options.sourceOverrides || null;
|
|
74
|
+
const { ruleIdAudit, knownRuleIds } = collectKnownRuleIds();
|
|
75
|
+
const violations = [];
|
|
76
|
+
const surfaceReports = [];
|
|
77
|
+
|
|
78
|
+
for (const surface of REQUIRED_REFLECTION_SURFACES) {
|
|
79
|
+
const sourceText = readSource(rootDir, surface.path, sourceOverrides);
|
|
80
|
+
if (sourceText === null) {
|
|
81
|
+
violations.push({
|
|
82
|
+
file: surface.path,
|
|
83
|
+
kind: 'surface.missing',
|
|
84
|
+
detail: 'Required reflection citation surface is missing.',
|
|
85
|
+
});
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const missingSnippets = surface.requiredSnippets.filter((snippet) => !sourceText.includes(snippet));
|
|
90
|
+
for (const missingSnippet of missingSnippets) {
|
|
91
|
+
violations.push({
|
|
92
|
+
file: surface.path,
|
|
93
|
+
kind: 'reflection.snippet-missing',
|
|
94
|
+
detail: `Missing required reflection snippet: ${missingSnippet}`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const citedRuleIds = collectRuleIdsFromSource(sourceText);
|
|
99
|
+
const unknownRuleIds = citedRuleIds.filter((ruleId) => !knownRuleIds.has(ruleId));
|
|
100
|
+
for (const unknownRuleId of unknownRuleIds) {
|
|
101
|
+
violations.push({
|
|
102
|
+
file: surface.path,
|
|
103
|
+
kind: 'rule-id.unknown',
|
|
104
|
+
detail: `Rule ID ${unknownRuleId} does not resolve to any canonical rule section.`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
surfaceReports.push({
|
|
109
|
+
path: surface.path,
|
|
110
|
+
missingSnippetCount: missingSnippets.length,
|
|
111
|
+
citedRuleIds,
|
|
112
|
+
unknownRuleIds,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
auditName: 'audit-reflection-citations',
|
|
118
|
+
reportVersion: '1.0.0',
|
|
119
|
+
generatedAt: new Date().toISOString(),
|
|
120
|
+
surfaceCount: REQUIRED_REFLECTION_SURFACES.length,
|
|
121
|
+
knownRuleIdCount: knownRuleIds.size,
|
|
122
|
+
ruleIdAuditPassed: ruleIdAudit.passed,
|
|
123
|
+
violationCount: violations.length,
|
|
124
|
+
passed: violations.length === 0,
|
|
125
|
+
surfaces: surfaceReports,
|
|
126
|
+
violations,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function main() {
|
|
131
|
+
const report = runReflectionCitationAudit();
|
|
132
|
+
|
|
133
|
+
if (JSON_ONLY) {
|
|
134
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
135
|
+
process.exit(report.passed ? 0 : 1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log('===============================================');
|
|
139
|
+
console.log(' audit:reflection-citations');
|
|
140
|
+
console.log('===============================================');
|
|
141
|
+
console.log(` Reflection surfaces: ${report.surfaceCount}`);
|
|
142
|
+
console.log(` Known rule IDs: ${report.knownRuleIdCount}`);
|
|
143
|
+
console.log('');
|
|
144
|
+
|
|
145
|
+
if (report.passed) {
|
|
146
|
+
console.log(' Reflection citation surfaces are present and all cited rule IDs resolve.');
|
|
147
|
+
process.stderr.write(`AUDIT_REFLECTION_CITATIONS_REPORT: ${JSON.stringify({ passed: true, surfaceCount: report.surfaceCount, knownRuleIdCount: report.knownRuleIdCount })}\n`);
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(' Violations:');
|
|
152
|
+
for (const violation of report.violations) {
|
|
153
|
+
console.log(` [${violation.kind}] ${violation.file}: ${violation.detail}`);
|
|
154
|
+
}
|
|
155
|
+
console.log('');
|
|
156
|
+
console.log(` ${report.violationCount} violation(s) found.`);
|
|
157
|
+
process.stderr.write(`AUDIT_REFLECTION_CITATIONS_REPORT: ${JSON.stringify({ passed: false, violationCount: report.violationCount, kinds: [...new Set(report.violations.map((violation) => violation.kind))] })}\n`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}` || process.argv[1].endsWith('audit-reflection-citations.mjs')) {
|
|
162
|
+
main();
|
|
163
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* audit-release-bundle.mjs
|
|
6
|
+
*
|
|
7
|
+
* Phase 5 Task 5.4 integrity gate. Validates that
|
|
8
|
+
* benchmarks/results/release-bundle-4.0.0.json exists, references real source
|
|
9
|
+
* artifacts, and that each recorded SHA-256 hash still matches the current
|
|
10
|
+
* file content. Wired into npm run validate.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { dirname, join, resolve } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const SCRIPT_FILE_PATH = fileURLToPath(import.meta.url);
|
|
19
|
+
const REPOSITORY_ROOT = resolve(dirname(SCRIPT_FILE_PATH), '..');
|
|
20
|
+
const ARGS = new Set(process.argv.slice(2));
|
|
21
|
+
const JSON_ONLY = ARGS.has('--json');
|
|
22
|
+
|
|
23
|
+
const DEFAULT_BUNDLE_PATH = join(REPOSITORY_ROOT, 'benchmarks', 'results', 'release-bundle-4.0.0.json');
|
|
24
|
+
|
|
25
|
+
function sha256Hex(buffer) {
|
|
26
|
+
// Normalize CRLF -> LF before hashing so bundle integrity is reproducible
|
|
27
|
+
// across platforms (Windows working tree may be CRLF; CI checkouts are LF).
|
|
28
|
+
const normalized = buffer.toString('utf8').replace(/\r\n/g, '\n');
|
|
29
|
+
const hash = createHash('sha256');
|
|
30
|
+
hash.update(normalized, 'utf8');
|
|
31
|
+
return hash.digest('hex');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function runReleaseBundleAudit(options = {}) {
|
|
35
|
+
const rootDir = options.rootDir ? resolve(String(options.rootDir)) : REPOSITORY_ROOT;
|
|
36
|
+
const bundlePath = options.bundlePath ? resolve(String(options.bundlePath)) : DEFAULT_BUNDLE_PATH;
|
|
37
|
+
const violations = [];
|
|
38
|
+
|
|
39
|
+
if (!existsSync(bundlePath)) {
|
|
40
|
+
violations.push({
|
|
41
|
+
kind: 'bundle.missing',
|
|
42
|
+
detail: `Release bundle is missing at ${bundlePath}. Run scripts/build-release-benchmark-bundle.mjs to regenerate.`,
|
|
43
|
+
});
|
|
44
|
+
return finalizeReport(violations, { bundlePath, artifactCount: 0 });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let parsed;
|
|
48
|
+
try {
|
|
49
|
+
parsed = JSON.parse(readFileSync(bundlePath, 'utf8'));
|
|
50
|
+
} catch (error) {
|
|
51
|
+
violations.push({
|
|
52
|
+
kind: 'bundle.invalid-json',
|
|
53
|
+
detail: `Release bundle is not valid JSON: ${error?.message || error}`,
|
|
54
|
+
});
|
|
55
|
+
return finalizeReport(violations, { bundlePath, artifactCount: 0 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!Array.isArray(parsed.artifacts) || parsed.artifacts.length === 0) {
|
|
59
|
+
violations.push({
|
|
60
|
+
kind: 'bundle.empty',
|
|
61
|
+
detail: 'Release bundle artifacts array must be non-empty.',
|
|
62
|
+
});
|
|
63
|
+
return finalizeReport(violations, { bundlePath, artifactCount: 0 });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (parsed.integrity?.hash_algorithm !== 'SHA-256') {
|
|
67
|
+
violations.push({
|
|
68
|
+
kind: 'bundle.unexpected-hash-algorithm',
|
|
69
|
+
detail: `Expected hash_algorithm "SHA-256", got "${parsed.integrity?.hash_algorithm}".`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (parsed.release_status !== 'release-candidate-unpublished' && parsed.release_status !== 'released') {
|
|
74
|
+
violations.push({
|
|
75
|
+
kind: 'bundle.invalid-release-status',
|
|
76
|
+
detail: `release_status must be either "release-candidate-unpublished" or "released"; got "${parsed.release_status}".`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const artifact of parsed.artifacts) {
|
|
81
|
+
if (!artifact.relative_path || typeof artifact.relative_path !== 'string') {
|
|
82
|
+
violations.push({
|
|
83
|
+
kind: 'artifact.missing-relative-path',
|
|
84
|
+
detail: `Artifact "${artifact.artifact_id}" missing relative_path.`,
|
|
85
|
+
});
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const absolutePath = join(rootDir, artifact.relative_path);
|
|
90
|
+
if (!existsSync(absolutePath)) {
|
|
91
|
+
violations.push({
|
|
92
|
+
kind: 'artifact.missing-file',
|
|
93
|
+
detail: `Artifact "${artifact.artifact_id}" references missing file at ${artifact.relative_path}.`,
|
|
94
|
+
});
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!artifact.sha256 || typeof artifact.sha256 !== 'string') {
|
|
99
|
+
violations.push({
|
|
100
|
+
kind: 'artifact.missing-sha256',
|
|
101
|
+
detail: `Artifact "${artifact.artifact_id}" missing sha256 hash.`,
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const currentHash = sha256Hex(readFileSync(absolutePath));
|
|
107
|
+
if (currentHash !== artifact.sha256) {
|
|
108
|
+
violations.push({
|
|
109
|
+
kind: 'artifact.hash-mismatch',
|
|
110
|
+
detail: `Artifact "${artifact.artifact_id}" sha256 drift: bundle records ${artifact.sha256}, current file is ${currentHash}. Regenerate the bundle via scripts/build-release-benchmark-bundle.mjs.`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return finalizeReport(violations, {
|
|
116
|
+
bundlePath,
|
|
117
|
+
artifactCount: parsed.artifacts.length,
|
|
118
|
+
releaseTarget: parsed.release_target || null,
|
|
119
|
+
releaseStatus: parsed.release_status || null,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function finalizeReport(violations, extras) {
|
|
124
|
+
return {
|
|
125
|
+
auditName: 'audit-release-bundle',
|
|
126
|
+
reportVersion: '1.0.0',
|
|
127
|
+
generatedAt: new Date().toISOString(),
|
|
128
|
+
violationCount: violations.length,
|
|
129
|
+
passed: violations.length === 0,
|
|
130
|
+
violations,
|
|
131
|
+
...extras,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function main() {
|
|
136
|
+
const report = runReleaseBundleAudit();
|
|
137
|
+
|
|
138
|
+
if (JSON_ONLY) {
|
|
139
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
140
|
+
process.exit(report.passed ? 0 : 1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log('===============================================');
|
|
144
|
+
console.log(' audit:release-bundle');
|
|
145
|
+
console.log('===============================================');
|
|
146
|
+
console.log(` Bundle path: ${report.bundlePath}`);
|
|
147
|
+
console.log(` Artifact count: ${report.artifactCount}`);
|
|
148
|
+
console.log(` Release target: ${report.releaseTarget || '(missing)'}`);
|
|
149
|
+
console.log(` Release status: ${report.releaseStatus || '(missing)'}`);
|
|
150
|
+
console.log('');
|
|
151
|
+
|
|
152
|
+
if (report.passed) {
|
|
153
|
+
console.log(' Release bundle is intact. All artifact hashes match.');
|
|
154
|
+
process.stderr.write(`AUDIT_RELEASE_BUNDLE_REPORT: ${JSON.stringify({ passed: true, artifactCount: report.artifactCount })}\n`);
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log(' Violations:');
|
|
159
|
+
for (const violation of report.violations) {
|
|
160
|
+
console.log(` [${violation.kind}] ${violation.detail}`);
|
|
161
|
+
}
|
|
162
|
+
console.log('');
|
|
163
|
+
console.log(` ${report.violationCount} violation(s) found.`);
|
|
164
|
+
process.stderr.write(`AUDIT_RELEASE_BUNDLE_REPORT: ${JSON.stringify({ passed: false, violationCount: report.violationCount })}\n`);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (process.argv[1] && (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}` || process.argv[1].endsWith('audit-release-bundle.mjs'))) {
|
|
169
|
+
main();
|
|
170
|
+
}
|