@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 +1 -1
- package/.github/workflows/docs-quality-drift-report.yml +37 -0
- package/.windsurfrules +1 -1
- package/README.md +20 -0
- package/lib/cli/commands/init.mjs +9 -1
- package/lib/cli/project-scaffolder.mjs +150 -21
- package/lib/cli/utils.mjs +9 -3
- package/package.json +2 -1
- package/scripts/docs-quality-drift-report.mjs +359 -0
- package/scripts/validate.mjs +2 -0
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
continue;
|
|
256
|
+
if (selectedFeaturePreset === 'Other') {
|
|
257
|
+
features = await askFeatureList(userInterface);
|
|
258
|
+
} else {
|
|
259
|
+
features = FEATURE_PRESET_MAP[selectedFeaturePreset] || [];
|
|
144
260
|
}
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
+
});
|
package/scripts/validate.mjs
CHANGED
|
@@ -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',
|