delimit-cli 4.1.44 → 4.1.47
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/CHANGELOG.md +6 -0
- package/bin/delimit-cli.js +365 -30
- package/bin/delimit-setup.js +100 -64
- package/gateway/ai/activate_helpers.py +253 -7
- package/gateway/ai/backends/gateway_core.py +236 -13
- package/gateway/ai/backends/repo_bridge.py +80 -16
- package/gateway/ai/backends/tools_infra.py +49 -32
- package/gateway/ai/checksums.sha256 +6 -0
- package/gateway/ai/continuity.py +462 -0
- package/gateway/ai/deliberation.pyi +53 -0
- package/gateway/ai/governance.pyi +32 -0
- package/gateway/ai/governance_hardening.py +569 -0
- package/gateway/ai/inbox_daemon_runner.py +217 -0
- package/gateway/ai/ledger_manager.py +40 -0
- package/gateway/ai/license.py +104 -3
- package/gateway/ai/license_core.py +177 -36
- package/gateway/ai/license_core.pyi +50 -0
- package/gateway/ai/loop_engine.py +786 -22
- package/gateway/ai/reddit_scanner.py +150 -5
- package/gateway/ai/server.py +254 -19
- package/gateway/ai/swarm.py +86 -0
- package/gateway/ai/swarm_infra.py +656 -0
- package/gateway/ai/tweet_corpus_schema.sql +76 -0
- package/gateway/core/diff_engine_v2.py +6 -2
- package/gateway/core/generator_drift.py +242 -0
- package/gateway/core/json_schema_diff.py +375 -0
- package/gateway/core/openapi_version.py +124 -0
- package/gateway/core/spec_detector.py +47 -7
- package/gateway/core/spec_health.py +5 -2
- package/lib/cross-model-hooks.js +4 -12
- package/package.json +8 -1
- package/scripts/sync-gateway.sh +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.1.45] - 2026-04-09
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Shim rename-hack removed** — install no longer races with npm reinstalls that clobbered `/usr/bin/claude` back to a symlink, causing `[Delimit] claude not found in PATH` mid-session. Shim now relies purely on `$HOME/.delimit/shims` being first in `PATH` plus a PATH-strip lookup for the real binary. Fixes regressions from the claude-real rename+wrap install mechanism.
|
|
7
|
+
- Shim exit screen parity and CLI lint output parity (LED-078, LED-087).
|
|
8
|
+
|
|
3
9
|
## [4.20.0] - 2026-04-20
|
|
4
10
|
|
|
5
11
|
*The highest state of AI governance.*
|
package/bin/delimit-cli.js
CHANGED
|
@@ -3704,17 +3704,127 @@ program
|
|
|
3704
3704
|
.description('Verify Delimit setup and diagnose common issues')
|
|
3705
3705
|
.option('--ci', 'Output JSON and exit non-zero on failures (for pipelines)')
|
|
3706
3706
|
.option('--fix', 'Automatically fix issues that have safe auto-fixes')
|
|
3707
|
+
.option('--dry-run', 'Preview what doctor --fix would create/modify without making changes')
|
|
3708
|
+
.option('--undo', 'Revert changes made by the last doctor --fix run')
|
|
3707
3709
|
.action(async (opts) => {
|
|
3708
3710
|
const ciMode = !!opts.ci;
|
|
3709
3711
|
const fixMode = !!opts.fix;
|
|
3712
|
+
const dryRunMode = !!opts.dryRun;
|
|
3713
|
+
const undoMode = !!opts.undo;
|
|
3710
3714
|
const homeDir = os.homedir();
|
|
3711
3715
|
const delimitHome = path.join(homeDir, '.delimit');
|
|
3716
|
+
const manifestPath = path.join(process.cwd(), '.delimit', 'doctor-manifest.json');
|
|
3717
|
+
|
|
3718
|
+
// --- Undo mode: revert last doctor --fix changes ---
|
|
3719
|
+
if (undoMode) {
|
|
3720
|
+
if (!fs.existsSync(manifestPath)) {
|
|
3721
|
+
console.log(chalk.yellow('\n No doctor-manifest.json found. Nothing to undo.\n'));
|
|
3722
|
+
return;
|
|
3723
|
+
}
|
|
3724
|
+
try {
|
|
3725
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
3726
|
+
const actions = manifest.actions || [];
|
|
3727
|
+
let reverted = 0;
|
|
3728
|
+
let skipped = 0;
|
|
3729
|
+
console.log(chalk.bold('\n Delimit Doctor — Undo\n'));
|
|
3730
|
+
for (const entry of actions) {
|
|
3731
|
+
const targetPath = entry.path;
|
|
3732
|
+
if (entry.action === 'created') {
|
|
3733
|
+
if (fs.existsSync(targetPath)) {
|
|
3734
|
+
const stat = fs.statSync(targetPath);
|
|
3735
|
+
if (stat.isDirectory()) {
|
|
3736
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
3737
|
+
} else {
|
|
3738
|
+
fs.unlinkSync(targetPath);
|
|
3739
|
+
}
|
|
3740
|
+
console.log(chalk.red(` - Removed: ${targetPath}`));
|
|
3741
|
+
reverted++;
|
|
3742
|
+
} else {
|
|
3743
|
+
console.log(chalk.gray(` - Already gone: ${targetPath}`));
|
|
3744
|
+
skipped++;
|
|
3745
|
+
}
|
|
3746
|
+
} else {
|
|
3747
|
+
console.log(chalk.yellow(` - Skipped (${entry.action}): ${targetPath}`));
|
|
3748
|
+
skipped++;
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
fs.unlinkSync(manifestPath);
|
|
3752
|
+
console.log(chalk.green(`\n Reverted ${reverted} item(s), skipped ${skipped}.\n`));
|
|
3753
|
+
} catch (e) {
|
|
3754
|
+
console.log(chalk.red(`\n Failed to read manifest: ${e.message}\n`));
|
|
3755
|
+
process.exitCode = 1;
|
|
3756
|
+
}
|
|
3757
|
+
return;
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
// --- Dry-run mode: preview what --fix would create/modify ---
|
|
3761
|
+
if (dryRunMode) {
|
|
3762
|
+
console.log(chalk.bold('\n Delimit Doctor — Dry Run Preview\n'));
|
|
3763
|
+
const planned = [];
|
|
3764
|
+
const delimitDir = path.join(process.cwd(), '.delimit');
|
|
3765
|
+
const policyFile = path.join(delimitDir, 'policies.yml');
|
|
3766
|
+
const ledgerDir = path.join(delimitDir, 'ledger');
|
|
3767
|
+
const evidenceDir = path.join(delimitDir, 'evidence');
|
|
3768
|
+
const memoryDir = path.join(delimitHome, 'memory');
|
|
3769
|
+
const mcpServerPath = path.join(delimitHome, 'server', 'ai', 'server.py');
|
|
3770
|
+
|
|
3771
|
+
if (!fs.existsSync(policyFile)) {
|
|
3772
|
+
if (!fs.existsSync(delimitDir)) {
|
|
3773
|
+
planned.push({ path: delimitDir, action: 'create_dir', description: '.delimit/ governance directory' });
|
|
3774
|
+
}
|
|
3775
|
+
planned.push({ path: policyFile, action: 'create_file', description: 'Governance policy rules (via delimit init)' });
|
|
3776
|
+
}
|
|
3777
|
+
if (!fs.existsSync(ledgerDir)) {
|
|
3778
|
+
planned.push({ path: ledgerDir, action: 'create_dir', description: 'Operations ledger directory' });
|
|
3779
|
+
}
|
|
3780
|
+
if (!fs.existsSync(evidenceDir)) {
|
|
3781
|
+
planned.push({ path: evidenceDir, action: 'create_dir', description: 'Audit trail events directory' });
|
|
3782
|
+
}
|
|
3783
|
+
if (!fs.existsSync(memoryDir)) {
|
|
3784
|
+
planned.push({ path: memoryDir, action: 'create_dir', description: '~/.delimit/memory/ directory' });
|
|
3785
|
+
}
|
|
3786
|
+
if (!fs.existsSync(mcpServerPath)) {
|
|
3787
|
+
planned.push({ path: mcpServerPath, action: 'create_file', description: 'MCP server (via delimit setup --all)' });
|
|
3788
|
+
}
|
|
3789
|
+
// GitHub workflow
|
|
3790
|
+
const workflowDir = path.join(process.cwd(), '.github', 'workflows');
|
|
3791
|
+
if (fs.existsSync(path.join(process.cwd(), '.github'))) {
|
|
3792
|
+
const wf = path.join(workflowDir, 'api-governance.yml');
|
|
3793
|
+
if (!fs.existsSync(wf)) {
|
|
3794
|
+
planned.push({ path: wf, action: 'create_file', description: 'API governance GitHub Action workflow' });
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
if (planned.length === 0) {
|
|
3799
|
+
console.log(chalk.green(' No changes needed. Everything looks good.\n'));
|
|
3800
|
+
} else {
|
|
3801
|
+
console.log(chalk.gray(` doctor --fix would create/modify ${planned.length} item(s):\n`));
|
|
3802
|
+
for (const p of planned) {
|
|
3803
|
+
const icon = p.action.startsWith('create') ? '+' : '~';
|
|
3804
|
+
console.log(chalk.gray(` ${icon} ${p.path}`));
|
|
3805
|
+
console.log(chalk.gray(` ${p.description}`));
|
|
3806
|
+
}
|
|
3807
|
+
console.log(chalk.gray(`\n Run ${chalk.bold('delimit doctor --fix')} to apply these changes.\n`));
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
if (ciMode) {
|
|
3811
|
+
console.log(JSON.stringify({ status: 'dry_run', planned_changes: planned, change_count: planned.length }, null, 2));
|
|
3812
|
+
}
|
|
3813
|
+
return;
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3712
3816
|
const results = []; // { name, status: 'pass'|'warn'|'fail', message, fix? }
|
|
3817
|
+
const manifestActions = []; // track what --fix creates
|
|
3713
3818
|
|
|
3714
3819
|
function addResult(name, status, message, fix) {
|
|
3715
3820
|
results.push({ name, status, message, fix: fix || null });
|
|
3716
3821
|
}
|
|
3717
3822
|
|
|
3823
|
+
// Helper: record a created file/dir in the manifest
|
|
3824
|
+
function trackCreated(filePath) {
|
|
3825
|
+
manifestActions.push({ path: filePath, action: 'created', timestamp: new Date().toISOString() });
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3718
3828
|
// --- Check 1: Policy file ---
|
|
3719
3829
|
const policyPath = path.join(process.cwd(), '.delimit', 'policies.yml');
|
|
3720
3830
|
if (fs.existsSync(policyPath)) {
|
|
@@ -3733,10 +3843,13 @@ program
|
|
|
3733
3843
|
addResult('policy-file', 'fail', 'No .delimit/policies.yml', 'delimit init');
|
|
3734
3844
|
if (fixMode) {
|
|
3735
3845
|
try {
|
|
3846
|
+
const delimitDirPre = fs.existsSync(path.join(process.cwd(), '.delimit'));
|
|
3736
3847
|
execSync('delimit init --dry-run', { stdio: 'pipe', cwd: process.cwd() });
|
|
3737
3848
|
// If dry-run works, run real init
|
|
3738
3849
|
execSync('delimit init', { stdio: 'pipe', cwd: process.cwd() });
|
|
3739
3850
|
addResult('policy-file-fix', 'pass', 'Auto-fixed: ran delimit init');
|
|
3851
|
+
if (!delimitDirPre) trackCreated(path.join(process.cwd(), '.delimit'));
|
|
3852
|
+
trackCreated(policyPath);
|
|
3740
3853
|
} catch {
|
|
3741
3854
|
addResult('policy-file-fix', 'warn', 'Auto-fix failed: run delimit init manually');
|
|
3742
3855
|
}
|
|
@@ -3834,6 +3947,7 @@ program
|
|
|
3834
3947
|
try {
|
|
3835
3948
|
execSync('delimit setup --all', { stdio: 'pipe' });
|
|
3836
3949
|
addResult('mcp-server-fix', 'pass', 'Auto-fixed: ran delimit setup --all');
|
|
3950
|
+
trackCreated(mcpServerPath);
|
|
3837
3951
|
} catch {
|
|
3838
3952
|
addResult('mcp-server-fix', 'warn', 'Auto-fix failed: run delimit setup --all manually');
|
|
3839
3953
|
}
|
|
@@ -3862,6 +3976,7 @@ program
|
|
|
3862
3976
|
try {
|
|
3863
3977
|
fs.mkdirSync(memoryDir, { recursive: true });
|
|
3864
3978
|
addResult('memory-health-fix', 'pass', 'Auto-fixed: created ~/.delimit/memory/');
|
|
3979
|
+
trackCreated(memoryDir);
|
|
3865
3980
|
} catch {
|
|
3866
3981
|
addResult('memory-health-fix', 'warn', `Auto-fix failed: run mkdir -p ${memoryDir}`);
|
|
3867
3982
|
}
|
|
@@ -4000,10 +4115,30 @@ program
|
|
|
4000
4115
|
}
|
|
4001
4116
|
}
|
|
4002
4117
|
|
|
4003
|
-
//
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4118
|
+
// Save manifest if --fix made changes (LED-265)
|
|
4119
|
+
if (fixMode && manifestActions.length > 0) {
|
|
4120
|
+
const manifestDir = path.join(process.cwd(), '.delimit');
|
|
4121
|
+
if (!fs.existsSync(manifestDir)) {
|
|
4122
|
+
fs.mkdirSync(manifestDir, { recursive: true });
|
|
4123
|
+
}
|
|
4124
|
+
const manifest = {
|
|
4125
|
+
version: 1,
|
|
4126
|
+
created: new Date().toISOString(),
|
|
4127
|
+
actions: manifestActions,
|
|
4128
|
+
};
|
|
4129
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
4130
|
+
console.log(chalk.bold('\n Manifest:'));
|
|
4131
|
+
console.log(chalk.gray(` Saved ${manifestActions.length} action(s) to .delimit/doctor-manifest.json`));
|
|
4132
|
+
console.log(chalk.gray(' Run: delimit doctor --undo to revert\n'));
|
|
4133
|
+
} else {
|
|
4134
|
+
// Undo instruction (LED-265)
|
|
4135
|
+
console.log(chalk.bold('\n Undo:'));
|
|
4136
|
+
if (fs.existsSync(manifestPath)) {
|
|
4137
|
+
console.log(chalk.gray(' delimit doctor --undo — revert last doctor --fix changes'));
|
|
4138
|
+
}
|
|
4139
|
+
console.log(chalk.gray(' rm -rf .delimit — remove all Delimit files'));
|
|
4140
|
+
console.log(chalk.gray(' delimit uninstall --dry-run — preview MCP removal\n'));
|
|
4141
|
+
}
|
|
4007
4142
|
|
|
4008
4143
|
// Health score and summary
|
|
4009
4144
|
const ok = results.filter(r => r.status === 'pass').length;
|
|
@@ -4955,50 +5090,250 @@ program
|
|
|
4955
5090
|
return;
|
|
4956
5091
|
}
|
|
4957
5092
|
|
|
4958
|
-
//
|
|
5093
|
+
// Detect CI environment — use plain output (no color) when not a TTY
|
|
5094
|
+
const isCI = !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.JENKINS_URL || process.env.GITLAB_CI || process.env.CIRCLECI || process.env.TRAVIS);
|
|
5095
|
+
const isTTY = process.stdout.isTTY;
|
|
5096
|
+
const useColor = isTTY && !isCI && !process.env.NO_COLOR;
|
|
5097
|
+
|
|
5098
|
+
// Severity classification for violations (mirrors Action's ci_formatter.py)
|
|
5099
|
+
const SEVERITY_MAP = {
|
|
5100
|
+
'no_endpoint_removal': { label: 'Critical', color: 'red' },
|
|
5101
|
+
'no_method_removal': { label: 'Critical', color: 'red' },
|
|
5102
|
+
'no_field_removal': { label: 'Critical', color: 'red' },
|
|
5103
|
+
'no_response_field_removal': { label: 'Critical', color: 'red' },
|
|
5104
|
+
'no_required_param_addition': { label: 'High', color: 'yellow' },
|
|
5105
|
+
'no_type_changes': { label: 'High', color: 'yellow' },
|
|
5106
|
+
'warn_type_change': { label: 'High', color: 'yellow' },
|
|
5107
|
+
'no_enum_removal': { label: 'High', color: 'yellow' },
|
|
5108
|
+
};
|
|
5109
|
+
|
|
5110
|
+
// Teachings — WHY each rule matters (mirrors Action's ci_formatter.py TEACHINGS)
|
|
5111
|
+
const TEACHINGS = {
|
|
5112
|
+
'no_endpoint_removal': 'Removing an endpoint breaks existing clients actively calling it. Their requests will return 404.',
|
|
5113
|
+
'no_method_removal': 'Removing an HTTP method breaks clients using that verb. They will receive 405 Method Not Allowed.',
|
|
5114
|
+
'no_required_param_addition': 'Adding a required parameter breaks every existing request that omits it. Clients get 400 Bad Request.',
|
|
5115
|
+
'no_field_removal': 'Removing a request field breaks clients sending it if the server rejects the payload or silently drops data.',
|
|
5116
|
+
'no_response_field_removal': 'Removing a response field breaks clients reading it. Their code hits undefined/null.',
|
|
5117
|
+
'no_type_changes': 'Changing a field type breaks serialization. Clients parsing the old type will fail.',
|
|
5118
|
+
'warn_type_change': 'Changing a field type breaks serialization. Clients parsing the old type will fail.',
|
|
5119
|
+
'no_enum_removal': 'Removing an enum value breaks clients that send or compare against it.',
|
|
5120
|
+
};
|
|
5121
|
+
|
|
5122
|
+
// Fix hints — HOW to fix each rule (mirrors Action's ci_formatter.py FIX_HINTS)
|
|
5123
|
+
const FIX_HINTS = {
|
|
5124
|
+
'no_endpoint_removal': 'Deprecate the endpoint first, then remove in a future major version.',
|
|
5125
|
+
'no_method_removal': 'Keep the old method available or redirect it. Remove only after a deprecation period.',
|
|
5126
|
+
'no_required_param_addition': 'Make the new parameter optional with a sensible default value.',
|
|
5127
|
+
'no_field_removal': 'Keep the field in the schema. Mark it deprecated and stop populating in a future version.',
|
|
5128
|
+
'no_response_field_removal': 'Restore the field. If removing is intentional, version the endpoint (e.g., /v2/).',
|
|
5129
|
+
'no_type_changes': 'Revert the type change, or introduce a new field with the desired type and deprecate the old one.',
|
|
5130
|
+
'warn_type_change': 'Revert the type change, or introduce a new field with the desired type and deprecate the old one.',
|
|
5131
|
+
'no_enum_removal': 'Keep the enum value and mark it deprecated. Remove only in a coordinated major release.',
|
|
5132
|
+
};
|
|
5133
|
+
|
|
5134
|
+
// Helper: colorize or plain text
|
|
5135
|
+
const c = {
|
|
5136
|
+
red: (s) => useColor ? chalk.red(s) : s,
|
|
5137
|
+
green: (s) => useColor ? chalk.green(s) : s,
|
|
5138
|
+
yellow: (s) => useColor ? chalk.yellow(s) : s,
|
|
5139
|
+
gray: (s) => useColor ? chalk.gray(s) : s,
|
|
5140
|
+
bold: (s) => useColor ? chalk.bold(s) : s,
|
|
5141
|
+
redBold: (s) => useColor ? chalk.red.bold(s) : s,
|
|
5142
|
+
greenBold: (s) => useColor ? chalk.green.bold(s) : s,
|
|
5143
|
+
yellowBold: (s) => useColor ? chalk.yellow.bold(s) : s,
|
|
5144
|
+
dim: (s) => useColor ? chalk.dim(s) : s,
|
|
5145
|
+
cyan: (s) => useColor ? chalk.cyan(s) : s,
|
|
5146
|
+
};
|
|
5147
|
+
|
|
4959
5148
|
const decision = result.decision;
|
|
4960
5149
|
const semver = result.semver;
|
|
4961
|
-
const
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
5150
|
+
const s = result.summary;
|
|
5151
|
+
const violations = result.violations || [];
|
|
5152
|
+
const allChanges = result.all_changes || [];
|
|
5153
|
+
const errors = violations.filter(v => v.severity === 'error');
|
|
5154
|
+
const warnings = violations.filter(v => v.severity === 'warning');
|
|
5155
|
+
const safe = allChanges.filter(ch => !ch.is_breaking);
|
|
4966
5156
|
|
|
4967
|
-
|
|
4968
|
-
const
|
|
5157
|
+
// ── Header Banner ──
|
|
5158
|
+
const divider = useColor ? chalk.dim('─'.repeat(60)) : '-'.repeat(60);
|
|
5159
|
+
console.log('');
|
|
5160
|
+
console.log(divider);
|
|
4969
5161
|
|
|
4970
|
-
|
|
5162
|
+
if (decision === 'fail') {
|
|
5163
|
+
console.log(c.redBold(' GOVERNANCE FAILED'));
|
|
5164
|
+
} else if (decision === 'warn') {
|
|
5165
|
+
console.log(c.yellowBold(' GOVERNANCE PASSED WITH WARNINGS'));
|
|
5166
|
+
} else {
|
|
5167
|
+
console.log(c.greenBold(' GOVERNANCE PASSED'));
|
|
5168
|
+
}
|
|
4971
5169
|
|
|
4972
|
-
//
|
|
4973
|
-
const
|
|
4974
|
-
|
|
5170
|
+
// Semver line
|
|
5171
|
+
const bumpLabel = semver ? semver.bump.toUpperCase() : 'NONE';
|
|
5172
|
+
const nextVerStr = semver && semver.next_version ? ` Next: ${semver.next_version}` : '';
|
|
5173
|
+
console.log(` Semver: ${c.bold(bumpLabel)}${nextVerStr}`);
|
|
5174
|
+
console.log(divider);
|
|
5175
|
+
|
|
5176
|
+
// ── Summary Stats ──
|
|
5177
|
+
console.log('');
|
|
5178
|
+
console.log(` Total changes: ${s.total_changes}`);
|
|
5179
|
+
console.log(` Breaking changes: ${s.breaking_changes > 0 ? c.red(String(s.breaking_changes)) : c.green('0')}`);
|
|
5180
|
+
console.log(` Policy violations: ${s.violations > 0 ? c.red(String(s.violations)) : c.green('0')}`);
|
|
4975
5181
|
if (s.violations > 0) {
|
|
4976
|
-
console.log(`
|
|
5182
|
+
console.log(` Errors: ${s.errors}`);
|
|
5183
|
+
console.log(` Warnings: ${s.warnings}`);
|
|
4977
5184
|
}
|
|
4978
5185
|
console.log('');
|
|
4979
5186
|
|
|
4980
|
-
//
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
5187
|
+
// ── Breaking Changes Table ──
|
|
5188
|
+
if (errors.length > 0 || warnings.length > 0) {
|
|
5189
|
+
console.log(c.bold(' Breaking Changes'));
|
|
5190
|
+
console.log(divider);
|
|
5191
|
+
console.log('');
|
|
5192
|
+
|
|
5193
|
+
// Table header
|
|
5194
|
+
const colSev = 10;
|
|
5195
|
+
const colLoc = 32;
|
|
5196
|
+
const colMsg = 50;
|
|
5197
|
+
const pad = (str, len) => {
|
|
5198
|
+
const stripped = str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
5199
|
+
const diff = len - stripped.length;
|
|
5200
|
+
return diff > 0 ? str + ' '.repeat(diff) : str;
|
|
5201
|
+
};
|
|
5202
|
+
|
|
5203
|
+
console.log(` ${pad(c.bold('Severity'), colSev)} ${pad(c.bold('Location'), colLoc)} ${c.bold('Description')}`);
|
|
5204
|
+
console.log(` ${'-'.repeat(colSev)} ${'-'.repeat(colLoc)} ${'-'.repeat(colMsg)}`);
|
|
5205
|
+
|
|
5206
|
+
errors.forEach(v => {
|
|
5207
|
+
const sev = SEVERITY_MAP[v.rule] || { label: 'Error', color: 'red' };
|
|
5208
|
+
const sevStr = sev.color === 'red' ? c.red(sev.label) : c.yellow(sev.label);
|
|
5209
|
+
const loc = v.path || '-';
|
|
5210
|
+
const truncLoc = loc.length > colLoc ? loc.substring(0, colLoc - 3) + '...' : loc;
|
|
5211
|
+
console.log(` ${pad(sevStr, colSev)} ${pad(c.cyan(truncLoc), colLoc)} ${v.message}`);
|
|
5212
|
+
});
|
|
5213
|
+
|
|
5214
|
+
warnings.forEach(v => {
|
|
5215
|
+
const sev = SEVERITY_MAP[v.rule] || { label: 'Medium', color: 'yellow' };
|
|
5216
|
+
const sevStr = c.yellow(sev.label);
|
|
5217
|
+
const loc = v.path || '-';
|
|
5218
|
+
const truncLoc = loc.length > colLoc ? loc.substring(0, colLoc - 3) + '...' : loc;
|
|
5219
|
+
console.log(` ${pad(sevStr, colSev)} ${pad(c.cyan(truncLoc), colLoc)} ${v.message}`);
|
|
5220
|
+
});
|
|
5221
|
+
|
|
5222
|
+
console.log('');
|
|
5223
|
+
}
|
|
5224
|
+
|
|
5225
|
+
// ── Why This Breaks (Teachings) ──
|
|
5226
|
+
if (errors.length > 0) {
|
|
5227
|
+
console.log(c.bold(' Why This Breaks'));
|
|
5228
|
+
console.log(divider);
|
|
5229
|
+
console.log('');
|
|
5230
|
+
|
|
5231
|
+
// Deduplicate by rule
|
|
5232
|
+
const seenRules = new Set();
|
|
5233
|
+
errors.forEach(v => {
|
|
5234
|
+
if (v.rule && TEACHINGS[v.rule] && !seenRules.has(v.rule)) {
|
|
5235
|
+
seenRules.add(v.rule);
|
|
5236
|
+
const ruleName = v.rule.replace(/^no_/, '').replace(/_/g, ' ');
|
|
5237
|
+
console.log(` ${c.red('*')} ${c.bold(ruleName)}`);
|
|
5238
|
+
console.log(` ${c.gray(TEACHINGS[v.rule])}`);
|
|
5239
|
+
console.log('');
|
|
5240
|
+
}
|
|
5241
|
+
});
|
|
5242
|
+
}
|
|
5243
|
+
|
|
5244
|
+
// ── How to Fix (Migration Hints) ──
|
|
5245
|
+
if (errors.length > 0) {
|
|
5246
|
+
console.log(c.bold(' How to Fix'));
|
|
5247
|
+
console.log(divider);
|
|
5248
|
+
console.log('');
|
|
5249
|
+
|
|
5250
|
+
errors.forEach((v, i) => {
|
|
5251
|
+
const loc = v.path || '-';
|
|
5252
|
+
const hint = FIX_HINTS[v.rule] || 'Review this change and update consumers accordingly.';
|
|
5253
|
+
console.log(` ${c.bold(`${i + 1}. ${loc}`)}`);
|
|
5254
|
+
console.log(` ${hint}`);
|
|
5255
|
+
console.log('');
|
|
5256
|
+
});
|
|
5257
|
+
}
|
|
5258
|
+
|
|
5259
|
+
// ── Migration Guide (if available from engine) ──
|
|
5260
|
+
if (result.migration && decision === 'fail') {
|
|
5261
|
+
console.log(c.bold(' Migration Guide'));
|
|
5262
|
+
console.log(divider);
|
|
5263
|
+
console.log('');
|
|
5264
|
+
// Indent migration text
|
|
5265
|
+
const migrationLines = result.migration.split('\n');
|
|
5266
|
+
migrationLines.forEach(line => {
|
|
5267
|
+
console.log(` ${line}`);
|
|
4987
5268
|
});
|
|
4988
5269
|
console.log('');
|
|
4989
5270
|
}
|
|
4990
5271
|
|
|
4991
|
-
// Non-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
console.log(
|
|
4995
|
-
|
|
5272
|
+
// ── Non-Breaking Additions ──
|
|
5273
|
+
if (safe.length > 0 && safe.length <= 20) {
|
|
5274
|
+
console.log(c.bold(` Non-Breaking Additions (${safe.length})`));
|
|
5275
|
+
console.log(divider);
|
|
5276
|
+
console.log('');
|
|
5277
|
+
safe.forEach(ch => {
|
|
5278
|
+
console.log(` ${c.green('+')} ${ch.message}`);
|
|
5279
|
+
if (ch.path) console.log(` ${c.gray(ch.path)}`);
|
|
5280
|
+
});
|
|
5281
|
+
console.log('');
|
|
5282
|
+
} else if (safe.length > 20) {
|
|
5283
|
+
console.log(c.bold(` Non-Breaking Additions (${safe.length})`));
|
|
5284
|
+
console.log(divider);
|
|
5285
|
+
console.log('');
|
|
5286
|
+
safe.slice(0, 10).forEach(ch => {
|
|
5287
|
+
console.log(` ${c.green('+')} ${ch.message}`);
|
|
5288
|
+
});
|
|
5289
|
+
console.log(c.gray(` ... and ${safe.length - 10} more additions`));
|
|
4996
5290
|
console.log('');
|
|
4997
5291
|
}
|
|
4998
5292
|
|
|
5293
|
+
// ── Governance Gates ──
|
|
5294
|
+
console.log(c.bold(' Governance Gates'));
|
|
5295
|
+
console.log(divider);
|
|
5296
|
+
console.log('');
|
|
5297
|
+
|
|
5298
|
+
const lintPass = s.breaking_changes === 0;
|
|
5299
|
+
const policyPass = violations.length === 0;
|
|
5300
|
+
const deployReady = lintPass && policyPass;
|
|
5301
|
+
|
|
5302
|
+
const gateIcon = (pass) => pass ? c.green('PASS') : c.red('FAIL');
|
|
5303
|
+
const gates = [
|
|
5304
|
+
['API Lint', lintPass],
|
|
5305
|
+
['Policy Compliance', policyPass],
|
|
5306
|
+
['Deploy Readiness', deployReady],
|
|
5307
|
+
];
|
|
5308
|
+
|
|
5309
|
+
const gateCol = 22;
|
|
5310
|
+
console.log(` ${c.bold('Gate'.padEnd(gateCol))} ${c.bold('Status')}`);
|
|
5311
|
+
console.log(` ${'-'.repeat(gateCol)} ${'-'.repeat(10)}`);
|
|
5312
|
+
gates.forEach(([name, pass]) => {
|
|
5313
|
+
const status = pass ? gateIcon(true) : gateIcon(false);
|
|
5314
|
+
if (name === 'Policy Compliance' && !policyPass) {
|
|
5315
|
+
console.log(` ${name.padEnd(gateCol)} ${status} (${violations.length} violation${violations.length !== 1 ? 's' : ''})`);
|
|
5316
|
+
} else if (name === 'Deploy Readiness' && !deployReady) {
|
|
5317
|
+
console.log(` ${name.padEnd(gateCol)} ${c.yellow('BLOCKED')}`);
|
|
5318
|
+
} else {
|
|
5319
|
+
console.log(` ${name.padEnd(gateCol)} ${status}`);
|
|
5320
|
+
}
|
|
5321
|
+
});
|
|
5322
|
+
console.log('');
|
|
5323
|
+
|
|
5324
|
+
if (!deployReady) {
|
|
5325
|
+
console.log(c.yellow(' Deploy blocked until all gates pass.'));
|
|
5326
|
+
console.log('');
|
|
5327
|
+
}
|
|
5328
|
+
|
|
5329
|
+
// ── Footer ──
|
|
5330
|
+
console.log(divider);
|
|
4999
5331
|
if (decision === 'pass') {
|
|
5000
|
-
console.log('Keep Building
|
|
5332
|
+
console.log(c.green(' Keep Building.'));
|
|
5333
|
+
} else {
|
|
5334
|
+
console.log(c.gray(' Fix the issues above, then re-run: npx delimit-cli lint'));
|
|
5001
5335
|
}
|
|
5336
|
+
console.log('');
|
|
5002
5337
|
|
|
5003
5338
|
process.exit(result.exit_code || 0);
|
|
5004
5339
|
} catch (err) {
|