@ryuenn3123/agentic-senior-core 2.0.23 → 2.0.25

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/.cursorrules CHANGED
@@ -1,6 +1,6 @@
1
1
  # AGENTIC-SENIOR-CORE DYNAMIC GOVERNANCE RULESET
2
2
 
3
- Generated by Agentic-Senior-Core CLI v2.0.23
3
+ Generated by Agentic-Senior-Core CLI v2.0.25
4
4
  Timestamp: 2026-04-15T00:14:51.184Z
5
5
  Selected profile: beginner
6
6
  Selected policy file: .agent-context/policies/llm-judge-threshold.json
@@ -30,9 +30,16 @@ jobs:
30
30
  node ./scripts/detection-benchmark.mjs > detection-benchmark-report.json
31
31
  test -s detection-benchmark-report.json
32
32
 
33
+ - name: Run benchmark anti-regression gate
34
+ run: |
35
+ node ./scripts/benchmark-gate.mjs > benchmark-gate-report.json
36
+ test -s benchmark-gate-report.json
37
+
33
38
  - name: Upload benchmark artifact
34
39
  if: always()
35
40
  uses: actions/upload-artifact@v4
36
41
  with:
37
42
  name: detection-benchmark-report
38
- path: detection-benchmark-report.json
43
+ path: |
44
+ detection-benchmark-report.json
45
+ benchmark-gate-report.json
@@ -0,0 +1,37 @@
1
+ name: Docs Quality Drift Report
2
+
3
+ on:
4
+ schedule:
5
+ - cron: '30 3 * * 2'
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ docs-quality-drift-report:
14
+ runs-on: ubuntu-latest
15
+ timeout-minutes: 10
16
+ env:
17
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
18
+ steps:
19
+ - name: Checkout repository
20
+ uses: actions/checkout@v4
21
+
22
+ - name: Setup Node.js
23
+ uses: actions/setup-node@v4
24
+ with:
25
+ node-version: '22'
26
+
27
+ - name: Run docs quality drift report
28
+ run: |
29
+ node ./scripts/docs-quality-drift-report.mjs > docs-quality-drift-report.json
30
+ test -s docs-quality-drift-report.json
31
+
32
+ - name: Upload docs quality drift artifact
33
+ if: always()
34
+ uses: actions/upload-artifact@v4
35
+ with:
36
+ name: docs-quality-drift-report
37
+ path: docs-quality-drift-report.json
package/.windsurfrules CHANGED
@@ -1,6 +1,6 @@
1
1
  # AGENTIC-SENIOR-CORE DYNAMIC GOVERNANCE RULESET
2
2
 
3
- Generated by Agentic-Senior-Core CLI v2.0.23
3
+ Generated by Agentic-Senior-Core CLI v2.0.25
4
4
  Timestamp: 2026-04-15T00:14:51.184Z
5
5
  Selected profile: beginner
6
6
  Selected policy file: .agent-context/policies/llm-judge-threshold.json
package/README.md CHANGED
@@ -57,6 +57,9 @@ npx @ryuenn3123/agentic-senior-core init
57
57
  npm exec --yes @ryuenn3123/agentic-senior-core init
58
58
  ```
59
59
 
60
+ If the target folder is empty, `init` now offers a quick-choice discovery mode by default.
61
+ You can finish setup using numbered options (Enter selects the first option), and switch to detailed typing when needed.
62
+
60
63
  **Alternative: Global install (optional)**
61
64
 
62
65
  If you want the tool available system-wide without repeating `npx`:
@@ -139,6 +142,9 @@ npx @ryuenn3123/agentic-senior-core init --newbie
139
142
 
140
143
  - `init` creates governance files **in your project folder** (the folder where you run the command).
141
144
  - `init` does not copy repository workflows from this project into your target repository.
145
+ - `init` project discovery accepts answers in any language; prompts stay in English, but non-English answers are supported.
146
+ - Generated docs default to English for consistency; use `--docs-lang` only when you explicitly need a different output language.
147
+ - After docs scaffolding, CLI prints prompt starter examples so users can iterate by prompt without rewriting full project context.
142
148
  - MCP server registration and trust/start are manual in IDE settings.
143
149
  - MCP workspace scaffold is opt-in via `--mcp-template` and creates `.vscode/mcp.json`.
144
150
 
@@ -293,6 +299,20 @@ npm run benchmark:bundle
293
299
 
294
300
  This gives a fast baseline of accuracy, writer-judge comparison, and evidence packaging in one pass.
295
301
 
302
+ ### Documentation Quality Drift Report
303
+
304
+ Generate a machine-readable documentation-quality drift artifact:
305
+
306
+ ```bash
307
+ npm run report:docs-quality-drift
308
+ ```
309
+
310
+ For CI pipelines that only need stdout JSON:
311
+
312
+ ```bash
313
+ node ./scripts/docs-quality-drift-report.mjs --stdout-only
314
+ ```
315
+
296
316
  ### Install and Setup Choices
297
317
 
298
318
  The CLI now supports a smaller decision surface for first-time setup:
@@ -471,7 +471,9 @@ export async function runInitCommand(targetDirectoryArgument, initOptions = {})
471
471
  discoveryAnswers = await loadProjectConfig(initOptions.projectConfig);
472
472
  console.log(`\nLoaded project configuration from: ${initOptions.projectConfig}`);
473
473
  } else {
474
- discoveryAnswers = await runProjectDiscovery(userInterface);
474
+ discoveryAnswers = await runProjectDiscovery(userInterface, {
475
+ defaultProjectName: path.basename(resolvedTargetDirectoryPath),
476
+ });
475
477
  }
476
478
 
477
479
  const normalizedConfigDocsLanguage = normalizeDocsLanguage(discoveryAnswers.docsLang || '');
@@ -563,6 +565,12 @@ export async function runInitCommand(targetDirectoryArgument, initOptions = {})
563
565
  console.log(`I prepared a ${selectedProfile.displayName.toLowerCase()} governance pack for a ${toTitleCase(selectedResolvedStackFileName)} project using the ${toTitleCase(selectedResolvedBlueprintFileName)} blueprint.`);
564
566
  if (scaffoldingResult) {
565
567
  console.log(`I also generated project documentation (${scaffoldingResult.docsLanguage}) based on your project description. AI agents will use docs/ as project context.`);
568
+
569
+ const promptProjectName = scaffoldingResult.discoveryAnswers?.projectName || 'this project';
570
+ console.log('\nPrompt starter examples (copy and adapt in your IDE):');
571
+ console.log(`- Build an MVP for ${promptProjectName}. Follow Layer 9 docs and keep the current stack, database, and auth constraints.`);
572
+ console.log('- Add [new feature] and update docs/project-brief.md plus docs/flow-overview.md in the same change.');
573
+ console.log('- If this change needs architecture migration, propose a migration plan first, then implement after approval.');
566
574
  }
567
575
  console.log('Your AI tools will now receive one compiled rulebook plus the original source rules, and your review threshold is stored in .agent-context/policies/llm-judge-threshold.json.');
568
576
  console.log('MCP server registration is manual inside your IDE settings, even when mcp.json exists.');
@@ -8,7 +8,7 @@ import path from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
 
10
10
  import { CLI_VERSION } from './constants.mjs';
11
- import { ensureDirectory, askChoice, toTitleCase, pathExists } from './utils.mjs';
11
+ import { ensureDirectory, askChoice, askYesNo, toTitleCase, pathExists } from './utils.mjs';
12
12
 
13
13
  const CURRENT_FILE_PATH = fileURLToPath(import.meta.url);
14
14
  const CURRENT_DIRECTORY_PATH = path.dirname(CURRENT_FILE_PATH);
@@ -50,6 +50,73 @@ const AUTH_CHOICES = [
50
50
  'Other',
51
51
  ];
52
52
 
53
+ const DISCOVERY_MODE_CHOICES = [
54
+ 'Quick mode (mostly choices, fastest)',
55
+ 'Detailed mode (type your own answers)',
56
+ ];
57
+
58
+ const DESCRIPTION_TEMPLATE_CHOICES = [
59
+ 'Marketplace / commerce platform',
60
+ 'Internal operations dashboard',
61
+ 'SaaS workflow product',
62
+ 'Developer platform / API product',
63
+ 'Content and community platform',
64
+ 'Other',
65
+ ];
66
+
67
+ const FEATURE_PRESET_CHOICES = [
68
+ 'MVP foundation (auth, core CRUD, role-based access)',
69
+ 'Commerce flow (catalog, cart, checkout)',
70
+ 'Operations flow (dashboard, approvals, reporting)',
71
+ 'API platform flow (keys, rate-limit, webhooks)',
72
+ 'Content flow (publish, moderation, search)',
73
+ 'Other',
74
+ ];
75
+
76
+ const FEATURE_PRESET_MAP = {
77
+ 'MVP foundation (auth, core CRUD, role-based access)': [
78
+ 'Authentication and user profiles',
79
+ 'Core CRUD workflow for primary resources',
80
+ 'Role-based access control',
81
+ ],
82
+ 'Commerce flow (catalog, cart, checkout)': [
83
+ 'Product catalog and filtering',
84
+ 'Shopping cart and wishlist',
85
+ 'Checkout and payment processing flow',
86
+ ],
87
+ 'Operations flow (dashboard, approvals, reporting)': [
88
+ 'Operational dashboard with KPIs',
89
+ 'Approval workflow and audit trail',
90
+ 'Reporting and export functionality',
91
+ ],
92
+ 'API platform flow (keys, rate-limit, webhooks)': [
93
+ 'API key management',
94
+ 'Rate limiting and usage quotas',
95
+ 'Webhook delivery and retry handling',
96
+ ],
97
+ 'Content flow (publish, moderation, search)': [
98
+ 'Draft and publish workflow',
99
+ 'Moderation queue and policy checks',
100
+ 'Search and discovery experience',
101
+ ],
102
+ };
103
+
104
+ async function askFeatureList(userInterface) {
105
+ console.log('\nList your key features (one per line, press Enter to finish):');
106
+
107
+ const features = [];
108
+ while (features.length < 10) {
109
+ const featureLine = (await userInterface.question(` Feature ${features.length + 1}: `)).trim();
110
+ if (!featureLine) {
111
+ break;
112
+ }
113
+
114
+ features.push(featureLine);
115
+ }
116
+
117
+ return features;
118
+ }
119
+
53
120
  export function normalizeDocsLanguage(rawDocsLanguage = 'en') {
54
121
  const normalizedDocsLanguage = String(rawDocsLanguage || 'en').trim().toLowerCase();
55
122
  return SUPPORTED_DOC_LANGUAGES.has(normalizedDocsLanguage) ? normalizedDocsLanguage : null;
@@ -83,17 +150,75 @@ async function resolveTemplateFilePath(templateFileName, docsLanguage) {
83
150
  * Run the project discovery interview.
84
151
  * Returns a structured object with all user responses.
85
152
  */
86
- export async function runProjectDiscovery(userInterface) {
153
+ export async function runProjectDiscovery(userInterface, options = {}) {
87
154
  console.log('\n--- Project Discovery ---');
88
155
  console.log('I will ask a few questions to generate project-specific documentation.');
89
156
  console.log('This helps AI agents understand your project before writing code.\n');
157
+ console.log('You can answer in your own language.');
158
+ console.log('CLI prompts stay in English, but non-English answers are fully supported.\n');
159
+
160
+ const selectedDiscoveryMode = await askChoice(
161
+ 'How do you want to answer project questions?',
162
+ DISCOVERY_MODE_CHOICES,
163
+ userInterface
164
+ );
165
+
166
+ const isQuickMode = selectedDiscoveryMode === DISCOVERY_MODE_CHOICES[0];
167
+
168
+ const defaultProjectName = (options.defaultProjectName || '').trim();
169
+ let projectName = '';
170
+
171
+ if (isQuickMode && defaultProjectName) {
172
+ const projectNameSource = await askChoice(
173
+ 'Project name source:',
174
+ [
175
+ `Use folder name (${defaultProjectName})`,
176
+ 'Type custom project name',
177
+ ],
178
+ userInterface
179
+ );
180
+
181
+ if (projectNameSource.startsWith('Use folder name')) {
182
+ projectName = defaultProjectName;
183
+ }
184
+ }
185
+
186
+ if (!projectName) {
187
+ const projectNamePrompt = defaultProjectName
188
+ ? `Project name (press Enter to use folder name: ${defaultProjectName}): `
189
+ : 'Project name: ';
190
+
191
+ projectName = (await userInterface.question(projectNamePrompt)).trim();
192
+
193
+ if (!projectName && defaultProjectName) {
194
+ projectName = defaultProjectName;
195
+ }
196
+ }
90
197
 
91
- const projectName = (await userInterface.question('Project name: ')).trim();
92
198
  if (!projectName) {
93
199
  throw new Error('Project name is required for documentation scaffolding.');
94
200
  }
95
201
 
96
- const projectDescription = (await userInterface.question('One-line description: ')).trim() || `A ${projectName} project.`;
202
+ let projectDescription = '';
203
+ if (isQuickMode) {
204
+ const selectedDescriptionTemplate = await askChoice(
205
+ 'Project description template:',
206
+ DESCRIPTION_TEMPLATE_CHOICES,
207
+ userInterface
208
+ );
209
+
210
+ if (selectedDescriptionTemplate === 'Other') {
211
+ projectDescription = (await userInterface.question('One-line description: ')).trim();
212
+ } else {
213
+ projectDescription = `${selectedDescriptionTemplate} for ${projectName}.`;
214
+ }
215
+ } else {
216
+ projectDescription = (await userInterface.question('One-line description: ')).trim();
217
+ }
218
+
219
+ if (!projectDescription) {
220
+ projectDescription = `A ${projectName} project.`;
221
+ }
97
222
 
98
223
  const domainSelection = await askChoice(
99
224
  'Primary domain:',
@@ -128,31 +253,43 @@ export async function runProjectDiscovery(userInterface) {
128
253
  authStrategy = (await userInterface.question('Describe your auth setup: ')).trim() || 'Custom auth';
129
254
  }
130
255
 
131
- console.log('\nList your key features (one per line, press Enter twice to finish):');
132
- const features = [];
133
- let consecutiveEmptyLineCount = 0;
134
-
135
- while (features.length < 10) {
136
- const featureLine = (await userInterface.question(` Feature ${features.length + 1}: `)).trim();
256
+ let features = [];
257
+ if (isQuickMode) {
258
+ const selectedFeaturePreset = await askChoice(
259
+ 'Feature set:',
260
+ FEATURE_PRESET_CHOICES,
261
+ userInterface
262
+ );
137
263
 
138
- if (!featureLine) {
139
- consecutiveEmptyLineCount += 1;
140
- if (consecutiveEmptyLineCount >= 1 && features.length >= 1) {
141
- break;
142
- }
143
- continue;
264
+ if (selectedFeaturePreset === 'Other') {
265
+ features = await askFeatureList(userInterface);
266
+ } else {
267
+ features = FEATURE_PRESET_MAP[selectedFeaturePreset] || [];
144
268
  }
145
-
146
- consecutiveEmptyLineCount = 0;
147
- features.push(featureLine);
269
+ } else {
270
+ features = await askFeatureList(userInterface);
148
271
  }
149
272
 
150
273
  if (features.length === 0) {
151
274
  features.push('Core functionality (define during development)');
152
275
  }
153
276
 
154
- const additionalContext = (await userInterface.question('\nAdditional context (optional, press Enter to skip): ')).trim()
155
- || 'No additional context provided.';
277
+ let additionalContext = 'No additional context provided.';
278
+ if (isQuickMode) {
279
+ const wantsAdditionalContext = await askYesNo(
280
+ 'Add additional context now?',
281
+ userInterface,
282
+ false
283
+ );
284
+
285
+ if (wantsAdditionalContext) {
286
+ additionalContext = (await userInterface.question('Additional context: ')).trim()
287
+ || 'No additional context provided.';
288
+ }
289
+ } else {
290
+ additionalContext = (await userInterface.question('\nAdditional context (optional, press Enter to skip): ')).trim()
291
+ || 'No additional context provided.';
292
+ }
156
293
 
157
294
  return {
158
295
  projectName,
package/lib/cli/utils.mjs CHANGED
@@ -52,7 +52,7 @@ export function printUsage() {
52
52
  console.log(' --mcp-template Create .vscode/mcp.json workspace template (MCP trust/start remains manual in IDE)');
53
53
  console.log(' --scaffold-docs Force project documentation scaffolding (architecture, database, API, flow)');
54
54
  console.log(' --no-scaffold-docs Skip project documentation scaffolding');
55
- console.log(' --docs-lang Language for generated project docs (en, id; default: en)');
55
+ console.log(' --docs-lang Optional override for generated project docs language (default: en)');
56
56
  console.log(' --project-config Path to a project config file for non-interactive doc scaffolding');
57
57
  console.log(' --dry-run Preview upgrade without writing files');
58
58
  console.log(' --yes Skip confirmation prompts for upgrade');
@@ -167,8 +167,14 @@ export async function askChoice(promptMessage, options, userInterface) {
167
167
  });
168
168
 
169
169
  while (true) {
170
- const selectedRawInput = await userInterface.question('Choose a number: ');
171
- const selectedIndex = Number.parseInt(selectedRawInput.trim(), 10) - 1;
170
+ const selectedRawInput = await userInterface.question('Choose a number (press Enter for 1): ');
171
+ const normalizedInput = selectedRawInput.trim();
172
+
173
+ if (!normalizedInput) {
174
+ return options[0];
175
+ }
176
+
177
+ const selectedIndex = Number.parseInt(normalizedInput, 10) - 1;
172
178
 
173
179
  if (Number.isNaN(selectedIndex) || selectedIndex < 0 || selectedIndex >= options.length) {
174
180
  console.log('Invalid choice. Please select a valid number.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryuenn3123/agentic-senior-core",
3
- "version": "2.0.23",
3
+ "version": "2.0.25",
4
4
  "type": "module",
5
5
  "description": "Force your AI Agent to code like a Staff Engineer, not a Junior.",
6
6
  "bin": {
@@ -53,6 +53,7 @@
53
53
  "benchmark:gate": "node ./scripts/benchmark-gate.mjs",
54
54
  "benchmark:intelligence": "node ./scripts/benchmark-intelligence.mjs",
55
55
  "report:quality-trend": "node ./scripts/quality-trend-report.mjs",
56
+ "report:docs-quality-drift": "node ./scripts/docs-quality-drift-report.mjs",
56
57
  "report:governance-weekly": "node ./scripts/governance-weekly-report.mjs",
57
58
  "validate": "node ./scripts/validate.mjs",
58
59
  "test": "node --test ./tests/cli-smoke.test.mjs ./tests/mcp-server.test.mjs ./tests/llm-judge.test.mjs ./tests/enterprise-ops.test.mjs ./tests/skill-tier-gate.test.mjs"
@@ -0,0 +1,359 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * docs-quality-drift-report.mjs
5
+ *
6
+ * Generates a machine-readable documentation quality drift artifact.
7
+ * Tracks plain-language readability signals and trend deltas over time.
8
+ */
9
+
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import fs from 'node:fs/promises';
12
+ import { dirname, join, relative, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ const SCRIPT_FILE_PATH = fileURLToPath(import.meta.url);
16
+ const SCRIPT_DIR = dirname(SCRIPT_FILE_PATH);
17
+ const REPOSITORY_ROOT = resolve(SCRIPT_DIR, '..');
18
+ const REPORT_PATH = join(REPOSITORY_ROOT, '.agent-context', 'state', 'docs-quality-drift-report.json');
19
+ const ARGUMENT_FLAGS = new Set(process.argv.slice(2));
20
+ const isStdoutOnlyMode = ARGUMENT_FLAGS.has('--stdout-only');
21
+ const HISTORY_LIMIT = 52;
22
+ const LONG_SENTENCE_WORD_THRESHOLD = 28;
23
+
24
+ const MONITORED_STATIC_FILE_PATHS = [
25
+ 'README.md',
26
+ 'CHANGELOG.md',
27
+ '.instructions.md',
28
+ 'AGENTS.md',
29
+ '.github/copilot-instructions.md',
30
+ '.gemini/instructions.md',
31
+ 'docs/deep_analysis_and_roadmap_backlog.md',
32
+ ];
33
+
34
+ const MONITORED_DIRECTORY_PATHS = [
35
+ 'docs',
36
+ '.agent-context/prompts',
37
+ '.agent-context/review-checklists',
38
+ ];
39
+
40
+ const FORBIDDEN_BUZZWORDS = [
41
+ 'delve',
42
+ 'leverage',
43
+ 'robust',
44
+ 'utilize',
45
+ 'seamless',
46
+ ];
47
+
48
+ function normalizeLineEndings(content) {
49
+ return content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
50
+ }
51
+
52
+ function extractWordCount(content) {
53
+ const words = content.match(/[A-Za-z0-9']+/g) || [];
54
+ return words.length;
55
+ }
56
+
57
+ function extractSentenceWordCounts(content) {
58
+ const sentenceFragments = content
59
+ .split(/[.!?]+/u)
60
+ .map((fragment) => fragment.trim())
61
+ .filter((fragment) => fragment.length > 0);
62
+
63
+ const sentenceWordCounts = [];
64
+ for (const sentenceFragment of sentenceFragments) {
65
+ const sentenceWordCount = extractWordCount(sentenceFragment);
66
+ if (sentenceWordCount > 0) {
67
+ sentenceWordCounts.push(sentenceWordCount);
68
+ }
69
+ }
70
+
71
+ return sentenceWordCounts;
72
+ }
73
+
74
+ function countEmoji(content) {
75
+ const emojiMatches = content.match(/[\p{Extended_Pictographic}]/gu) || [];
76
+ return emojiMatches.length;
77
+ }
78
+
79
+ function countForbiddenBuzzwords(content) {
80
+ const buzzwordCounts = {};
81
+
82
+ for (const forbiddenBuzzword of FORBIDDEN_BUZZWORDS) {
83
+ const buzzwordRegex = new RegExp(`\\b${forbiddenBuzzword}\\b`, 'gi');
84
+ const matchCount = (content.match(buzzwordRegex) || []).length;
85
+ buzzwordCounts[forbiddenBuzzword] = matchCount;
86
+ }
87
+
88
+ return buzzwordCounts;
89
+ }
90
+
91
+ async function collectMarkdownFiles(directoryPath) {
92
+ const markdownFilePaths = [];
93
+
94
+ async function walk(currentDirectoryPath) {
95
+ if (!existsSync(currentDirectoryPath)) {
96
+ return;
97
+ }
98
+
99
+ const directoryEntries = await fs.readdir(currentDirectoryPath, { withFileTypes: true });
100
+
101
+ for (const directoryEntry of directoryEntries) {
102
+ if (directoryEntry.name === 'node_modules' || directoryEntry.name === '.git' || directoryEntry.name === '.benchmarks') {
103
+ continue;
104
+ }
105
+
106
+ const entryPath = join(currentDirectoryPath, directoryEntry.name);
107
+
108
+ if (directoryEntry.isDirectory()) {
109
+ await walk(entryPath);
110
+ continue;
111
+ }
112
+
113
+ if (directoryEntry.isFile() && directoryEntry.name.toLowerCase().endsWith('.md')) {
114
+ markdownFilePaths.push(entryPath);
115
+ }
116
+ }
117
+ }
118
+
119
+ await walk(directoryPath);
120
+ return markdownFilePaths;
121
+ }
122
+
123
+ async function collectMonitoredFiles() {
124
+ const collectedFilePathSet = new Set();
125
+
126
+ for (const relativeFilePath of MONITORED_STATIC_FILE_PATHS) {
127
+ const absoluteFilePath = join(REPOSITORY_ROOT, relativeFilePath);
128
+ if (existsSync(absoluteFilePath)) {
129
+ collectedFilePathSet.add(absoluteFilePath);
130
+ }
131
+ }
132
+
133
+ for (const relativeDirectoryPath of MONITORED_DIRECTORY_PATHS) {
134
+ const absoluteDirectoryPath = join(REPOSITORY_ROOT, relativeDirectoryPath);
135
+ const markdownFiles = await collectMarkdownFiles(absoluteDirectoryPath);
136
+
137
+ for (const markdownFilePath of markdownFiles) {
138
+ collectedFilePathSet.add(markdownFilePath);
139
+ }
140
+ }
141
+
142
+ return Array.from(collectedFilePathSet).sort((firstPath, secondPath) => firstPath.localeCompare(secondPath));
143
+ }
144
+
145
+ function readJsonOrNull(filePath) {
146
+ if (!existsSync(filePath)) {
147
+ return null;
148
+ }
149
+
150
+ try {
151
+ return JSON.parse(readFileSync(filePath, 'utf8'));
152
+ } catch {
153
+ return null;
154
+ }
155
+ }
156
+
157
+ function clamp(value, minimum, maximum) {
158
+ return Math.max(minimum, Math.min(maximum, value));
159
+ }
160
+
161
+ function buildQualityScore(metrics) {
162
+ const longSentencePenalty = metrics.longSentenceRatePercent * 0.4;
163
+ const buzzwordPenalty = metrics.totalForbiddenBuzzwordHits * 1.5;
164
+ const emojiPenalty = metrics.emojiCount * 5;
165
+ const averageSentencePenalty = metrics.averageWordsPerSentence > 24
166
+ ? (metrics.averageWordsPerSentence - 24) * 1.2
167
+ : 0;
168
+
169
+ const rawScore = 100 - longSentencePenalty - buzzwordPenalty - emojiPenalty - averageSentencePenalty;
170
+ return Number(clamp(rawScore, 0, 100).toFixed(2));
171
+ }
172
+
173
+ function summarizeTrend(currentSummary, previousSummary) {
174
+ if (!previousSummary) {
175
+ return {
176
+ hasBaseline: false,
177
+ trend: 'baseline-created',
178
+ deltaQualityScore: null,
179
+ deltaLongSentenceRatePercent: null,
180
+ deltaForbiddenBuzzwordHits: null,
181
+ deltaEmojiCount: null,
182
+ };
183
+ }
184
+
185
+ const deltaQualityScore = Number((currentSummary.qualityScore - previousSummary.qualityScore).toFixed(2));
186
+ const deltaLongSentenceRatePercent = Number((currentSummary.longSentenceRatePercent - previousSummary.longSentenceRatePercent).toFixed(2));
187
+ const deltaForbiddenBuzzwordHits = currentSummary.totalForbiddenBuzzwordHits - previousSummary.totalForbiddenBuzzwordHits;
188
+ const deltaEmojiCount = currentSummary.emojiCount - previousSummary.emojiCount;
189
+
190
+ let trend = 'stable';
191
+ if (deltaQualityScore >= 1 && deltaLongSentenceRatePercent <= 0 && deltaForbiddenBuzzwordHits <= 0 && deltaEmojiCount <= 0) {
192
+ trend = 'improving';
193
+ } else if (deltaQualityScore <= -1 || deltaLongSentenceRatePercent > 0 || deltaForbiddenBuzzwordHits > 0 || deltaEmojiCount > 0) {
194
+ trend = 'regressing';
195
+ }
196
+
197
+ return {
198
+ hasBaseline: true,
199
+ trend,
200
+ deltaQualityScore,
201
+ deltaLongSentenceRatePercent,
202
+ deltaForbiddenBuzzwordHits,
203
+ deltaEmojiCount,
204
+ };
205
+ }
206
+
207
+ function buildHistoryEntry(report) {
208
+ return {
209
+ generatedAt: report.generatedAt,
210
+ qualityScore: report.summary.qualityScore,
211
+ documentCount: report.summary.documentCount,
212
+ averageWordsPerSentence: report.summary.averageWordsPerSentence,
213
+ longSentenceRatePercent: report.summary.longSentenceRatePercent,
214
+ totalForbiddenBuzzwordHits: report.summary.totalForbiddenBuzzwordHits,
215
+ emojiCount: report.summary.emojiCount,
216
+ };
217
+ }
218
+
219
+ function mergeHistory(previousReport, currentHistoryEntry) {
220
+ const existingHistory = Array.isArray(previousReport?.history) ? previousReport.history : [];
221
+ const mergedHistory = [...existingHistory, currentHistoryEntry];
222
+
223
+ if (mergedHistory.length <= HISTORY_LIMIT) {
224
+ return mergedHistory;
225
+ }
226
+
227
+ return mergedHistory.slice(mergedHistory.length - HISTORY_LIMIT);
228
+ }
229
+
230
+ async function runDocsQualityDriftReport() {
231
+ const monitoredFilePaths = await collectMonitoredFiles();
232
+
233
+ const fileSummaries = [];
234
+ let totalLineCount = 0;
235
+ let totalWordCount = 0;
236
+ let totalSentenceCount = 0;
237
+ let totalLongSentenceCount = 0;
238
+ let totalEmojiCount = 0;
239
+ const forbiddenBuzzwordTotals = Object.fromEntries(FORBIDDEN_BUZZWORDS.map((word) => [word, 0]));
240
+
241
+ for (const monitoredFilePath of monitoredFilePaths) {
242
+ const rawContent = await fs.readFile(monitoredFilePath, 'utf8');
243
+ const normalizedContent = normalizeLineEndings(rawContent);
244
+
245
+ const lineCount = normalizedContent.length === 0 ? 0 : normalizedContent.split('\n').length;
246
+ const wordCount = extractWordCount(normalizedContent);
247
+ const sentenceWordCounts = extractSentenceWordCounts(normalizedContent);
248
+ const sentenceCount = sentenceWordCounts.length;
249
+ const longSentenceCount = sentenceWordCounts.filter((sentenceWordCount) => sentenceWordCount > LONG_SENTENCE_WORD_THRESHOLD).length;
250
+ const emojiCount = countEmoji(normalizedContent);
251
+ const forbiddenBuzzwordCounts = countForbiddenBuzzwords(normalizedContent);
252
+
253
+ totalLineCount += lineCount;
254
+ totalWordCount += wordCount;
255
+ totalSentenceCount += sentenceCount;
256
+ totalLongSentenceCount += longSentenceCount;
257
+ totalEmojiCount += emojiCount;
258
+
259
+ for (const forbiddenBuzzword of FORBIDDEN_BUZZWORDS) {
260
+ forbiddenBuzzwordTotals[forbiddenBuzzword] += forbiddenBuzzwordCounts[forbiddenBuzzword];
261
+ }
262
+
263
+ fileSummaries.push({
264
+ filePath: relative(REPOSITORY_ROOT, monitoredFilePath).replace(/\\/g, '/'),
265
+ lineCount,
266
+ wordCount,
267
+ sentenceCount,
268
+ longSentenceCount,
269
+ emojiCount,
270
+ forbiddenBuzzwordCounts,
271
+ });
272
+ }
273
+
274
+ const averageWordsPerSentence = totalSentenceCount === 0
275
+ ? 0
276
+ : Number((totalWordCount / totalSentenceCount).toFixed(2));
277
+ const longSentenceRatePercent = totalSentenceCount === 0
278
+ ? 0
279
+ : Number(((totalLongSentenceCount / totalSentenceCount) * 100).toFixed(2));
280
+ const totalForbiddenBuzzwordHits = FORBIDDEN_BUZZWORDS.reduce(
281
+ (sum, forbiddenBuzzword) => sum + forbiddenBuzzwordTotals[forbiddenBuzzword],
282
+ 0
283
+ );
284
+
285
+ const summary = {
286
+ documentCount: monitoredFilePaths.length,
287
+ totalLineCount,
288
+ totalWordCount,
289
+ totalSentenceCount,
290
+ averageWordsPerSentence,
291
+ longSentenceCount: totalLongSentenceCount,
292
+ longSentenceRatePercent,
293
+ emojiCount: totalEmojiCount,
294
+ forbiddenBuzzwordTotals,
295
+ totalForbiddenBuzzwordHits,
296
+ qualityScore: 0,
297
+ };
298
+
299
+ summary.qualityScore = buildQualityScore(summary);
300
+
301
+ const previousReport = readJsonOrNull(REPORT_PATH);
302
+ const previousSummary = previousReport?.summary || null;
303
+ const trend = summarizeTrend(summary, previousSummary);
304
+
305
+ const sortedBuzzwordBreakdown = Object.entries(forbiddenBuzzwordTotals)
306
+ .map(([term, hits]) => ({ term, hits }))
307
+ .sort((firstEntry, secondEntry) => secondEntry.hits - firstEntry.hits);
308
+
309
+ const docsQualityDriftReportSnapshot = {
310
+ generatedAt: new Date().toISOString(),
311
+ reportName: 'docs-quality-drift-report',
312
+ passed: summary.emojiCount === 0,
313
+ methodology: {
314
+ monitoredStaticFiles: MONITORED_STATIC_FILE_PATHS,
315
+ monitoredDirectories: MONITORED_DIRECTORY_PATHS,
316
+ forbiddenBuzzwords: FORBIDDEN_BUZZWORDS,
317
+ longSentenceWordThreshold: LONG_SENTENCE_WORD_THRESHOLD,
318
+ },
319
+ summary,
320
+ trend,
321
+ buzzwordBreakdown: sortedBuzzwordBreakdown,
322
+ topLongSentenceRiskFiles: fileSummaries
323
+ .map((fileSummary) => ({
324
+ filePath: fileSummary.filePath,
325
+ longSentenceCount: fileSummary.longSentenceCount,
326
+ }))
327
+ .filter((fileSummary) => fileSummary.longSentenceCount > 0)
328
+ .sort((firstFile, secondFile) => secondFile.longSentenceCount - firstFile.longSentenceCount)
329
+ .slice(0, 10),
330
+ fileSummaries,
331
+ artifact: {
332
+ path: REPORT_PATH,
333
+ writeMode: isStdoutOnlyMode ? 'stdout-only' : 'stdout-and-file',
334
+ },
335
+ };
336
+
337
+ const history = mergeHistory(previousReport, buildHistoryEntry(docsQualityDriftReportSnapshot));
338
+ const docsQualityDriftReport = {
339
+ ...docsQualityDriftReportSnapshot,
340
+ history,
341
+ };
342
+
343
+ if (!isStdoutOnlyMode) {
344
+ await fs.mkdir(dirname(REPORT_PATH), { recursive: true });
345
+ await fs.writeFile(REPORT_PATH, JSON.stringify(docsQualityDriftReport, null, 2) + '\n', 'utf8');
346
+ }
347
+
348
+ return docsQualityDriftReport;
349
+ }
350
+
351
+ runDocsQualityDriftReport()
352
+ .then((docsQualityDriftReport) => {
353
+ console.log(JSON.stringify(docsQualityDriftReport, null, 2));
354
+ })
355
+ .catch((docsQualityError) => {
356
+ const errorMessage = docsQualityError instanceof Error ? docsQualityError.message : String(docsQualityError);
357
+ console.error(`Docs quality drift report failed: ${errorMessage}`);
358
+ process.exit(1);
359
+ });
@@ -36,6 +36,7 @@ const REQUIRED_FRONTEND_PARITY_SNIPPETS = [
36
36
  'UX Narrative and Conversion Clarity',
37
37
  'Release Evidence',
38
38
  ];
39
+ const BENCHMARK_GATE_SCRIPT_PATH = 'scripts/benchmark-gate.mjs';
39
40
 
40
41
  function readText(relativeFilePath) {
41
42
  const absolutePath = resolve(REPOSITORY_ROOT, relativeFilePath);
@@ -54,6 +55,46 @@ function pushResult(results, isPassed, checkName, details) {
54
55
  });
55
56
  }
56
57
 
58
+ function parseMachineReadableReport(rawOutput) {
59
+ if (typeof rawOutput !== 'string' || rawOutput.trim().length === 0) {
60
+ return null;
61
+ }
62
+
63
+ try {
64
+ return JSON.parse(rawOutput);
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ function runMachineReadableScript(scriptRelativePath) {
71
+ try {
72
+ const rawOutput = execFileSync('node', [scriptRelativePath], {
73
+ cwd: REPOSITORY_ROOT,
74
+ encoding: 'utf8',
75
+ maxBuffer: 1024 * 1024,
76
+ });
77
+
78
+ return {
79
+ report: parseMachineReadableReport(rawOutput),
80
+ executionErrorMessage: null,
81
+ };
82
+ } catch (scriptExecutionError) {
83
+ const rawOutput = scriptExecutionError && typeof scriptExecutionError === 'object' && 'stdout' in scriptExecutionError
84
+ ? String(scriptExecutionError.stdout ?? '')
85
+ : '';
86
+ const parsedReport = parseMachineReadableReport(rawOutput);
87
+ const executionErrorMessage = scriptExecutionError instanceof Error
88
+ ? scriptExecutionError.message
89
+ : 'Unknown execution error';
90
+
91
+ return {
92
+ report: parsedReport,
93
+ executionErrorMessage,
94
+ };
95
+ }
96
+ }
97
+
57
98
  function validateCompatibilityManifestShape(parsedManifest, skillDomainName) {
58
99
  const validationErrors = [];
59
100
 
@@ -82,6 +123,7 @@ function validateCompatibilityManifestShape(parsedManifest, skillDomainName) {
82
123
 
83
124
  function runReleaseGate() {
84
125
  const results = [];
126
+ const diagnostics = {};
85
127
  const packageJsonPath = 'package.json';
86
128
  const changelogPath = 'CHANGELOG.md';
87
129
  const roadmapPath = 'docs/roadmap.md';
@@ -245,12 +287,48 @@ function runReleaseGate() {
245
287
  pushResult(results, false, 'frontend-usability-audit', `Failed to execute frontend usability audit: ${frontendAuditErrorMessage}`);
246
288
  }
247
289
 
290
+ const benchmarkGateExecution = runMachineReadableScript(BENCHMARK_GATE_SCRIPT_PATH);
291
+ if (!benchmarkGateExecution.report) {
292
+ const failureDetails = benchmarkGateExecution.executionErrorMessage
293
+ ? `Benchmark gate execution failed before producing a machine-readable report: ${benchmarkGateExecution.executionErrorMessage}`
294
+ : 'Benchmark gate did not produce machine-readable JSON output';
295
+ pushResult(results, false, 'benchmark-threshold-gate', failureDetails);
296
+ } else {
297
+ diagnostics.benchmarkGate = benchmarkGateExecution.report;
298
+ pushResult(
299
+ results,
300
+ true,
301
+ 'benchmark-threshold-gate',
302
+ `Benchmark threshold gate executed (passed=${benchmarkGateExecution.report.passed}, failures=${benchmarkGateExecution.report.failureCount})`
303
+ );
304
+
305
+ if (benchmarkGateExecution.report.passed === true) {
306
+ pushResult(results, true, 'benchmark-regression-block', 'Benchmark thresholds are healthy; release remains eligible');
307
+ } else {
308
+ const failedBenchmarkChecks = Array.isArray(benchmarkGateExecution.report.results)
309
+ ? benchmarkGateExecution.report.results
310
+ .filter((benchmarkCheckResult) => !benchmarkCheckResult.passed)
311
+ .map((benchmarkCheckResult) => `${benchmarkCheckResult.checkName}: ${benchmarkCheckResult.details}`)
312
+ : [];
313
+ const failureSummary = failedBenchmarkChecks.length > 0
314
+ ? failedBenchmarkChecks.join('; ')
315
+ : 'Benchmark gate failed but did not report individual failed checks';
316
+ pushResult(
317
+ results,
318
+ false,
319
+ 'benchmark-regression-block',
320
+ `Benchmark threshold regression detected. ${failureSummary}`
321
+ );
322
+ }
323
+ }
324
+
248
325
  const failureCount = results.filter((checkResult) => !checkResult.passed).length;
249
326
  const releaseGateReport = {
250
327
  generatedAt: new Date().toISOString(),
251
328
  gateName: 'release-gate',
252
329
  passed: failureCount === 0,
253
330
  failureCount,
331
+ diagnostics,
254
332
  results,
255
333
  };
256
334
 
@@ -166,6 +166,7 @@ async function validateRequiredFiles() {
166
166
  'scripts/benchmark-writer-judge-matrix.mjs',
167
167
  'scripts/benchmark-gate.mjs',
168
168
  'scripts/benchmark-intelligence.mjs',
169
+ 'scripts/docs-quality-drift-report.mjs',
169
170
  'scripts/governance-weekly-report.mjs',
170
171
  'scripts/mcp-server.mjs',
171
172
  'scripts/frontend-usability-audit.mjs',
@@ -198,6 +199,7 @@ async function validateRequiredFiles() {
198
199
  '.github/workflows/release-gate.yml',
199
200
  '.github/workflows/sbom-compliance.yml',
200
201
  '.github/workflows/benchmark-intelligence.yml',
202
+ '.github/workflows/docs-quality-drift-report.yml',
201
203
  '.github/workflows/governance-weekly-report.yml',
202
204
  'tests/cli-smoke.test.mjs',
203
205
  'tests/mcp-server.test.mjs',