@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,313 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* audit-rule-id-uniqueness.mjs
|
|
6
|
+
*
|
|
7
|
+
* Phase 1 GATE B citability gate. Checks every migrated rule file under
|
|
8
|
+
* `.agent-context/rules/` for:
|
|
9
|
+
*
|
|
10
|
+
* 1. YAML frontmatter parses and contains required keys (id_prefix, domain,
|
|
11
|
+
* priority, scope, applies_to, keywords).
|
|
12
|
+
* 2. Section IDs match the file's locked id_prefix and are unique within
|
|
13
|
+
* the file.
|
|
14
|
+
* 3. Every `[REF:<PREFIX>-NNN]` mention across the rules pack, prompts/,
|
|
15
|
+
* and review-checklists/ resolves to a real section ID.
|
|
16
|
+
* 4. Every `related:` entry in any rule's frontmatter resolves to a real
|
|
17
|
+
* section ID.
|
|
18
|
+
* 5. No ambiguous prose references (`see above`, `as noted earlier`,
|
|
19
|
+
* `the next section`, etc.) survive in any rule body.
|
|
20
|
+
*
|
|
21
|
+
* Files that have not been migrated yet (no YAML frontmatter) are skipped
|
|
22
|
+
* with a notice. The audit only enforces shape on migrated files.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* node scripts/audit-rule-id-uniqueness.mjs (human + report line)
|
|
26
|
+
* node scripts/audit-rule-id-uniqueness.mjs --json (JSON only)
|
|
27
|
+
*
|
|
28
|
+
* Exit codes:
|
|
29
|
+
* 0 — clean
|
|
30
|
+
* 1 — at least one violation
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
34
|
+
import { dirname, join, resolve } from 'node:path';
|
|
35
|
+
import { fileURLToPath } from 'node:url';
|
|
36
|
+
import { parse as parseYaml } from 'yaml';
|
|
37
|
+
|
|
38
|
+
const SCRIPT_FILE_PATH = fileURLToPath(import.meta.url);
|
|
39
|
+
const REPOSITORY_ROOT = resolve(dirname(SCRIPT_FILE_PATH), '..');
|
|
40
|
+
const RULES_DIRECTORY = join(REPOSITORY_ROOT, '.agent-context', 'rules');
|
|
41
|
+
const PROMPTS_DIRECTORY = join(REPOSITORY_ROOT, '.agent-context', 'prompts');
|
|
42
|
+
const REVIEW_CHECKLISTS_DIRECTORY = join(REPOSITORY_ROOT, '.agent-context', 'review-checklists');
|
|
43
|
+
|
|
44
|
+
const ARGS = new Set(process.argv.slice(2));
|
|
45
|
+
const JSON_ONLY = ARGS.has('--json');
|
|
46
|
+
|
|
47
|
+
const RULE_ID_PATTERN = /\b([A-Z]+)-(\d{3,4})(?:-([A-Z]))?\b/;
|
|
48
|
+
const REF_PATTERN = /\[REF:([A-Z]+-\d{3,4}(?:-[A-Z])?)\]/g;
|
|
49
|
+
const SECTION_HEADING_PATTERN = /^##\s+([A-Z]+-\d{3,4}(?:-[A-Z])?):\s+(.+)$/;
|
|
50
|
+
const REQUIRED_FRONTMATTER_KEYS = ['id_prefix', 'domain', 'priority', 'scope', 'applies_to', 'keywords'];
|
|
51
|
+
const FRONTMATTER_PATTERN = /^---\n([\s\S]*?)\n---\n/;
|
|
52
|
+
|
|
53
|
+
const AMBIGUOUS_PROSE_REFERENCE_PATTERNS = [
|
|
54
|
+
/\bsee above\b/i,
|
|
55
|
+
/\bsee below\b/i,
|
|
56
|
+
/\bas noted earlier\b/i,
|
|
57
|
+
/\bas noted above\b/i,
|
|
58
|
+
/\bas mentioned earlier\b/i,
|
|
59
|
+
/\bas mentioned above\b/i,
|
|
60
|
+
/\bin the previous section\b/i,
|
|
61
|
+
/\bin the next section\b/i,
|
|
62
|
+
/\bthe next section\b/i,
|
|
63
|
+
/\bthe previous section\b/i,
|
|
64
|
+
/\bsee earlier\b/i,
|
|
65
|
+
/\bsee later\b/i,
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
function listRuleFiles() {
|
|
69
|
+
return readdirSync(RULES_DIRECTORY)
|
|
70
|
+
.filter((filename) => filename.endsWith('.md') && !filename.endsWith('.candidate.md'))
|
|
71
|
+
.sort()
|
|
72
|
+
.map((filename) => ({ filename, absolutePath: join(RULES_DIRECTORY, filename) }));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseFrontmatter(sourceText) {
|
|
76
|
+
const match = sourceText.match(FRONTMATTER_PATTERN);
|
|
77
|
+
if (!match) return null;
|
|
78
|
+
try {
|
|
79
|
+
return parseYaml(match[1]);
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function extractSectionIds(sourceText) {
|
|
86
|
+
const sectionIds = [];
|
|
87
|
+
for (const line of sourceText.split(/\r?\n/)) {
|
|
88
|
+
const match = line.match(SECTION_HEADING_PATTERN);
|
|
89
|
+
if (match) {
|
|
90
|
+
sectionIds.push({ id: match[1], title: match[2].trim() });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return sectionIds;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function findAmbiguousProseReferences(sourceText) {
|
|
97
|
+
const findings = [];
|
|
98
|
+
const lines = sourceText.split(/\r?\n/);
|
|
99
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
100
|
+
for (const pattern of AMBIGUOUS_PROSE_REFERENCE_PATTERNS) {
|
|
101
|
+
if (pattern.test(lines[lineIndex])) {
|
|
102
|
+
findings.push({ lineNumber: lineIndex + 1, snippet: lines[lineIndex].trim() });
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return findings;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function listMarkdownFilesIn(absoluteDirectoryPath) {
|
|
111
|
+
if (!existsSync(absoluteDirectoryPath)) return [];
|
|
112
|
+
return readdirSync(absoluteDirectoryPath)
|
|
113
|
+
.filter((filename) => filename.endsWith('.md'))
|
|
114
|
+
.map((filename) => join(absoluteDirectoryPath, filename));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function collectAllRefMentions() {
|
|
118
|
+
const mentions = [];
|
|
119
|
+
const candidatePaths = [
|
|
120
|
+
...listRuleFiles().map((entry) => entry.absolutePath),
|
|
121
|
+
...listMarkdownFilesIn(PROMPTS_DIRECTORY),
|
|
122
|
+
...listMarkdownFilesIn(REVIEW_CHECKLISTS_DIRECTORY),
|
|
123
|
+
];
|
|
124
|
+
for (const filePath of candidatePaths) {
|
|
125
|
+
const sourceText = readFileSync(filePath, 'utf8');
|
|
126
|
+
for (const match of sourceText.matchAll(REF_PATTERN)) {
|
|
127
|
+
mentions.push({ filePath, refId: match[1] });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return mentions;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function runRuleIdUniquenessAudit() {
|
|
134
|
+
const violations = [];
|
|
135
|
+
const ruleFiles = listRuleFiles();
|
|
136
|
+
const allKnownSectionIds = new Set();
|
|
137
|
+
const migratedFileCount = { migrated: 0, skipped: 0 };
|
|
138
|
+
const perFile = [];
|
|
139
|
+
|
|
140
|
+
for (const { filename, absolutePath } of ruleFiles) {
|
|
141
|
+
const sourceText = readFileSync(absolutePath, 'utf8');
|
|
142
|
+
const frontmatter = parseFrontmatter(sourceText);
|
|
143
|
+
if (!frontmatter) {
|
|
144
|
+
migratedFileCount.skipped += 1;
|
|
145
|
+
perFile.push({ filename, status: 'skipped', reason: 'no-yaml-frontmatter' });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
migratedFileCount.migrated += 1;
|
|
150
|
+
|
|
151
|
+
for (const requiredKey of REQUIRED_FRONTMATTER_KEYS) {
|
|
152
|
+
if (!(requiredKey in frontmatter)) {
|
|
153
|
+
violations.push({
|
|
154
|
+
file: filename,
|
|
155
|
+
kind: 'frontmatter.missing-key',
|
|
156
|
+
detail: `frontmatter is missing required key: ${requiredKey}`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const idPrefix = frontmatter.id_prefix;
|
|
162
|
+
if (typeof idPrefix !== 'string' || idPrefix.length === 0) {
|
|
163
|
+
violations.push({ file: filename, kind: 'frontmatter.invalid-id-prefix', detail: 'id_prefix must be a non-empty string' });
|
|
164
|
+
perFile.push({ filename, status: 'failed', reason: 'invalid-id-prefix' });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const sections = extractSectionIds(sourceText);
|
|
169
|
+
const seenIdsInFile = new Set();
|
|
170
|
+
for (const section of sections) {
|
|
171
|
+
const idMatch = section.id.match(RULE_ID_PATTERN);
|
|
172
|
+
if (!idMatch || idMatch[1] !== idPrefix) {
|
|
173
|
+
violations.push({
|
|
174
|
+
file: filename,
|
|
175
|
+
kind: 'id.prefix-mismatch',
|
|
176
|
+
detail: `section id '${section.id}' does not match the file's id_prefix '${idPrefix}'`,
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (seenIdsInFile.has(section.id)) {
|
|
181
|
+
violations.push({
|
|
182
|
+
file: filename,
|
|
183
|
+
kind: 'id.duplicate',
|
|
184
|
+
detail: `section id '${section.id}' is reused within the same file`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
seenIdsInFile.add(section.id);
|
|
188
|
+
allKnownSectionIds.add(section.id);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const ambiguousProse = findAmbiguousProseReferences(sourceText);
|
|
192
|
+
for (const finding of ambiguousProse) {
|
|
193
|
+
violations.push({
|
|
194
|
+
file: filename,
|
|
195
|
+
kind: 'prose.ambiguous-reference',
|
|
196
|
+
detail: `line ${finding.lineNumber}: ${finding.snippet}`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
perFile.push({
|
|
201
|
+
filename,
|
|
202
|
+
status: 'audited',
|
|
203
|
+
idPrefix,
|
|
204
|
+
sectionCount: sections.length,
|
|
205
|
+
ambiguousProseCount: ambiguousProse.length,
|
|
206
|
+
knownSectionIdsInFile: sections.map((section) => section.id),
|
|
207
|
+
relatedRefs: (frontmatter.related && typeof frontmatter.related === 'object' && !Array.isArray(frontmatter.related))
|
|
208
|
+
? frontmatter.related
|
|
209
|
+
: null,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const fileEntry of perFile) {
|
|
214
|
+
if (fileEntry.status !== 'audited') continue;
|
|
215
|
+
if (!fileEntry.relatedRefs) continue;
|
|
216
|
+
for (const [parentId, relatedList] of Object.entries(fileEntry.relatedRefs)) {
|
|
217
|
+
if (!fileEntry.knownSectionIdsInFile.includes(parentId)) {
|
|
218
|
+
violations.push({
|
|
219
|
+
file: fileEntry.filename,
|
|
220
|
+
kind: 'related.parent-missing',
|
|
221
|
+
detail: `related map key '${parentId}' is not a section in this file`,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (!Array.isArray(relatedList)) {
|
|
225
|
+
violations.push({
|
|
226
|
+
file: fileEntry.filename,
|
|
227
|
+
kind: 'related.malformed',
|
|
228
|
+
detail: `related[${parentId}] must be an array of <PREFIX>-NNN ids`,
|
|
229
|
+
});
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
for (const relatedId of relatedList) {
|
|
233
|
+
if (typeof relatedId !== 'string' || !RULE_ID_PATTERN.test(relatedId)) {
|
|
234
|
+
violations.push({
|
|
235
|
+
file: fileEntry.filename,
|
|
236
|
+
kind: 'related.malformed',
|
|
237
|
+
detail: `related[${parentId}] entry '${relatedId}' is not a valid <PREFIX>-NNN id`,
|
|
238
|
+
});
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (!allKnownSectionIds.has(relatedId)) {
|
|
242
|
+
violations.push({
|
|
243
|
+
file: fileEntry.filename,
|
|
244
|
+
kind: 'related.unresolved',
|
|
245
|
+
detail: `related[${parentId}] entry '${relatedId}' does not resolve to any section in the rules pack`,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const refMentions = collectAllRefMentions();
|
|
253
|
+
for (const mention of refMentions) {
|
|
254
|
+
if (!allKnownSectionIds.has(mention.refId)) {
|
|
255
|
+
violations.push({
|
|
256
|
+
file: mention.filePath.replace(REPOSITORY_ROOT, '').replace(/^[\\/]+/, ''),
|
|
257
|
+
kind: 'ref.unresolved',
|
|
258
|
+
detail: `[REF:${mention.refId}] does not resolve to any section in the rules pack`,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
auditName: 'audit-rule-id-uniqueness',
|
|
265
|
+
reportVersion: '1.0.0',
|
|
266
|
+
generatedAt: new Date().toISOString(),
|
|
267
|
+
migratedFileCount: migratedFileCount.migrated,
|
|
268
|
+
skippedFileCount: migratedFileCount.skipped,
|
|
269
|
+
knownSectionIdCount: allKnownSectionIds.size,
|
|
270
|
+
refMentionCount: refMentions.length,
|
|
271
|
+
perFile,
|
|
272
|
+
violationCount: violations.length,
|
|
273
|
+
violations,
|
|
274
|
+
passed: violations.length === 0,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function main() {
|
|
279
|
+
const report = runRuleIdUniquenessAudit();
|
|
280
|
+
|
|
281
|
+
if (JSON_ONLY) {
|
|
282
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
283
|
+
process.exit(report.passed ? 0 : 1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
console.log('===============================================');
|
|
287
|
+
console.log(' audit:rule-id-uniqueness');
|
|
288
|
+
console.log('===============================================');
|
|
289
|
+
console.log(` Migrated rule files: ${report.migratedFileCount} of ${report.migratedFileCount + report.skippedFileCount} scanned`);
|
|
290
|
+
console.log(` Pre-migration files: ${report.skippedFileCount} (no YAML frontmatter, skipped)`);
|
|
291
|
+
console.log(` Known section IDs: ${report.knownSectionIdCount}`);
|
|
292
|
+
console.log(` [REF:...] mentions: ${report.refMentionCount}`);
|
|
293
|
+
console.log('');
|
|
294
|
+
|
|
295
|
+
if (report.violationCount === 0) {
|
|
296
|
+
console.log(' All migrated files clean. No prefix mismatches, duplicates, ambiguous prose references, or unresolved [REF:] / related: links.');
|
|
297
|
+
process.stderr.write(`AUDIT_RULE_ID_REPORT: ${JSON.stringify({ passed: true, ...{ migratedFileCount: report.migratedFileCount, knownSectionIdCount: report.knownSectionIdCount, refMentionCount: report.refMentionCount } })}\n`);
|
|
298
|
+
process.exit(0);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
console.log(' Violations:');
|
|
302
|
+
for (const violation of report.violations) {
|
|
303
|
+
console.log(` [${violation.kind}] ${violation.file}: ${violation.detail}`);
|
|
304
|
+
}
|
|
305
|
+
console.log('');
|
|
306
|
+
console.log(` ${report.violationCount} violation(s) found. Phase 1 GATE B requires zero.`);
|
|
307
|
+
process.stderr.write(`AUDIT_RULE_ID_REPORT: ${JSON.stringify({ passed: false, violationCount: report.violationCount, kinds: [...new Set(report.violations.map((v) => v.kind))] })}\n`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}` || process.argv[1].endsWith('audit-rule-id-uniqueness.mjs')) {
|
|
312
|
+
main();
|
|
313
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* build-release-benchmark-bundle.mjs
|
|
6
|
+
*
|
|
7
|
+
* Phase 5 Task 5.4. Reads the locked benchmark artifacts produced in Phases
|
|
8
|
+
* 0-3 and emits a single release bundle that references each artifact by
|
|
9
|
+
* SHA-256 hash plus a non-marketing summary section. The script never
|
|
10
|
+
* regenerates Phase 0-3 numbers; it only references and hashes them.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync } 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 SOURCE_ARTIFACTS = [
|
|
24
|
+
{
|
|
25
|
+
artifactId: 'phase-0-baseline',
|
|
26
|
+
relativePath: 'benchmarks/results/baseline-2026-05-16.json',
|
|
27
|
+
role: 'token-baseline',
|
|
28
|
+
description: 'Phase 0 token-usage baseline measured across providers using free count-tokens APIs and tiktoken estimates.',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
artifactId: 'phase-2-cache',
|
|
32
|
+
relativePath: 'benchmarks/results/cache-phase-2-2026-05-16.json',
|
|
33
|
+
role: 'cache-simulation',
|
|
34
|
+
description: 'Phase 2 offline warm-cache simulation. Direct provider API integration mode; see docs/plan/research-foundation.md D4 for the per-tool scope matrix.',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
artifactId: 'phase-3-anti-halu',
|
|
38
|
+
relativePath: 'benchmarks/results/anti-halu-phase-3-2026-05-16.json',
|
|
39
|
+
role: 'anti-halu-benchmark',
|
|
40
|
+
description: 'Phase 3 offline provider-free anti-halu benchmark.',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
artifactId: 'scorecard-2026-05-16',
|
|
44
|
+
relativePath: 'benchmarks/results/scorecard-2026-05-16.json',
|
|
45
|
+
role: 'supply-chain-snapshot',
|
|
46
|
+
description: 'Phase 5 Task 5.3 supply-chain snapshot. Scorecard CLI was not installed locally; fallback signals are documented honestly.',
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
function sha256Hex(buffer) {
|
|
51
|
+
// Normalize CRLF -> LF before hashing so bundle integrity is reproducible
|
|
52
|
+
// across platforms (Windows working tree may be CRLF; CI checkouts are LF).
|
|
53
|
+
const normalized = buffer.toString('utf8').replace(/\r\n/g, '\n');
|
|
54
|
+
const hash = createHash('sha256');
|
|
55
|
+
hash.update(normalized, 'utf8');
|
|
56
|
+
return hash.digest('hex');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function loadArtifact(rootDir, descriptor) {
|
|
60
|
+
const absolutePath = join(rootDir, descriptor.relativePath);
|
|
61
|
+
if (!existsSync(absolutePath)) {
|
|
62
|
+
return {
|
|
63
|
+
...descriptor,
|
|
64
|
+
status: 'missing',
|
|
65
|
+
sha256: null,
|
|
66
|
+
sizeBytes: 0,
|
|
67
|
+
summary: null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const content = readFileSync(absolutePath);
|
|
71
|
+
const sha = sha256Hex(content);
|
|
72
|
+
let summary = null;
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(content.toString('utf8'));
|
|
75
|
+
summary = summarizeArtifact(descriptor.role, parsed);
|
|
76
|
+
} catch {
|
|
77
|
+
summary = { error: 'artifact is not valid JSON' };
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
...descriptor,
|
|
81
|
+
status: 'present',
|
|
82
|
+
sha256: sha,
|
|
83
|
+
sizeBytes: content.length,
|
|
84
|
+
summary,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function summarizeArtifact(role, parsed) {
|
|
89
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (role === 'token-baseline') {
|
|
93
|
+
return {
|
|
94
|
+
report_version: parsed.report_version || null,
|
|
95
|
+
generated_at: parsed.generated_at || null,
|
|
96
|
+
provider_count: Array.isArray(parsed.providers) ? parsed.providers.length : null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (role === 'cache-simulation') {
|
|
100
|
+
const anthropicWithLoaded = Array.isArray(parsed.summary)
|
|
101
|
+
? parsed.summary.find((row) => row.provider === 'anthropic' && row.scenario === 'with_loaded_rules')
|
|
102
|
+
: null;
|
|
103
|
+
return {
|
|
104
|
+
report_version: parsed.report_version || null,
|
|
105
|
+
integration_mode: parsed.integration_mode || null,
|
|
106
|
+
fixture_count: parsed.fixture_count || null,
|
|
107
|
+
provider_count: parsed.provider_count || null,
|
|
108
|
+
anthropic_with_loaded_avg_total_input: anthropicWithLoaded?.average_total_input_tokens ?? null,
|
|
109
|
+
anthropic_with_loaded_avg_warm_read: anthropicWithLoaded?.average_warm_read_effective_tokens ?? null,
|
|
110
|
+
scope_caveat_present: typeof parsed.scope_caveat === 'string' && parsed.scope_caveat.length > 0,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (role === 'anti-halu-benchmark') {
|
|
114
|
+
return {
|
|
115
|
+
reportVersion: parsed.reportVersion || null,
|
|
116
|
+
generatedAt: parsed.generatedAt || null,
|
|
117
|
+
passRatePercent: parsed.passRatePercent ?? null,
|
|
118
|
+
citationValidityRatePercent: parsed.citationValidityRatePercent ?? null,
|
|
119
|
+
fixtureCount: parsed.fixtureCount ?? null,
|
|
120
|
+
passedCount: parsed.passedCount ?? null,
|
|
121
|
+
failedCount: parsed.failedCount ?? null,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (role === 'supply-chain-snapshot') {
|
|
125
|
+
return {
|
|
126
|
+
report_version: parsed.report_version || null,
|
|
127
|
+
status: parsed.status || null,
|
|
128
|
+
npm_audit_full: parsed.fallback_signals?.npm_audit_full || null,
|
|
129
|
+
lockfile_consistent: parsed.fallback_signals?.lockfile_consistent ?? null,
|
|
130
|
+
runtime_dependencies_count: parsed.fallback_signals?.runtime_dependencies_count ?? null,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function buildReleaseBenchmarkBundle(options = {}) {
|
|
137
|
+
const rootDir = options.rootDir ? resolve(String(options.rootDir)) : REPOSITORY_ROOT;
|
|
138
|
+
const artifacts = SOURCE_ARTIFACTS.map((descriptor) => loadArtifact(rootDir, descriptor));
|
|
139
|
+
const missingArtifacts = artifacts.filter((artifact) => artifact.status === 'missing');
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
bundle_version: '1.0.0',
|
|
143
|
+
release_target: '4.0.0',
|
|
144
|
+
release_status: 'released',
|
|
145
|
+
generated_at: new Date().toISOString(),
|
|
146
|
+
description: 'Phase 5 release benchmark bundle. References Phase 0-3 locked artifacts plus the Phase 5 supply-chain snapshot. No artifact is regenerated by this bundle. Artifact integrity is verified by SHA-256 hash via scripts/audit-release-bundle.mjs.',
|
|
147
|
+
sources: {
|
|
148
|
+
research_foundation: 'docs/plan/research-foundation.md',
|
|
149
|
+
d4_caching_scope_matrix: 'docs/plan/research-foundation.md#d4',
|
|
150
|
+
caching_reporting_format: 'docs/benchmark-reference.md',
|
|
151
|
+
phase_2_outcome: 'docs/plan/phase-2-outcome.md',
|
|
152
|
+
phase_3_outcome: 'docs/plan/phase-3-outcome.md',
|
|
153
|
+
phase_5_plan: 'docs/plan/phase-5-hardening.md',
|
|
154
|
+
},
|
|
155
|
+
integrity: {
|
|
156
|
+
hash_algorithm: 'SHA-256',
|
|
157
|
+
missing_artifact_count: missingArtifacts.length,
|
|
158
|
+
missing_artifacts: missingArtifacts.map((artifact) => artifact.relativePath),
|
|
159
|
+
},
|
|
160
|
+
artifacts: artifacts.map((artifact) => ({
|
|
161
|
+
artifact_id: artifact.artifactId,
|
|
162
|
+
role: artifact.role,
|
|
163
|
+
relative_path: artifact.relativePath,
|
|
164
|
+
description: artifact.description,
|
|
165
|
+
status: artifact.status,
|
|
166
|
+
sha256: artifact.sha256,
|
|
167
|
+
size_bytes: artifact.sizeBytes,
|
|
168
|
+
summary: artifact.summary,
|
|
169
|
+
})),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function main() {
|
|
174
|
+
const bundle = buildReleaseBenchmarkBundle();
|
|
175
|
+
|
|
176
|
+
if (bundle.integrity.missing_artifact_count > 0) {
|
|
177
|
+
process.stderr.write(`build-release-benchmark-bundle: ${bundle.integrity.missing_artifact_count} artifact(s) missing: ${bundle.integrity.missing_artifacts.join(', ')}\n`);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const outputPath = join(REPOSITORY_ROOT, 'benchmarks', 'results', 'release-bundle-4.0.0.json');
|
|
182
|
+
writeFileSync(outputPath, `${JSON.stringify(bundle, null, 2)}\n`, 'utf8');
|
|
183
|
+
|
|
184
|
+
if (JSON_ONLY) {
|
|
185
|
+
process.stdout.write(`${JSON.stringify(bundle, null, 2)}\n`);
|
|
186
|
+
process.exit(0);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log('===============================================');
|
|
190
|
+
console.log(' build-release-benchmark-bundle');
|
|
191
|
+
console.log('===============================================');
|
|
192
|
+
console.log(` Release target: ${bundle.release_target}`);
|
|
193
|
+
console.log(` Release status: ${bundle.release_status}`);
|
|
194
|
+
console.log(` Artifact count: ${bundle.artifacts.length}`);
|
|
195
|
+
console.log(` Missing artifacts: ${bundle.integrity.missing_artifact_count}`);
|
|
196
|
+
console.log(` Output: benchmarks/results/release-bundle-4.0.0.json`);
|
|
197
|
+
console.log('');
|
|
198
|
+
console.log(' Bundle written. Run scripts/audit-release-bundle.mjs to verify integrity before release.');
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (process.argv[1] && (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}` || process.argv[1].endsWith('build-release-benchmark-bundle.mjs'))) {
|
|
203
|
+
main();
|
|
204
|
+
}
|
|
@@ -26,7 +26,7 @@ const SUPPORTED_MODES = new Set([DEFAULT_MODE, 'diagnostic']);
|
|
|
26
26
|
const DEFAULT_WORKFLOW = 'standard';
|
|
27
27
|
|
|
28
28
|
const REQUIRED_ARCHITECTURE_RULE_SNIPPETS = [
|
|
29
|
-
'## Invisible State Management with Explain-on-Demand',
|
|
29
|
+
'## ARCH-006: Invisible State Management with Explain-on-Demand',
|
|
30
30
|
'Default responses must avoid unnecessary state-file internals.',
|
|
31
31
|
'State internals are exposed only on explicit user request.',
|
|
32
32
|
'Diagnostic mode explains relevant state decisions when needed.',
|
|
@@ -80,6 +80,7 @@ function runGitFileQuery(commandArguments) {
|
|
|
80
80
|
cwd: REPOSITORY_ROOT,
|
|
81
81
|
encoding: 'utf8',
|
|
82
82
|
maxBuffer: 1024 * 1024,
|
|
83
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
83
84
|
});
|
|
84
85
|
|
|
85
86
|
return parseGitFileList(rawOutput);
|
|
@@ -55,32 +55,32 @@ const REQUIRED_ARCHITECTURE_CHECKLIST_SNIPPETS = [
|
|
|
55
55
|
const REQUIRED_FRONTEND_RULE_SNIPPETS = [
|
|
56
56
|
'Frontend Design and Interaction Boundaries',
|
|
57
57
|
'Load this rule for UI-facing work. Keep the loaded surface small.',
|
|
58
|
-
'## Activation',
|
|
59
|
-
'## Authority',
|
|
58
|
+
'## FE-001: Activation',
|
|
59
|
+
'## FE-002: Authority',
|
|
60
60
|
'Treat `.agent-context/` as design governance authority.',
|
|
61
61
|
'Treat `README.md` as public and developer overview, setup, usage, and user-facing context only',
|
|
62
62
|
'Do not choose final style, framework, palette, typography, layout paradigm, or animation library offline.',
|
|
63
63
|
'Keep design continuity opt-in.',
|
|
64
64
|
'Repo evidence outranks memory residue.',
|
|
65
|
-
'## Required Design Contract',
|
|
66
|
-
'## Anti-Generic UI Gate',
|
|
65
|
+
'## FE-003: Required Design Contract',
|
|
66
|
+
'## FE-004: Anti-Generic UI Gate',
|
|
67
67
|
'Do not ship interchangeable dashboard chrome',
|
|
68
68
|
'Do not let repeated surfaces share one visual treatment by habit',
|
|
69
69
|
'Use the rename test:',
|
|
70
70
|
'decorative geometry are invalid as wallpaper',
|
|
71
|
-
'## Dynamic Anchor Gate',
|
|
72
|
-
'## Motion, Palette, and 3D',
|
|
71
|
+
'## FE-005: Dynamic Anchor Gate',
|
|
72
|
+
'## FE-006: Motion, Palette, and 3D',
|
|
73
73
|
'Treat motion, 3D, WebGL, canvas, scroll choreography, and animation libraries as first-class options.',
|
|
74
74
|
'Prefer visually exploratory, product-derived palettes while preserving WCAG contrast and status clarity.',
|
|
75
|
-
'## Responsive Mutation',
|
|
75
|
+
'## FE-008: Responsive Mutation',
|
|
76
76
|
'Responsive quality is not scale-only.',
|
|
77
77
|
'container queries',
|
|
78
|
-
'## Accessibility',
|
|
78
|
+
'## FE-009: Accessibility',
|
|
79
79
|
'WCAG 2.2 AA is the hard floor.',
|
|
80
80
|
'APCA is advisory perceptual tuning only.',
|
|
81
|
-
'## CSS Production Hardening',
|
|
81
|
+
'## FE-010: CSS Production Hardening',
|
|
82
82
|
'overflow, wrapping, truncation',
|
|
83
|
-
'## Implementation Boundaries',
|
|
83
|
+
'## FE-011: Implementation Boundaries',
|
|
84
84
|
'Do not hardcode Zustand, React Query, smart/dumb component doctrine',
|
|
85
85
|
];
|
|
86
86
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Loaders for the PR checklist content and the threshold profile JSON. The
|
|
5
|
+
* thresholds loader returns a safe balanced-profile default when the policy
|
|
6
|
+
* file is absent so the gate stays functional in fresh checkouts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
10
|
+
|
|
11
|
+
import { PR_CHECKLIST_PATH, THRESHOLDS_PATH } from './constants.mjs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Loads and returns the PR checklist markdown content.
|
|
15
|
+
*
|
|
16
|
+
* @returns {string} The checklist file contents
|
|
17
|
+
*/
|
|
18
|
+
export function loadPrChecklist() {
|
|
19
|
+
if (!existsSync(PR_CHECKLIST_PATH)) {
|
|
20
|
+
throw new Error(`PR checklist not found at: ${PR_CHECKLIST_PATH}`);
|
|
21
|
+
}
|
|
22
|
+
return readFileSync(PR_CHECKLIST_PATH, 'utf-8');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Loads the LLM judge thresholds, falling back to a safe balanced-profile
|
|
27
|
+
* default when the policy file is missing.
|
|
28
|
+
*
|
|
29
|
+
* @returns {any} The thresholds object
|
|
30
|
+
*/
|
|
31
|
+
export function loadThresholds() {
|
|
32
|
+
if (!existsSync(THRESHOLDS_PATH)) {
|
|
33
|
+
return {
|
|
34
|
+
selectedProfile: 'balanced',
|
|
35
|
+
profileThresholds: {
|
|
36
|
+
balanced: {
|
|
37
|
+
blockingSeverities: ['critical', 'high'],
|
|
38
|
+
failOnMalformedResponse: true,
|
|
39
|
+
failOnProviderError: false,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return JSON.parse(readFileSync(THRESHOLDS_PATH, 'utf-8'));
|
|
45
|
+
}
|