@nerviq/cli 1.8.9 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -24
- package/bin/cli.js +173 -104
- package/package.json +59 -59
- package/src/activity.js +68 -12
- package/src/aider/freshness.js +168 -168
- package/src/anti-patterns.js +13 -11
- package/src/audit.js +16 -14
- package/src/auto-suggest.js +62 -9
- package/src/benchmark.js +52 -41
- package/src/codex/freshness.js +167 -167
- package/src/copilot/freshness.js +197 -197
- package/src/cursor/freshness.js +214 -214
- package/src/dashboard.js +36 -14
- package/src/freshness.js +19 -19
- package/src/gemini/freshness.js +204 -204
- package/src/i18n.js +63 -0
- package/src/instruction-surfaces.js +185 -0
- package/src/locales/en.json +34 -0
- package/src/locales/es.json +34 -0
- package/src/mcp-server.js +1 -1
- package/src/opencode/freshness.js +158 -158
- package/src/stack-checks.js +1 -1
- package/src/synergy/report.js +1 -0
- package/src/techniques.js +174 -58
- package/src/windsurf/freshness.js +215 -215
- package/src/workspace.js +51 -6
package/src/audit.js
CHANGED
|
@@ -33,6 +33,7 @@ const { loadPlugins, mergePluginChecks } = require('./plugins');
|
|
|
33
33
|
const { hasWorkspaceConfig, detectWorkspaceGlobs, detectWorkspaces } = require('./workspace');
|
|
34
34
|
const { detectDeprecationWarnings } = require('./deprecation');
|
|
35
35
|
const { version: packageVersion } = require('../package.json');
|
|
36
|
+
const { t } = require('./i18n');
|
|
36
37
|
|
|
37
38
|
const COLORS = {
|
|
38
39
|
reset: '\x1b[0m',
|
|
@@ -872,27 +873,27 @@ function getCodexDomainPackSignals(ctx) {
|
|
|
872
873
|
|
|
873
874
|
function printLiteAudit(result, dir) {
|
|
874
875
|
console.log('');
|
|
875
|
-
const productLabel = result.platform === 'codex' ? '
|
|
876
|
+
const productLabel = result.platform === 'codex' ? t('audit.codexQuickScan') : t('audit.quickScan');
|
|
876
877
|
console.log(colorize(` ${productLabel}`, 'bold'));
|
|
877
878
|
console.log(colorize(' ═══════════════════════════════════════', 'dim'));
|
|
878
|
-
console.log(colorize(`
|
|
879
|
+
console.log(colorize(` ${t('audit.scanning', { dir })}`, 'dim'));
|
|
879
880
|
console.log('');
|
|
880
881
|
if (result.detectedConfigFiles && result.detectedConfigFiles.length > 0) {
|
|
881
882
|
console.log(colorize(` Found: ${result.detectedConfigFiles.join(', ')}`, 'dim'));
|
|
882
883
|
}
|
|
883
884
|
console.log('');
|
|
884
|
-
console.log(`
|
|
885
|
+
console.log(` ${t('audit.score', { score: colorize(`${result.score}/100`, 'bold'), passed: result.passed, total: result.passed + result.failed })}`);
|
|
885
886
|
|
|
886
887
|
// Score explanation line (lite mode only)
|
|
887
888
|
const _critCount = (result.results || []).filter(r => r.passed === false && r.impact === 'critical').length;
|
|
888
889
|
const _highCount = (result.results || []).filter(r => r.passed === false && r.impact === 'high').length;
|
|
889
890
|
let scoreExplanation;
|
|
890
891
|
if (result.score >= 90) {
|
|
891
|
-
scoreExplanation = '
|
|
892
|
+
scoreExplanation = t('audit.excellent');
|
|
892
893
|
} else if (result.score >= 70) {
|
|
893
|
-
scoreExplanation =
|
|
894
|
+
scoreExplanation = t('audit.strong', { count: _critCount });
|
|
894
895
|
} else if (result.score >= 50) {
|
|
895
|
-
scoreExplanation =
|
|
896
|
+
scoreExplanation = t('audit.good', { count: _critCount + _highCount });
|
|
896
897
|
} else if (result.score >= 30) {
|
|
897
898
|
// Find weakest category (most failures)
|
|
898
899
|
const catFailures = {};
|
|
@@ -901,13 +902,14 @@ function printLiteAudit(result, dir) {
|
|
|
901
902
|
catFailures[cat] = (catFailures[cat] || 0) + 1;
|
|
902
903
|
});
|
|
903
904
|
const weakestCategory = Object.keys(catFailures).sort((a, b) => catFailures[b] - catFailures[a])[0] || 'config';
|
|
904
|
-
scoreExplanation =
|
|
905
|
+
scoreExplanation = t('audit.basic', { category: weakestCategory });
|
|
905
906
|
} else {
|
|
906
|
-
scoreExplanation = '
|
|
907
|
-
}
|
|
908
|
-
console.log(colorize(` ${scoreExplanation}`, 'dim'));
|
|
909
|
-
|
|
910
|
-
|
|
907
|
+
scoreExplanation = t('audit.early');
|
|
908
|
+
}
|
|
909
|
+
console.log(colorize(` ${scoreExplanation}`, 'dim'));
|
|
910
|
+
console.log(colorize(' Score type: live repo audit (current files only, not snapshot history or benchmark projection).', 'dim'));
|
|
911
|
+
|
|
912
|
+
if (result.platformScopeNote) {
|
|
911
913
|
console.log(colorize(` Scope: ${result.platformScopeNote.message}`, 'dim'));
|
|
912
914
|
}
|
|
913
915
|
if (result.workspaceHint && result.workspaceHint.workspaces.length > 0) {
|
|
@@ -1233,10 +1235,10 @@ async function audit(options) {
|
|
|
1233
1235
|
|
|
1234
1236
|
// Display results
|
|
1235
1237
|
console.log('');
|
|
1236
|
-
const auditTitle = spec.platform === 'codex' ? '
|
|
1238
|
+
const auditTitle = spec.platform === 'codex' ? t('audit.codexTitle') : t('audit.title');
|
|
1237
1239
|
console.log(colorize(` ${auditTitle}`, 'bold'));
|
|
1238
1240
|
console.log(colorize(' ═══════════════════════════════════════', 'dim'));
|
|
1239
|
-
console.log(colorize(`
|
|
1241
|
+
console.log(colorize(` ${t('audit.scanning', { dir: options.dir })}`, 'dim'));
|
|
1240
1242
|
if (spec.platformVersion) {
|
|
1241
1243
|
console.log(colorize(` Platform: ${spec.platformLabel} (${spec.platformVersion})`, 'blue'));
|
|
1242
1244
|
}
|
package/src/auto-suggest.js
CHANGED
|
@@ -52,23 +52,72 @@ function analyzeSuggestions(dir) {
|
|
|
52
52
|
.sort((a, b) => b[1] - a[1])
|
|
53
53
|
.map(([key, count]) => ({ key, failCount: count, auditCount: auditSnapshots.length }));
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
const hasSuggestions = suggestedRules.length > 0 || suggestedSuppressions.length > 0 || suggestedPriorities.length > 0;
|
|
56
|
+
let bootstrap = { ready: true, state: 'ready', message: null, steps: [] };
|
|
57
|
+
|
|
58
|
+
if (totalEvents === 0 && auditSnapshots.length === 0) {
|
|
59
|
+
bootstrap = {
|
|
60
|
+
ready: false,
|
|
61
|
+
state: 'empty',
|
|
62
|
+
message: 'No local usage or snapshot history exists yet.',
|
|
63
|
+
steps: [
|
|
64
|
+
'Run `nerviq audit --snapshot` to save the baseline.',
|
|
65
|
+
'Use `nerviq fix`, `nerviq fix --all-critical`, or `nerviq feedback` to record recommendation outcomes.',
|
|
66
|
+
'Run `nerviq audit --snapshot` again after a meaningful repo change.',
|
|
67
|
+
'Re-run `nerviq suggest-rules`.',
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
} else if (!hasSuggestions && totalEvents === 0 && auditSnapshots.length > 0) {
|
|
71
|
+
bootstrap = {
|
|
72
|
+
ready: false,
|
|
73
|
+
state: 'snapshots-only',
|
|
74
|
+
message: `${auditSnapshots.length} audit snapshot(s) exist, but no recommendation outcomes have been recorded yet.`,
|
|
75
|
+
steps: [
|
|
76
|
+
'Run `nerviq fix` or `nerviq feedback` so Nerviq can learn which recommendations you accept or reject.',
|
|
77
|
+
'Re-run `nerviq suggest-rules` after another fix cycle.',
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
} else if (!hasSuggestions && totalEvents > 0 && auditSnapshots.length === 0) {
|
|
81
|
+
bootstrap = {
|
|
82
|
+
ready: false,
|
|
83
|
+
state: 'patterns-only',
|
|
84
|
+
message: `${totalEvents} usage event(s) exist, but no audit snapshots have been saved yet.`,
|
|
85
|
+
steps: [
|
|
86
|
+
'Run `nerviq audit --snapshot` to save the baseline.',
|
|
87
|
+
'Run it again after changes so repeated failures can be prioritized.',
|
|
88
|
+
'Re-run `nerviq suggest-rules`.',
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
} else if (!hasSuggestions) {
|
|
92
|
+
bootstrap = {
|
|
93
|
+
ready: false,
|
|
94
|
+
state: 'warming-up',
|
|
95
|
+
message: `Nerviq has some local history (${totalEvents} pattern events, ${auditSnapshots.length} audit snapshots), but not enough repeated signals yet.`,
|
|
96
|
+
steps: [
|
|
97
|
+
'Keep saving snapshots with `nerviq audit --snapshot`.',
|
|
98
|
+
'Keep recording outcomes with `nerviq fix` or `nerviq feedback`.',
|
|
99
|
+
'Re-run `nerviq suggest-rules` after another change cycle.',
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { totalEvents, auditCount: auditSnapshots.length, suggestedRules, suggestedSuppressions, suggestedPriorities, bootstrap };
|
|
56
105
|
}
|
|
57
106
|
|
|
58
107
|
/**
|
|
59
108
|
* Format suggestions for CLI output.
|
|
60
109
|
*/
|
|
61
110
|
function formatSuggestions(suggestions) {
|
|
62
|
-
const { totalEvents, auditCount, suggestedRules, suggestedSuppressions, suggestedPriorities } = suggestions;
|
|
63
|
-
|
|
64
|
-
if (totalEvents === 0 && auditCount === 0) {
|
|
65
|
-
return ' No usage data yet. Run nerviq fix or nerviq audit to build pattern history.';
|
|
66
|
-
}
|
|
111
|
+
const { totalEvents, auditCount, suggestedRules, suggestedSuppressions, suggestedPriorities, bootstrap } = suggestions;
|
|
67
112
|
|
|
68
113
|
const sources = [];
|
|
69
114
|
if (totalEvents > 0) sources.push(`${totalEvents} pattern events`);
|
|
70
115
|
if (auditCount > 0) sources.push(`${auditCount} audit snapshots`);
|
|
71
|
-
const lines = [
|
|
116
|
+
const lines = [
|
|
117
|
+
sources.length > 0
|
|
118
|
+
? ` Auto-Suggested Rules (based on ${sources.join(', ')}):`
|
|
119
|
+
: ' Auto-Suggested Rules:',
|
|
120
|
+
];
|
|
72
121
|
|
|
73
122
|
if (suggestedRules.length > 0) {
|
|
74
123
|
lines.push('', ' Suggested as required (always accepted):');
|
|
@@ -91,8 +140,12 @@ function formatSuggestions(suggestions) {
|
|
|
91
140
|
}
|
|
92
141
|
}
|
|
93
142
|
|
|
94
|
-
if (suggestedRules.length === 0 && suggestedSuppressions.length === 0 && suggestedPriorities.length === 0) {
|
|
95
|
-
lines.push('',
|
|
143
|
+
if (suggestedRules.length === 0 && suggestedSuppressions.length === 0 && suggestedPriorities.length === 0 && bootstrap && !bootstrap.ready) {
|
|
144
|
+
lines.push('', ` ${bootstrap.message}`);
|
|
145
|
+
lines.push(' Bootstrap it with:');
|
|
146
|
+
for (let i = 0; i < bootstrap.steps.length; i++) {
|
|
147
|
+
lines.push(` ${i + 1}. ${bootstrap.steps[i]}`);
|
|
148
|
+
}
|
|
96
149
|
}
|
|
97
150
|
|
|
98
151
|
return lines.join('\n');
|
package/src/benchmark.js
CHANGED
|
@@ -201,31 +201,36 @@ function buildCaseStudy(before, after, applyResult) {
|
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
function renderBenchmarkMarkdown(report) {
|
|
205
|
-
return [
|
|
206
|
-
'# NERVIQ CLI Benchmark Report',
|
|
207
|
-
'',
|
|
208
|
-
`- Generated by: ${report.generatedBy}`,
|
|
209
|
-
`- Created at: ${report.createdAt}`,
|
|
210
|
-
`- Source repo: ${report.directory}`,
|
|
211
|
-
'',
|
|
212
|
-
'##
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
'',
|
|
220
|
-
'##
|
|
221
|
-
`-
|
|
222
|
-
`- Organic score: ${report.
|
|
223
|
-
`- Passing checks: ${report.
|
|
224
|
-
'',
|
|
225
|
-
'##
|
|
226
|
-
`-
|
|
227
|
-
`-
|
|
228
|
-
`-
|
|
204
|
+
function renderBenchmarkMarkdown(report) {
|
|
205
|
+
return [
|
|
206
|
+
'# NERVIQ CLI Benchmark Report',
|
|
207
|
+
'',
|
|
208
|
+
`- Generated by: ${report.generatedBy}`,
|
|
209
|
+
`- Created at: ${report.createdAt}`,
|
|
210
|
+
`- Source repo: ${report.directory}`,
|
|
211
|
+
'',
|
|
212
|
+
'## Score Semantics',
|
|
213
|
+
`- Baseline live audit score: ${report.scoreSemantics.baseline}`,
|
|
214
|
+
`- Projected benchmark score: ${report.scoreSemantics.projected}`,
|
|
215
|
+
`- Organic score: ${report.scoreSemantics.organic}`,
|
|
216
|
+
'',
|
|
217
|
+
'## Methodology',
|
|
218
|
+
...report.methodology.map(item => `- ${item}`),
|
|
219
|
+
'',
|
|
220
|
+
'## Baseline (Live Repo)',
|
|
221
|
+
`- Live audit score: ${report.before.score}/100`,
|
|
222
|
+
`- Organic live score: ${report.before.organicScore}/100`,
|
|
223
|
+
`- Passing checks: ${report.before.passed}/${report.before.checkCount}`,
|
|
224
|
+
'',
|
|
225
|
+
'## Projected (Isolated Benchmark Copy)',
|
|
226
|
+
`- Projected benchmark score: ${report.after.score}/100`,
|
|
227
|
+
`- Projected organic score: ${report.after.organicScore}/100`,
|
|
228
|
+
`- Passing checks: ${report.after.passed}/${report.after.checkCount}`,
|
|
229
|
+
'',
|
|
230
|
+
'## Delta',
|
|
231
|
+
`- Projected score delta: ${report.delta.score}`,
|
|
232
|
+
`- Projected organic score delta: ${report.delta.organicScore}`,
|
|
233
|
+
`- Passed checks delta: ${report.delta.passed}`,
|
|
229
234
|
'',
|
|
230
235
|
'## Executive Summary',
|
|
231
236
|
`- ${report.executiveSummary.headline}`,
|
|
@@ -285,9 +290,14 @@ async function runBenchmark(options) {
|
|
|
285
290
|
schemaVersion: 1,
|
|
286
291
|
generatedBy: `nerviq@${version}`,
|
|
287
292
|
createdAt: new Date().toISOString(),
|
|
288
|
-
directory: sourceDir,
|
|
289
|
-
platform,
|
|
290
|
-
|
|
293
|
+
directory: sourceDir,
|
|
294
|
+
platform,
|
|
295
|
+
scoreSemantics: {
|
|
296
|
+
baseline: 'current repo state before benchmark runs',
|
|
297
|
+
projected: 'starter-safe post-setup score measured on an isolated temp copy',
|
|
298
|
+
organic: 'repo-owned config quality excluding starter-generated Nerviq assets',
|
|
299
|
+
},
|
|
300
|
+
methodology: [
|
|
291
301
|
'Run a baseline audit on the source repo.',
|
|
292
302
|
'Copy the repo into a temporary isolated workspace.',
|
|
293
303
|
`Apply starter-safe ${platform === 'codex' ? 'Codex' : 'Claude'} artifacts only on the isolated copy.`,
|
|
@@ -316,19 +326,20 @@ function printBenchmark(report, options = {}) {
|
|
|
316
326
|
return;
|
|
317
327
|
}
|
|
318
328
|
|
|
319
|
-
console.log('');
|
|
320
|
-
console.log(' nerviq benchmark');
|
|
321
|
-
console.log(' ═══════════════════════════════════════');
|
|
322
|
-
console.log(' Runs in an isolated temp copy. Your current repo is not modified.');
|
|
323
|
-
console.log('');
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
console.log(`
|
|
328
|
-
console.log(
|
|
329
|
-
console.log(
|
|
330
|
-
console.log(`
|
|
331
|
-
console.log(
|
|
329
|
+
console.log('');
|
|
330
|
+
console.log(' nerviq benchmark');
|
|
331
|
+
console.log(' ═══════════════════════════════════════');
|
|
332
|
+
console.log(' Runs in an isolated temp copy. Your current repo is not modified.');
|
|
333
|
+
console.log(' Score type: baseline = live repo audit, projected = isolated post-setup benchmark.');
|
|
334
|
+
console.log('');
|
|
335
|
+
const orgDeltaSign = report.delta.organicScore >= 0 ? '+' : '';
|
|
336
|
+
const totalDeltaSign = report.delta.score >= 0 ? '+' : '';
|
|
337
|
+
console.log(` Projected organic delta: \x1b[1m${orgDeltaSign}${report.delta.organicScore} points\x1b[0m (repo-owned config quality)`);
|
|
338
|
+
console.log(` Projected total delta with nerviq setup: ${totalDeltaSign}${report.delta.score} points`);
|
|
339
|
+
console.log('');
|
|
340
|
+
console.log(` Baseline live audit: organic ${report.before.organicScore}/100, total ${report.before.score}/100`);
|
|
341
|
+
console.log(` Projected after setup: organic ${report.after.organicScore}/100, total ${report.after.score}/100`);
|
|
342
|
+
console.log('');
|
|
332
343
|
console.log(` ${report.executiveSummary.headline}`);
|
|
333
344
|
console.log(` Recommendation: ${report.executiveSummary.decisionGuidance}`);
|
|
334
345
|
console.log(` Workflow evidence: ${report.workflowEvidence.summary.passed}/${report.workflowEvidence.summary.total} tasks (${report.workflowEvidence.summary.coverageScore}%)`);
|
package/src/codex/freshness.js
CHANGED
|
@@ -1,167 +1,167 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Codex Freshness Operationalization — CP-12
|
|
3
|
-
*
|
|
4
|
-
* Release gates, recurring probes, propagation checklists,
|
|
5
|
-
* and staleness blocking for Codex surfaces.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const { version } = require('../../package.json');
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* P0 sources that must be fresh before any Codex release claim.
|
|
12
|
-
* Each source has a staleness threshold in days.
|
|
13
|
-
*/
|
|
14
|
-
const P0_SOURCES = [
|
|
15
|
-
{
|
|
16
|
-
key: 'codex-cli-docs',
|
|
17
|
-
label: 'Codex CLI Official Docs',
|
|
18
|
-
url: 'https://
|
|
19
|
-
stalenessThresholdDays: 30,
|
|
20
|
-
verifiedAt:
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
key: 'codex-config-reference',
|
|
24
|
-
label: 'Codex Config Reference',
|
|
25
|
-
url: 'https://
|
|
26
|
-
stalenessThresholdDays: 30,
|
|
27
|
-
verifiedAt:
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
key: 'codex-github-action',
|
|
31
|
-
label: 'Codex GitHub Action',
|
|
32
|
-
url: 'https://github.com/openai/codex-action',
|
|
33
|
-
stalenessThresholdDays: 30,
|
|
34
|
-
verifiedAt:
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
key: 'codex-changelog',
|
|
38
|
-
label: 'Codex CLI Changelog',
|
|
39
|
-
url: 'https://github.com/openai/codex
|
|
40
|
-
stalenessThresholdDays: 14,
|
|
41
|
-
verifiedAt:
|
|
42
|
-
},
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Propagation checklist: when a Codex source changes, these must update.
|
|
47
|
-
*/
|
|
48
|
-
const PROPAGATION_CHECKLIST = [
|
|
49
|
-
{
|
|
50
|
-
trigger: 'Codex CLI release with config changes',
|
|
51
|
-
targets: [
|
|
52
|
-
'src/codex/techniques.js — update LEGACY_CONFIG_PATTERNS if keys renamed/removed',
|
|
53
|
-
'src/codex/config-parser.js — update validation rules',
|
|
54
|
-
'src/codex/governance.js — update caveats if behavior changes',
|
|
55
|
-
'test/codex-check-matrix.js — update check expectations',
|
|
56
|
-
],
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
trigger: 'New Codex hook event type added',
|
|
60
|
-
targets: [
|
|
61
|
-
'src/codex/techniques.js — add to SUPPORTED_HOOK_EVENTS',
|
|
62
|
-
'src/codex/governance.js — add to CODEX_HOOK_REGISTRY',
|
|
63
|
-
'src/codex/setup.js — update hooks starter template',
|
|
64
|
-
],
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
trigger: 'New Codex MCP transport or field',
|
|
68
|
-
targets: [
|
|
69
|
-
'src/codex/mcp-packs.js — update pack TOML projections',
|
|
70
|
-
'src/codex/techniques.js — update MCP checks',
|
|
71
|
-
],
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
trigger: 'Codex domain pack definitions change',
|
|
75
|
-
targets: [
|
|
76
|
-
'src/codex/domain-packs.js — update pack registry',
|
|
77
|
-
'src/codex/governance.js — governance export picks up changes',
|
|
78
|
-
],
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
trigger: 'New check category added',
|
|
82
|
-
targets: [
|
|
83
|
-
'src/codex/techniques.js — add check implementations',
|
|
84
|
-
'test/codex-check-matrix.js — add pass/fail scenarios',
|
|
85
|
-
'test/codex-golden-matrix.js — update golden scores',
|
|
86
|
-
],
|
|
87
|
-
},
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Release gate: check if all P0 sources are within staleness threshold.
|
|
92
|
-
* Returns { ready, stale, fresh } arrays.
|
|
93
|
-
*/
|
|
94
|
-
function checkReleaseGate(sourceVerifications = {}) {
|
|
95
|
-
const now = new Date();
|
|
96
|
-
const results = P0_SOURCES.map(source => {
|
|
97
|
-
const verifiedAt = sourceVerifications[source.key]
|
|
98
|
-
? new Date(sourceVerifications[source.key])
|
|
99
|
-
: source.verifiedAt ? new Date(source.verifiedAt) : null;
|
|
100
|
-
|
|
101
|
-
if (!verifiedAt) {
|
|
102
|
-
return { ...source, status: 'unverified', daysStale: null };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const daysSince = Math.floor((now - verifiedAt) / (1000 * 60 * 60 * 24));
|
|
106
|
-
const isStale = daysSince > source.stalenessThresholdDays;
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
...source,
|
|
110
|
-
verifiedAt: verifiedAt.toISOString(),
|
|
111
|
-
daysStale: daysSince,
|
|
112
|
-
status: isStale ? 'stale' : 'fresh',
|
|
113
|
-
};
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
ready: results.every(r => r.status === 'fresh'),
|
|
118
|
-
stale: results.filter(r => r.status === 'stale' || r.status === 'unverified'),
|
|
119
|
-
fresh: results.filter(r => r.status === 'fresh'),
|
|
120
|
-
results,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Format the release gate results for display.
|
|
126
|
-
*/
|
|
127
|
-
function formatReleaseGate(gateResult) {
|
|
128
|
-
const lines = [
|
|
129
|
-
`Codex Freshness Gate (nerviq v${version})`,
|
|
130
|
-
'═══════════════════════════════════════',
|
|
131
|
-
'',
|
|
132
|
-
`Status: ${gateResult.ready ? 'READY' : 'BLOCKED'}`,
|
|
133
|
-
`Fresh: ${gateResult.fresh.length}/${gateResult.results.length}`,
|
|
134
|
-
'',
|
|
135
|
-
];
|
|
136
|
-
|
|
137
|
-
for (const result of gateResult.results) {
|
|
138
|
-
const icon = result.status === 'fresh' ? '✓' : result.status === 'stale' ? '✗' : '?';
|
|
139
|
-
const age = result.daysStale !== null ? ` (${result.daysStale}d ago)` : ' (unverified)';
|
|
140
|
-
lines.push(` ${icon} ${result.label}${age} — threshold: ${result.stalenessThresholdDays}d`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (!gateResult.ready) {
|
|
144
|
-
lines.push('');
|
|
145
|
-
lines.push('Action required: verify stale/unverified sources before claiming release freshness.');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return lines.join('\n');
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Get the propagation checklist for a given trigger.
|
|
153
|
-
*/
|
|
154
|
-
function getPropagationTargets(triggerKeyword) {
|
|
155
|
-
const keyword = triggerKeyword.toLowerCase();
|
|
156
|
-
return PROPAGATION_CHECKLIST.filter(item =>
|
|
157
|
-
item.trigger.toLowerCase().includes(keyword)
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
module.exports = {
|
|
162
|
-
P0_SOURCES,
|
|
163
|
-
PROPAGATION_CHECKLIST,
|
|
164
|
-
checkReleaseGate,
|
|
165
|
-
formatReleaseGate,
|
|
166
|
-
getPropagationTargets,
|
|
167
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Codex Freshness Operationalization — CP-12
|
|
3
|
+
*
|
|
4
|
+
* Release gates, recurring probes, propagation checklists,
|
|
5
|
+
* and staleness blocking for Codex surfaces.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { version } = require('../../package.json');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* P0 sources that must be fresh before any Codex release claim.
|
|
12
|
+
* Each source has a staleness threshold in days.
|
|
13
|
+
*/
|
|
14
|
+
const P0_SOURCES = [
|
|
15
|
+
{
|
|
16
|
+
key: 'codex-cli-docs',
|
|
17
|
+
label: 'Codex CLI Official Docs',
|
|
18
|
+
url: 'https://developers.openai.com/codex/cli',
|
|
19
|
+
stalenessThresholdDays: 30,
|
|
20
|
+
verifiedAt: '2026-04-07',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: 'codex-config-reference',
|
|
24
|
+
label: 'Codex Config Reference',
|
|
25
|
+
url: 'https://developers.openai.com/codex/config-reference',
|
|
26
|
+
stalenessThresholdDays: 30,
|
|
27
|
+
verifiedAt: '2026-04-07',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: 'codex-github-action',
|
|
31
|
+
label: 'Codex GitHub Action',
|
|
32
|
+
url: 'https://github.com/openai/codex-action',
|
|
33
|
+
stalenessThresholdDays: 30,
|
|
34
|
+
verifiedAt: '2026-04-07',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: 'codex-changelog',
|
|
38
|
+
label: 'Codex CLI Changelog',
|
|
39
|
+
url: 'https://github.com/openai/codex/releases',
|
|
40
|
+
stalenessThresholdDays: 14,
|
|
41
|
+
verifiedAt: '2026-04-07',
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Propagation checklist: when a Codex source changes, these must update.
|
|
47
|
+
*/
|
|
48
|
+
const PROPAGATION_CHECKLIST = [
|
|
49
|
+
{
|
|
50
|
+
trigger: 'Codex CLI release with config changes',
|
|
51
|
+
targets: [
|
|
52
|
+
'src/codex/techniques.js — update LEGACY_CONFIG_PATTERNS if keys renamed/removed',
|
|
53
|
+
'src/codex/config-parser.js — update validation rules',
|
|
54
|
+
'src/codex/governance.js — update caveats if behavior changes',
|
|
55
|
+
'test/codex-check-matrix.js — update check expectations',
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
trigger: 'New Codex hook event type added',
|
|
60
|
+
targets: [
|
|
61
|
+
'src/codex/techniques.js — add to SUPPORTED_HOOK_EVENTS',
|
|
62
|
+
'src/codex/governance.js — add to CODEX_HOOK_REGISTRY',
|
|
63
|
+
'src/codex/setup.js — update hooks starter template',
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
trigger: 'New Codex MCP transport or field',
|
|
68
|
+
targets: [
|
|
69
|
+
'src/codex/mcp-packs.js — update pack TOML projections',
|
|
70
|
+
'src/codex/techniques.js — update MCP checks',
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
trigger: 'Codex domain pack definitions change',
|
|
75
|
+
targets: [
|
|
76
|
+
'src/codex/domain-packs.js — update pack registry',
|
|
77
|
+
'src/codex/governance.js — governance export picks up changes',
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
trigger: 'New check category added',
|
|
82
|
+
targets: [
|
|
83
|
+
'src/codex/techniques.js — add check implementations',
|
|
84
|
+
'test/codex-check-matrix.js — add pass/fail scenarios',
|
|
85
|
+
'test/codex-golden-matrix.js — update golden scores',
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Release gate: check if all P0 sources are within staleness threshold.
|
|
92
|
+
* Returns { ready, stale, fresh } arrays.
|
|
93
|
+
*/
|
|
94
|
+
function checkReleaseGate(sourceVerifications = {}) {
|
|
95
|
+
const now = new Date();
|
|
96
|
+
const results = P0_SOURCES.map(source => {
|
|
97
|
+
const verifiedAt = sourceVerifications[source.key]
|
|
98
|
+
? new Date(sourceVerifications[source.key])
|
|
99
|
+
: source.verifiedAt ? new Date(source.verifiedAt) : null;
|
|
100
|
+
|
|
101
|
+
if (!verifiedAt) {
|
|
102
|
+
return { ...source, status: 'unverified', daysStale: null };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const daysSince = Math.floor((now - verifiedAt) / (1000 * 60 * 60 * 24));
|
|
106
|
+
const isStale = daysSince > source.stalenessThresholdDays;
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
...source,
|
|
110
|
+
verifiedAt: verifiedAt.toISOString(),
|
|
111
|
+
daysStale: daysSince,
|
|
112
|
+
status: isStale ? 'stale' : 'fresh',
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
ready: results.every(r => r.status === 'fresh'),
|
|
118
|
+
stale: results.filter(r => r.status === 'stale' || r.status === 'unverified'),
|
|
119
|
+
fresh: results.filter(r => r.status === 'fresh'),
|
|
120
|
+
results,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Format the release gate results for display.
|
|
126
|
+
*/
|
|
127
|
+
function formatReleaseGate(gateResult) {
|
|
128
|
+
const lines = [
|
|
129
|
+
`Codex Freshness Gate (nerviq v${version})`,
|
|
130
|
+
'═══════════════════════════════════════',
|
|
131
|
+
'',
|
|
132
|
+
`Status: ${gateResult.ready ? 'READY' : 'BLOCKED'}`,
|
|
133
|
+
`Fresh: ${gateResult.fresh.length}/${gateResult.results.length}`,
|
|
134
|
+
'',
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
for (const result of gateResult.results) {
|
|
138
|
+
const icon = result.status === 'fresh' ? '✓' : result.status === 'stale' ? '✗' : '?';
|
|
139
|
+
const age = result.daysStale !== null ? ` (${result.daysStale}d ago)` : ' (unverified)';
|
|
140
|
+
lines.push(` ${icon} ${result.label}${age} — threshold: ${result.stalenessThresholdDays}d`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!gateResult.ready) {
|
|
144
|
+
lines.push('');
|
|
145
|
+
lines.push('Action required: verify stale/unverified sources before claiming release freshness.');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return lines.join('\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get the propagation checklist for a given trigger.
|
|
153
|
+
*/
|
|
154
|
+
function getPropagationTargets(triggerKeyword) {
|
|
155
|
+
const keyword = triggerKeyword.toLowerCase();
|
|
156
|
+
return PROPAGATION_CHECKLIST.filter(item =>
|
|
157
|
+
item.trigger.toLowerCase().includes(keyword)
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
P0_SOURCES,
|
|
163
|
+
PROPAGATION_CHECKLIST,
|
|
164
|
+
checkReleaseGate,
|
|
165
|
+
formatReleaseGate,
|
|
166
|
+
getPropagationTargets,
|
|
167
|
+
};
|