@ryuenn3123/agentic-senior-core 2.0.23 → 2.0.24

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.24
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
@@ -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.24
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,67 @@ 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
+ projectName = (await userInterface.question('Project name: ')).trim();
188
+ }
90
189
 
91
- const projectName = (await userInterface.question('Project name: ')).trim();
92
190
  if (!projectName) {
93
191
  throw new Error('Project name is required for documentation scaffolding.');
94
192
  }
95
193
 
96
- const projectDescription = (await userInterface.question('One-line description: ')).trim() || `A ${projectName} project.`;
194
+ let projectDescription = '';
195
+ if (isQuickMode) {
196
+ const selectedDescriptionTemplate = await askChoice(
197
+ 'Project description template:',
198
+ DESCRIPTION_TEMPLATE_CHOICES,
199
+ userInterface
200
+ );
201
+
202
+ if (selectedDescriptionTemplate === 'Other') {
203
+ projectDescription = (await userInterface.question('One-line description: ')).trim();
204
+ } else {
205
+ projectDescription = `${selectedDescriptionTemplate} for ${projectName}.`;
206
+ }
207
+ } else {
208
+ projectDescription = (await userInterface.question('One-line description: ')).trim();
209
+ }
210
+
211
+ if (!projectDescription) {
212
+ projectDescription = `A ${projectName} project.`;
213
+ }
97
214
 
98
215
  const domainSelection = await askChoice(
99
216
  'Primary domain:',
@@ -128,31 +245,43 @@ export async function runProjectDiscovery(userInterface) {
128
245
  authStrategy = (await userInterface.question('Describe your auth setup: ')).trim() || 'Custom auth';
129
246
  }
130
247
 
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();
248
+ let features = [];
249
+ if (isQuickMode) {
250
+ const selectedFeaturePreset = await askChoice(
251
+ 'Feature set:',
252
+ FEATURE_PRESET_CHOICES,
253
+ userInterface
254
+ );
137
255
 
138
- if (!featureLine) {
139
- consecutiveEmptyLineCount += 1;
140
- if (consecutiveEmptyLineCount >= 1 && features.length >= 1) {
141
- break;
142
- }
143
- continue;
256
+ if (selectedFeaturePreset === 'Other') {
257
+ features = await askFeatureList(userInterface);
258
+ } else {
259
+ features = FEATURE_PRESET_MAP[selectedFeaturePreset] || [];
144
260
  }
145
-
146
- consecutiveEmptyLineCount = 0;
147
- features.push(featureLine);
261
+ } else {
262
+ features = await askFeatureList(userInterface);
148
263
  }
149
264
 
150
265
  if (features.length === 0) {
151
266
  features.push('Core functionality (define during development)');
152
267
  }
153
268
 
154
- const additionalContext = (await userInterface.question('\nAdditional context (optional, press Enter to skip): ')).trim()
155
- || 'No additional context provided.';
269
+ let additionalContext = 'No additional context provided.';
270
+ if (isQuickMode) {
271
+ const wantsAdditionalContext = await askYesNo(
272
+ 'Add additional context now?',
273
+ userInterface,
274
+ false
275
+ );
276
+
277
+ if (wantsAdditionalContext) {
278
+ additionalContext = (await userInterface.question('Additional context: ')).trim()
279
+ || 'No additional context provided.';
280
+ }
281
+ } else {
282
+ additionalContext = (await userInterface.question('\nAdditional context (optional, press Enter to skip): ')).trim()
283
+ || 'No additional context provided.';
284
+ }
156
285
 
157
286
  return {
158
287
  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.24",
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
+ });
@@ -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',