codehere 1.0.1 → 1.0.2
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 +36 -192
- package/dist/application/use-cases/check-claimed-tests.js +81 -0
- package/dist/application/use-cases/detect-test-weakening.js +237 -0
- package/dist/application/use-cases/rerun-tests.js +110 -7
- package/dist/index.js +38 -1212
- package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -323
- package/dist/presentation/cli/commands/check-command.js +178 -23
- package/dist/presentation/cli/commands/help-command.js +21 -285
- package/dist/ui.js +5 -236
- package/dist/utils/version.js +0 -6
- package/package.json +9 -38
- package/dist/application/services/verdict-service.js +0 -154
- package/dist/application/use-cases/assess-cross-vendor.js +0 -126
- package/dist/application/use-cases/build-cross-vendor-verdict.js +0 -118
- package/dist/application/use-cases/domain-trust.js +0 -110
- package/dist/application/use-cases/export-aat.js +0 -107
- package/dist/application/use-cases/generate-acceptance-criteria.js +0 -223
- package/dist/application/use-cases/generate-final-report.js +0 -259
- package/dist/application/use-cases/generate-narration.js +0 -130
- package/dist/application/use-cases/generate-thread-title.js +0 -181
- package/dist/application/use-cases/narrator-prompt-template.js +0 -158
- package/dist/application/use-cases/plan-auto-review.js +0 -57
- package/dist/application/use-cases/reconcile-audit.js +0 -100
- package/dist/application/use-cases/search-sessions.js +0 -70
- package/dist/application/use-cases/should-auto-review.js +0 -71
- package/dist/application/use-cases/thread-export.js +0 -245
- package/dist/application/use-cases/verify-claim.js +0 -456
- package/dist/application/use-cases/write-pass.js +0 -277
- package/dist/domain/audit.js +0 -11
- package/dist/domain/services/codebase-detector.js +0 -85
- package/dist/domain/task.js +0 -91
- package/dist/error-handler.js +0 -139
- package/dist/infrastructure/completion/bash-completion.js +0 -29
- package/dist/infrastructure/completion/fish-completion.js +0 -17
- package/dist/infrastructure/completion/zsh-completion.js +0 -25
- package/dist/infrastructure/config/config-manager.js +0 -174
- package/dist/infrastructure/config/product-context.js +0 -49
- package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
- package/dist/infrastructure/context/adapters/figma-source.js +0 -198
- package/dist/infrastructure/context/adapters/github-issue-source.js +0 -160
- package/dist/infrastructure/context/adapters/linear-source.js +0 -190
- package/dist/infrastructure/context/context-resolver.js +0 -144
- package/dist/infrastructure/context/context-source.js +0 -114
- package/dist/infrastructure/context/github-api.js +0 -110
- package/dist/infrastructure/context/github-device-flow.js +0 -65
- package/dist/infrastructure/context/oauth-flow.js +0 -207
- package/dist/infrastructure/context/oauth-store.js +0 -122
- package/dist/infrastructure/context/registry-bootstrap.js +0 -95
- package/dist/infrastructure/context/write-pass-resolver.js +0 -106
- package/dist/infrastructure/cost/budget-config.js +0 -114
- package/dist/infrastructure/cost/cost-router.js +0 -97
- package/dist/infrastructure/cost/spend-aggregator.js +0 -222
- package/dist/infrastructure/cost/team-ledger.js +0 -355
- package/dist/infrastructure/discovery/discovery-log.js +0 -85
- package/dist/infrastructure/hooks/audit-tip-signature.js +0 -336
- package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -1018
- package/dist/infrastructure/hooks/hook-mode.js +0 -87
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -49
- package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
- package/dist/infrastructure/hooks/session-analyzer.js +0 -177
- package/dist/infrastructure/observability/execution-tracer.js +0 -227
- package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
- package/dist/infrastructure/orchestration/agent-installer.js +0 -101
- package/dist/infrastructure/orchestration/agents-config-seed.js +0 -68
- package/dist/infrastructure/orchestration/aider-adapter.js +0 -611
- package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
- package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -592
- package/dist/infrastructure/orchestration/codex-adapter.js +0 -574
- package/dist/infrastructure/orchestration/conductor-adapter.js +0 -416
- package/dist/infrastructure/orchestration/context-handoff.js +0 -162
- package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
- package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
- package/dist/infrastructure/orchestration/detect-ollama.js +0 -75
- package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
- package/dist/infrastructure/orchestration/failover-config.js +0 -67
- package/dist/infrastructure/orchestration/failover-policy.js +0 -106
- package/dist/infrastructure/orchestration/gemini-adapter.js +0 -663
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -689
- package/dist/infrastructure/orchestration/graphify-context.js +0 -209
- package/dist/infrastructure/orchestration/memory-injector.js +0 -380
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -388
- package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
- package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
- package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
- package/dist/infrastructure/orchestration/opencode-adapter.js +0 -580
- package/dist/infrastructure/orchestration/progress-aggregator.js +0 -906
- package/dist/infrastructure/orchestration/readiness.js +0 -103
- package/dist/infrastructure/orchestration/session-manager.js +0 -1590
- package/dist/infrastructure/orchestration/session-persistence.js +0 -394
- package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -38
- package/dist/infrastructure/protocols/cloud-sync.js +0 -207
- package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
- package/dist/infrastructure/protocols/mcp-server-impl.js +0 -3935
- package/dist/infrastructure/protocols/mcp-tools.js +0 -401
- package/dist/infrastructure/protocols/probe-port.js +0 -36
- package/dist/infrastructure/protocols/thread-phase.js +0 -105
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +0 -121
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +0 -44
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +0 -56
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +0 -588
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +0 -168
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +0 -67
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +0 -115
- package/dist/infrastructure/protocols/ui/app-script-failover.js +0 -133
- package/dist/infrastructure/protocols/ui/app-script-github.js +0 -113
- package/dist/infrastructure/protocols/ui/app-script-observer.js +0 -104
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +0 -80
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +0 -203
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +0 -136
- package/dist/infrastructure/protocols/ui/app-script-render.js +0 -166
- package/dist/infrastructure/protocols/ui/app-script-share.js +0 -78
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +0 -194
- package/dist/infrastructure/protocols/ui/app-script-tour.js +0 -78
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +0 -312
- package/dist/infrastructure/protocols/ui/app-script.js +0 -2998
- package/dist/infrastructure/protocols/ui/font-faces.js +0 -7
- package/dist/infrastructure/protocols/ui/styles.js +0 -2147
- package/dist/infrastructure/protocols/ui/tokens.js +0 -96
- package/dist/infrastructure/protocols/ui-page-v2.js +0 -320
- package/dist/infrastructure/protocols/verify-page.js +0 -1677
- package/dist/infrastructure/security/file-collection.js +0 -118
- package/dist/infrastructure/security/fix-code.js +0 -144
- package/dist/infrastructure/security/fix-secret.js +0 -89
- package/dist/infrastructure/security/permissions-config.js +0 -235
- package/dist/infrastructure/security/scan-types.js +0 -10
- package/dist/infrastructure/security/ship-scan.js +0 -195
- package/dist/infrastructure/security/trust-scorer.js +0 -724
- package/dist/infrastructure/security/vibe-checks.js +0 -99
- package/dist/infrastructure/storage/atomic-write.js +0 -86
- package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
- package/dist/infrastructure/storage/project-thread-migration.js +0 -237
- package/dist/infrastructure/storage/project-thread-mirror.js +0 -671
- package/dist/infrastructure/storage/project-thread-reader.js +0 -409
- package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
- package/dist/monitoring.js +0 -212
- package/dist/presentation/cli/commands/audit-command.js +0 -201
- package/dist/presentation/cli/commands/budget-command.js +0 -79
- package/dist/presentation/cli/commands/config-command.js +0 -125
- package/dist/presentation/cli/commands/context-source-command.js +0 -217
- package/dist/presentation/cli/commands/hook-command.js +0 -265
- package/dist/presentation/cli/commands/init-command.js +0 -590
- package/dist/presentation/cli/commands/memory-command.js +0 -1031
- package/dist/presentation/cli/commands/narrator-command.js +0 -101
- package/dist/presentation/cli/commands/permission-command.js +0 -105
- package/dist/presentation/cli/commands/protocols-builder.js +0 -254
- package/dist/presentation/cli/commands/protocols-command.js +0 -84
- package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
- package/dist/presentation/cli/commands/report-command.js +0 -403
- package/dist/presentation/cli/commands/scan-command.js +0 -52
- package/dist/presentation/cli/commands/scan-tool-command.js +0 -110
- package/dist/presentation/cli/commands/search-command.js +0 -51
- package/dist/presentation/cli/commands/setup-command.js +0 -330
- package/dist/presentation/cli/commands/status-command.js +0 -614
- package/dist/presentation/cli/commands/team-command.js +0 -256
- package/dist/presentation/cli/commands/thread-command.js +0 -370
- package/dist/presentation/cli/commands/trust-command.js +0 -328
- package/dist/presentation/cli/commands/verify-command.js +0 -298
- package/dist/presentation/cli/error-display.js +0 -328
- package/dist/presentation/cli/repl.js +0 -805
- package/dist/presentation/cli/task-deliverable.js +0 -153
- package/dist/testing/cassette.js +0 -125
- package/dist/testing/replay-adapter.js +0 -147
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI Command: trust
|
|
3
|
-
*
|
|
4
|
-
* Layer 0 trust scorer — instant static analysis, zero API calls.
|
|
5
|
-
* Runs on a file, directory, or piped git diff.
|
|
6
|
-
*
|
|
7
|
-
* Produces a trust score 0–100 with plain-English explanation:
|
|
8
|
-
* 100 = no findings
|
|
9
|
-
* 80+ = minor issues, safe to merge
|
|
10
|
-
* 60-79 = review recommended
|
|
11
|
-
* 40-59 = high-risk, human review required
|
|
12
|
-
* < 40 = block until resolved
|
|
13
|
-
*
|
|
14
|
-
* Usage:
|
|
15
|
-
* codehere trust src/utils.ts # score a file
|
|
16
|
-
* codehere trust . # score all files in directory
|
|
17
|
-
* git diff HEAD | codehere trust # score a diff
|
|
18
|
-
* codehere trust --diff HEAD # equivalent using git directly
|
|
19
|
-
* codehere trust --badge # output badge markdown
|
|
20
|
-
*/
|
|
21
|
-
import { existsSync } from 'fs';
|
|
22
|
-
import { join, relative, isAbsolute } from 'path';
|
|
23
|
-
import { execFileSync } from 'child_process';
|
|
24
|
-
import { scoreFile, aggregateTrust } from '../../../infrastructure/security/trust-scorer.js';
|
|
25
|
-
import { collectCodeFiles } from '../../../infrastructure/security/file-collection.js';
|
|
26
|
-
import { colors, sectionHeader, error, newline, info, randomCleanScan } from '../../../ui.js';
|
|
27
|
-
import { loadProductContext, adjustRecommendation } from '../../../infrastructure/config/product-context.js';
|
|
28
|
-
// ── File collection ──────────────────────────────────────────────────────────
|
|
29
|
-
// See src/infrastructure/security/file-collection.ts for exclusion rules.
|
|
30
|
-
/**
|
|
31
|
-
* Read a file safely — handles iCloud-evicted files on macOS.
|
|
32
|
-
* readFileSync blocks forever on iCloud-evicted files (dehydrated placeholders in ~/Documents).
|
|
33
|
-
* We use cat with a timeout as a subprocess so the main thread never blocks.
|
|
34
|
-
*/
|
|
35
|
-
function safeReadFile(filepath) {
|
|
36
|
-
try {
|
|
37
|
-
return execFileSync('cat', [filepath], { encoding: 'utf-8', timeout: 2000, maxBuffer: 5 * 1024 * 1024 });
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return null; // File unreadable (iCloud-evicted, permissions, too large) — skip silently
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
// ── Git diff mode ────────────────────────────────────────────────────────────
|
|
44
|
-
function parseDiffFiles(diff) {
|
|
45
|
-
const files = [];
|
|
46
|
-
const fileBlocks = diff.split(/^diff --git /m).filter(Boolean);
|
|
47
|
-
for (const block of fileBlocks) {
|
|
48
|
-
const filenameMatch = block.match(/^a\/(.+?) b\/(.+?)$/m);
|
|
49
|
-
if (!filenameMatch)
|
|
50
|
-
continue;
|
|
51
|
-
const filepath = filenameMatch[2];
|
|
52
|
-
// Skip internal backup/cache directories to avoid false findings from old code
|
|
53
|
-
if (filepath.startsWith('.codehere/') || filepath.startsWith('.codehere-cache/'))
|
|
54
|
-
continue;
|
|
55
|
-
// Extract added lines only (lines starting with +, excluding +++)
|
|
56
|
-
const addedLines = block
|
|
57
|
-
.split('\n')
|
|
58
|
-
.filter((l) => l.startsWith('+') && !l.startsWith('+++'))
|
|
59
|
-
.map((l) => l.substring(1))
|
|
60
|
-
.join('\n');
|
|
61
|
-
if (addedLines.trim()) {
|
|
62
|
-
files.push({ filepath, code: addedLines });
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return files;
|
|
66
|
-
}
|
|
67
|
-
// ── Report formatting ────────────────────────────────────────────────────────
|
|
68
|
-
function gradeColor(grade) {
|
|
69
|
-
switch (grade) {
|
|
70
|
-
case 'A':
|
|
71
|
-
return colors.green(grade);
|
|
72
|
-
case 'B':
|
|
73
|
-
return colors.cyan(grade);
|
|
74
|
-
case 'C':
|
|
75
|
-
return colors.yellow(grade);
|
|
76
|
-
case 'D':
|
|
77
|
-
return colors.red(grade);
|
|
78
|
-
case 'F':
|
|
79
|
-
return colors.red(colors.bold(grade));
|
|
80
|
-
default:
|
|
81
|
-
return grade;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
function scoreBar(score, width = 20) {
|
|
85
|
-
const filled = Math.round((score / 100) * width);
|
|
86
|
-
const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
87
|
-
if (score >= 75)
|
|
88
|
-
return colors.green(bar);
|
|
89
|
-
if (score >= 60)
|
|
90
|
-
return colors.yellow(bar);
|
|
91
|
-
return colors.red(bar);
|
|
92
|
-
}
|
|
93
|
-
function badgeMarkdown(report) {
|
|
94
|
-
const color = report.grade === 'A'
|
|
95
|
-
? 'brightgreen'
|
|
96
|
-
: report.grade === 'B'
|
|
97
|
-
? 'green'
|
|
98
|
-
: report.grade === 'C'
|
|
99
|
-
? 'yellow'
|
|
100
|
-
: report.grade === 'D'
|
|
101
|
-
? 'orange'
|
|
102
|
-
: 'red';
|
|
103
|
-
return ``;
|
|
104
|
-
}
|
|
105
|
-
// ── Main command ─────────────────────────────────────────────────────────────
|
|
106
|
-
export async function executeTrustCommand(target, options = {}) {
|
|
107
|
-
const cwd = process.cwd();
|
|
108
|
-
let filesToScan = [];
|
|
109
|
-
// ── Input source ──────────────────────────────────────────────────────────
|
|
110
|
-
if (options.diff !== undefined) {
|
|
111
|
-
// Run git diff and scan added lines
|
|
112
|
-
try {
|
|
113
|
-
// THREAT-2 fix · ref comes from a user-supplied CLI flag. Using template-
|
|
114
|
-
// string interpolation with execSync allows shell metacharacters in `ref`
|
|
115
|
-
// (e.g. `--diff "main; rm -rf /"`) to escape the git invocation. Use the
|
|
116
|
-
// execFileSync array form so the shell is never invoked.
|
|
117
|
-
const ref = options.diff || 'HEAD';
|
|
118
|
-
const diffOutput = execFileSync('git', ['diff', ref], { cwd, encoding: 'utf-8' });
|
|
119
|
-
filesToScan = parseDiffFiles(diffOutput);
|
|
120
|
-
if (filesToScan.length === 0) {
|
|
121
|
-
console.log(info('No changed files to scan'));
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
catch (e) {
|
|
126
|
-
console.error(error('git diff failed: ' + String(e)));
|
|
127
|
-
process.exit(1);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
else if (!process.stdin.isTTY && !target) {
|
|
131
|
-
// Piped input (e.g., git diff HEAD | codehere trust)
|
|
132
|
-
let diffInput = '';
|
|
133
|
-
for await (const chunk of process.stdin) {
|
|
134
|
-
diffInput += chunk;
|
|
135
|
-
}
|
|
136
|
-
if (diffInput.trim().startsWith('diff --git')) {
|
|
137
|
-
filesToScan = parseDiffFiles(diffInput);
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
// Treat as raw code
|
|
141
|
-
filesToScan = [{ filepath: 'stdin', code: diffInput }];
|
|
142
|
-
}
|
|
143
|
-
if (filesToScan.length === 0) {
|
|
144
|
-
console.log(info('No content to scan'));
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
else {
|
|
149
|
-
// File or directory
|
|
150
|
-
const resolved = target ? (isAbsolute(target) ? target : join(cwd, target)) : cwd;
|
|
151
|
-
if (!existsSync(resolved)) {
|
|
152
|
-
console.error(error(`Not found: ${resolved}`));
|
|
153
|
-
process.exit(1);
|
|
154
|
-
}
|
|
155
|
-
const paths = collectCodeFiles(resolved);
|
|
156
|
-
let skippedCount = 0;
|
|
157
|
-
for (const p of paths) {
|
|
158
|
-
const code = safeReadFile(p);
|
|
159
|
-
if (code !== null) {
|
|
160
|
-
filesToScan.push({ filepath: relative(cwd, p), code });
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
skippedCount++;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
if (skippedCount > 0) {
|
|
167
|
-
console.log(colors.dim(` Skipped ${skippedCount} unreadable file${skippedCount > 1 ? 's' : ''}`));
|
|
168
|
-
}
|
|
169
|
-
if (filesToScan.length === 0 && skippedCount > 0) {
|
|
170
|
-
console.log(info('No readable files to scan.'));
|
|
171
|
-
console.log(colors.dim(' Check file permissions or availability and retry.'));
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
// ── Scan ──────────────────────────────────────────────────────────────────
|
|
176
|
-
const results = filesToScan.map((f) => scoreFile(f.filepath, f.code));
|
|
177
|
-
// S8 · score + grade come from the ONE shared aggregation (trust-scorer's
|
|
178
|
-
// aggregateTrust) — `trust`, `--badge`, and `report` can no longer drift.
|
|
179
|
-
const { overallScore, grade, criticalCount, highCount, mediumCount, lowCount, totalFindings } = aggregateTrust(results);
|
|
180
|
-
// Nothing scanned is not a clean scan.
|
|
181
|
-
//
|
|
182
|
-
// aggregateTrust over zero files returns 100/A, and the recommendation below then read
|
|
183
|
-
// "Clean scan. Nothing blocking a merge." — an explicit merge recommendation earned by
|
|
184
|
-
// looking at nothing. `trust --ci` advertises itself as a gate ("Exit 1 on critical
|
|
185
|
-
// findings"), so a CI job pointed at the wrong path, or one whose diff resolution returned
|
|
186
|
-
// no files, went green and told the reader the code was fine. Same defect as `audit verify
|
|
187
|
-
// --json` reporting valid:true over an empty chain (f030769): a walk over nothing trivially
|
|
188
|
-
// succeeds, and success over nothing is not success.
|
|
189
|
-
const scanned = filesToScan.length > 0;
|
|
190
|
-
const baseRecommendation = !scanned
|
|
191
|
-
? 'No files scanned — nothing was graded. Point trust at a path with source in it, or check your diff range.'
|
|
192
|
-
: criticalCount > 0
|
|
193
|
-
? `${criticalCount} critical ${criticalCount === 1 ? 'issue' : 'issues'} · worth sorting before this merges.`
|
|
194
|
-
: highCount > 0
|
|
195
|
-
? `Nothing critical, but ${highCount} high-severity ${highCount === 1 ? 'pattern' : 'patterns'} worth a look.`
|
|
196
|
-
: mediumCount + lowCount > 0
|
|
197
|
-
? `${mediumCount + lowCount} low-priority ${mediumCount + lowCount === 1 ? 'finding' : 'findings'} · nothing blocking, but worth a pass.`
|
|
198
|
-
: 'Clean scan. Nothing blocking a merge.';
|
|
199
|
-
const productCtx = loadProductContext(process.cwd());
|
|
200
|
-
const recommendation = adjustRecommendation(baseRecommendation, criticalCount, highCount, overallScore, productCtx);
|
|
201
|
-
const report = {
|
|
202
|
-
files: results,
|
|
203
|
-
// `scanned` is the field a machine should branch on. Without it, files:[] plus grade:'A'
|
|
204
|
-
// reads as a pass to anything that only looks at the grade.
|
|
205
|
-
scanned,
|
|
206
|
-
overallScore,
|
|
207
|
-
grade,
|
|
208
|
-
totalFindings,
|
|
209
|
-
criticalCount,
|
|
210
|
-
highCount,
|
|
211
|
-
mediumCount,
|
|
212
|
-
lowCount,
|
|
213
|
-
recommendation,
|
|
214
|
-
scannedAt: new Date().toISOString(),
|
|
215
|
-
};
|
|
216
|
-
// Track CI failure — will throw after display so the CLI action handler catches it
|
|
217
|
-
const ciFailed = options.ci && (!scanned || overallScore < 60 || criticalCount > 0);
|
|
218
|
-
// ── Output ────────────────────────────────────────────────────────────────
|
|
219
|
-
if (options.json) {
|
|
220
|
-
console.log(JSON.stringify(report, null, 2));
|
|
221
|
-
if (options.ci && (!scanned || overallScore < 60 || criticalCount > 0))
|
|
222
|
-
process.exit(1);
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
if (options.badge) {
|
|
226
|
-
console.log(badgeMarkdown(report));
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
// Human-readable report
|
|
230
|
-
newline();
|
|
231
|
-
sectionHeader('codehere Trust Report');
|
|
232
|
-
newline();
|
|
233
|
-
// Score — but only when something was scored.
|
|
234
|
-
//
|
|
235
|
-
// A full green bar reading 100/100 A over zero files is the headline lie, and it survived
|
|
236
|
-
// fixing the JSON and the exit code: those matter to a pipeline, this is what the human
|
|
237
|
-
// reads. DESIGN.md §1 reserves green for a measurement, and an aggregation over nothing is
|
|
238
|
-
// not one. No score, no grade, no green tick — just what happened.
|
|
239
|
-
if (!scanned) {
|
|
240
|
-
console.log(` ${colors.dim('—')} no files scanned, so there is no score`);
|
|
241
|
-
newline();
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
console.log(` Score ${scoreBar(overallScore)} ${colors.bold(String(overallScore))}/100 ${gradeColor(grade)}`);
|
|
245
|
-
newline();
|
|
246
|
-
// Findings summary (inline, compact)
|
|
247
|
-
const parts = [
|
|
248
|
-
criticalCount > 0 ? colors.red(`${criticalCount} critical`) : null,
|
|
249
|
-
highCount > 0 ? colors.yellow(`${highCount} high`) : null,
|
|
250
|
-
mediumCount > 0 ? colors.dim(`${mediumCount} medium`) : null,
|
|
251
|
-
lowCount > 0 ? colors.dim(`${lowCount} low`) : null,
|
|
252
|
-
].filter(Boolean);
|
|
253
|
-
if (parts.length === 0) {
|
|
254
|
-
console.log(` ${colors.green('✓')} ${randomCleanScan()}`);
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
console.log(` ${parts.join(colors.dim(' · '))}`);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
newline();
|
|
261
|
-
// Per-file results (show worst first)
|
|
262
|
-
if (results.length > 1) {
|
|
263
|
-
const sorted = [...results].sort((a, b) => a.score - b.score);
|
|
264
|
-
console.log(colors.dim(' FILES'));
|
|
265
|
-
for (const r of sorted.slice(0, 10)) {
|
|
266
|
-
const icon = r.score >= 75 ? colors.green('✓') : r.score >= 60 ? colors.yellow('!') : colors.red('✕');
|
|
267
|
-
console.log(` ${icon} ${gradeColor(r.grade)} ${String(r.score).padStart(3)}/100 ${r.filepath}`);
|
|
268
|
-
}
|
|
269
|
-
if (sorted.length > 10) {
|
|
270
|
-
console.log(colors.dim(` + ${sorted.length - 10} more files`));
|
|
271
|
-
}
|
|
272
|
-
newline();
|
|
273
|
-
}
|
|
274
|
-
// Findings detail
|
|
275
|
-
if (options.verbose) {
|
|
276
|
-
console.log(colors.dim(' FINDINGS'));
|
|
277
|
-
for (const r of results) {
|
|
278
|
-
if (r.findings.length === 0)
|
|
279
|
-
continue;
|
|
280
|
-
console.log(` ${colors.bold(r.filepath)}`);
|
|
281
|
-
for (const f of r.findings) {
|
|
282
|
-
const sev = f.severity === 'critical'
|
|
283
|
-
? colors.red('crit')
|
|
284
|
-
: f.severity === 'high'
|
|
285
|
-
? colors.yellow('high')
|
|
286
|
-
: colors.dim(f.severity.slice(0, 4));
|
|
287
|
-
const ln = f.line ? colors.dim(`L${f.line}`) : '';
|
|
288
|
-
console.log(` ${sev} ${ln.padEnd(6)} ${f.description}`);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
newline();
|
|
292
|
-
}
|
|
293
|
-
else if (totalFindings > 0) {
|
|
294
|
-
console.log(colors.dim(' FINDINGS'));
|
|
295
|
-
const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
296
|
-
const allFindings = results.flatMap((r) => r.findings.map((f) => ({ ...f, filepath: r.filepath })));
|
|
297
|
-
const sorted = allFindings.sort((a, b) => severityRank[a.severity] - severityRank[b.severity]);
|
|
298
|
-
for (const f of sorted.slice(0, 5)) {
|
|
299
|
-
const sev = f.severity === 'critical'
|
|
300
|
-
? colors.red('crit')
|
|
301
|
-
: f.severity === 'high'
|
|
302
|
-
? colors.yellow('high')
|
|
303
|
-
: colors.dim(f.severity.slice(0, 4));
|
|
304
|
-
const ln = f.line ? `:${f.line}` : '';
|
|
305
|
-
console.log(` ${sev} ${f.filepath}${ln}`);
|
|
306
|
-
console.log(` ${colors.dim(f.description)}`);
|
|
307
|
-
}
|
|
308
|
-
if (sorted.length > 5) {
|
|
309
|
-
console.log(colors.dim(` + ${sorted.length - 5} more findings (use --verbose to see all)`));
|
|
310
|
-
}
|
|
311
|
-
newline();
|
|
312
|
-
}
|
|
313
|
-
// Recommendation
|
|
314
|
-
const recColor = criticalCount > 0 ? colors.red : highCount > 0 ? colors.yellow : colors.green;
|
|
315
|
-
console.log(recColor(recommendation));
|
|
316
|
-
newline();
|
|
317
|
-
// Tips footer (quiet, separated)
|
|
318
|
-
if (results.length > 0 && !options.ci) {
|
|
319
|
-
console.log(colors.dim(' ─────'));
|
|
320
|
-
console.log(colors.dim(' trust --badge Add a badge to your README'));
|
|
321
|
-
console.log(colors.dim(' trust --ci Exit 1 on critical findings'));
|
|
322
|
-
newline();
|
|
323
|
-
}
|
|
324
|
-
if (ciFailed) {
|
|
325
|
-
throw new Error(`CI_FAIL:${criticalCount > 0 ? criticalCount + ' critical finding(s)' : 'score ' + overallScore + ' below threshold 60'}`);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
//# sourceMappingURL=trust-command.js.map
|
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI command: verify
|
|
3
|
-
*
|
|
4
|
-
* v0.7 verifier-agent · Slice 2 (CLI surface for the pure verify-claim use case).
|
|
5
|
-
*
|
|
6
|
-
* Compares what the agent's Final Report TEXT claims against the structured
|
|
7
|
-
* reality (artifacts + steps) captured during the session. Catches the
|
|
8
|
-
* easy lies before they reach a PR description.
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* codehere verify <session-id> # run verifier on a completed session
|
|
12
|
-
* codehere verify <session-id> --json # machine-readable output for CI
|
|
13
|
-
* codehere verify # list recent sessions to pick from
|
|
14
|
-
*
|
|
15
|
-
* Exit codes:
|
|
16
|
-
* 0 = verified OR partial (warnings only · unverifiable claims)
|
|
17
|
-
* 0 = no-report (nothing to verify; not a failure)
|
|
18
|
-
* 1 = broken (at least one claim contradicts recorded reality)
|
|
19
|
-
* 2 = session not found / invalid input
|
|
20
|
-
*
|
|
21
|
-
* The verifier is intentionally conservative: only claims that map to
|
|
22
|
-
* structured data are checked. Prose claims pass through silently.
|
|
23
|
-
*/
|
|
24
|
-
import { colors, symbols } from '../../../ui.js';
|
|
25
|
-
import { loadSessionById, loadRecent, } from '../../../infrastructure/orchestration/session-persistence.js';
|
|
26
|
-
import { assessSession } from '../../../application/services/verdict-service.js';
|
|
27
|
-
import { describeCompleted } from '../../../domain/task.js';
|
|
28
|
-
import { buildCrossVendorVerdict } from '../../../application/use-cases/build-cross-vendor-verdict.js';
|
|
29
|
-
export async function executeVerifyCommand(sessionId, options = {}) {
|
|
30
|
-
// No session id: show a picker list and exit gracefully.
|
|
31
|
-
if (!sessionId) {
|
|
32
|
-
return showSessionPicker(options.json ?? false);
|
|
33
|
-
}
|
|
34
|
-
const session = loadSessionById(sessionId);
|
|
35
|
-
if (!session) {
|
|
36
|
-
if (options.json) {
|
|
37
|
-
process.stdout.write(JSON.stringify({
|
|
38
|
-
error: 'session-not-found',
|
|
39
|
-
sessionId,
|
|
40
|
-
hint: 'Run `codehere verify` (no args) to list recent sessions.',
|
|
41
|
-
}, null, 2) + '\n');
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
process.stderr.write(colors.error(`${symbols.error} No completed session found matching "${sessionId}".\n`) +
|
|
45
|
-
colors.dim(' Tip: run `codehere verify` (no args) to see recent sessions.\n'));
|
|
46
|
-
}
|
|
47
|
-
return 2;
|
|
48
|
-
}
|
|
49
|
-
const result = assessSession(session);
|
|
50
|
-
const crossVendor = buildCrossVendorVerdict(session);
|
|
51
|
-
if (options.json) {
|
|
52
|
-
process.stdout.write(JSON.stringify({ ...serializeResult(session, result), crossVendor: crossVendor ?? null }, null, 2) + '\n');
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
printHumanReadable(session, result, crossVendor);
|
|
56
|
-
}
|
|
57
|
-
// Exit code: only `broken` is a hard failure (CI-friendly).
|
|
58
|
-
return result.status === 'broken' ? 1 : 0;
|
|
59
|
-
}
|
|
60
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
61
|
-
/* Picker mode · no session id given */
|
|
62
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
63
|
-
function showSessionPicker(json) {
|
|
64
|
-
const recent = loadRecent(15);
|
|
65
|
-
if (recent.length === 0) {
|
|
66
|
-
if (json) {
|
|
67
|
-
process.stdout.write(JSON.stringify({ sessions: [], note: 'No completed sessions found.' }, null, 2) + '\n');
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
process.stdout.write(colors.dim('No completed sessions found. Run a task first, then verify it:\n') +
|
|
71
|
-
colors.dim(' codehere task "describe what you want done"\n'));
|
|
72
|
-
}
|
|
73
|
-
return 0;
|
|
74
|
-
}
|
|
75
|
-
if (json) {
|
|
76
|
-
const items = recent.map((s) => ({
|
|
77
|
-
sessionId: s.sessionId,
|
|
78
|
-
agent: s.agent,
|
|
79
|
-
prompt: s.prompt,
|
|
80
|
-
completedAt: s.completedAt,
|
|
81
|
-
hasFinalReport: Boolean(s.finalReport),
|
|
82
|
-
}));
|
|
83
|
-
process.stdout.write(JSON.stringify({ sessions: items }, null, 2) + '\n');
|
|
84
|
-
return 0;
|
|
85
|
-
}
|
|
86
|
-
process.stdout.write('\n' + colors.bold('Recent sessions') + colors.dim(' (pass --json for machine output)\n\n'));
|
|
87
|
-
for (const s of recent) {
|
|
88
|
-
const shortId = s.sessionId.slice(0, 8);
|
|
89
|
-
const when = formatRelativeTime(s.completedAt);
|
|
90
|
-
const reportTag = s.finalReport ? colors.gray('· report ✓') : colors.gray('· no report');
|
|
91
|
-
const promptLine = truncate(s.prompt.replace(/\s+/g, ' ').trim(), 70);
|
|
92
|
-
process.stdout.write(` ${colors.cyan(shortId)} ${colors.gray(s.agent.padEnd(11))}${colors.dim(when.padEnd(14))} ${reportTag}\n` +
|
|
93
|
-
` ${colors.dim(promptLine)}\n\n`);
|
|
94
|
-
}
|
|
95
|
-
process.stdout.write(colors.dim('To verify one of these: ') + colors.bold('codehere verify <id>') + '\n\n');
|
|
96
|
-
return 0;
|
|
97
|
-
}
|
|
98
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
99
|
-
/* Cross-vendor review rendering */
|
|
100
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
101
|
-
function printCrossVendor(cv) {
|
|
102
|
-
process.stdout.write(colors.bold('Cross-vendor review\n'));
|
|
103
|
-
const lead = cv.proven ? colors.success(`${symbols.success} `) : ' ';
|
|
104
|
-
process.stdout.write(`${lead}${cv.summary}\n`);
|
|
105
|
-
for (const c of cv.caveats)
|
|
106
|
-
process.stdout.write(colors.dim(` · ${c}\n`));
|
|
107
|
-
if (cv.reviewText) {
|
|
108
|
-
process.stdout.write('\n' + colors.dim(`${cv.reviewer.agent}'s findings (verbatim · codehere does not judge them):\n`));
|
|
109
|
-
process.stdout.write(colors.gray(truncate(cv.reviewText.replace(/\s+/g, ' ').trim(), 400)) + '\n');
|
|
110
|
-
}
|
|
111
|
-
process.stdout.write('\n');
|
|
112
|
-
}
|
|
113
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
114
|
-
/* Human-readable output */
|
|
115
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
116
|
-
function printHumanReadable(session, result, crossVendor) {
|
|
117
|
-
const shortId = session.sessionId.slice(0, 8);
|
|
118
|
-
const when = formatRelativeTime(session.completedAt);
|
|
119
|
-
// Header
|
|
120
|
-
process.stdout.write('\n' +
|
|
121
|
-
colors.dim('session ') +
|
|
122
|
-
colors.cyan(shortId) +
|
|
123
|
-
colors.dim(' · ') +
|
|
124
|
-
colors.gray(session.agent) +
|
|
125
|
-
colors.dim(' · ') +
|
|
126
|
-
colors.gray(when) +
|
|
127
|
-
'\n' +
|
|
128
|
-
colors.dim('task: ') +
|
|
129
|
-
truncate(session.prompt.replace(/\s+/g, ' ').trim(), 80) +
|
|
130
|
-
'\n\n');
|
|
131
|
-
// What the agent did — lead with the deliverable, not the audit. A
|
|
132
|
-
// non-technical user opening this wants "what happened" first, then "does
|
|
133
|
-
// it check out". Both come straight from recorded state (no screen lies).
|
|
134
|
-
if (session.finalReport?.summary) {
|
|
135
|
-
process.stdout.write(session.finalReport.summary.trim() + '\n\n');
|
|
136
|
-
}
|
|
137
|
-
// Files changed + trust, or the honest can't-track note.
|
|
138
|
-
const files = session.artifacts?.files ?? [];
|
|
139
|
-
if (session.artifacts?.noGitTracking) {
|
|
140
|
-
process.stdout.write(colors.dim("Files: codehere couldn't track changes here (this folder isn't a git repository).\n\n"));
|
|
141
|
-
}
|
|
142
|
-
else if (files.length > 0) {
|
|
143
|
-
process.stdout.write(colors.bold('Files changed:\n'));
|
|
144
|
-
for (const f of files) {
|
|
145
|
-
const trust = typeof f.trust === 'number' ? colors.dim(` · trust ${f.trust}/100`) : '';
|
|
146
|
-
process.stdout.write(` • ${f.path} ${colors.success('+' + f.added)}/${colors.error('-' + f.removed)}${trust}\n`);
|
|
147
|
-
}
|
|
148
|
-
process.stdout.write('\n');
|
|
149
|
-
}
|
|
150
|
-
// Verdict line · color-coded by status
|
|
151
|
-
const verdict = verdictLine(result);
|
|
152
|
-
process.stdout.write(colors.bold('Fact-check: ') + verdict + '\n\n');
|
|
153
|
-
// Cross-vendor review — when a DIFFERENT vendor reviewed an earlier writer in
|
|
154
|
-
// this room, this is the receipt no single-vendor tool can produce.
|
|
155
|
-
if (crossVendor)
|
|
156
|
-
printCrossVendor(crossVendor);
|
|
157
|
-
// Findings detail
|
|
158
|
-
if (result.findings.length > 0 && result.status !== 'no-report') {
|
|
159
|
-
process.stdout.write(colors.bold('Findings:\n\n'));
|
|
160
|
-
for (let i = 0; i < result.findings.length; i++) {
|
|
161
|
-
const f = result.findings[i];
|
|
162
|
-
const num = colors.dim(` ${(i + 1).toString().padStart(2)}. `);
|
|
163
|
-
const sev = severityTag(f.severity);
|
|
164
|
-
const kind = colors.gray(`[${f.kind}]`);
|
|
165
|
-
process.stdout.write(`${num}${sev} ${kind} ${f.message}\n`);
|
|
166
|
-
process.stdout.write(` ${colors.dim('claim:')} ${colors.gray(f.claim)}\n`);
|
|
167
|
-
process.stdout.write(` ${colors.dim('reality:')} ${colors.gray(f.reality)}\n\n`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
// What was checked (so users know the verifier's scope)
|
|
171
|
-
process.stdout.write(colors.dim('What was checked:\n'));
|
|
172
|
-
process.stdout.write(colors.dim(' · numeric test claims (X tests passed, X of Y, etc) vs artifacts.tests\n'));
|
|
173
|
-
process.stdout.write(colors.dim(' · universal claims (all tests pass, no failures) vs artifacts.tests.failed\n'));
|
|
174
|
-
process.stdout.write(colors.dim(' · file claims (X files changed, no files changed) vs artifacts.files\n'));
|
|
175
|
-
process.stdout.write(colors.dim('Not checked: prose claims, intent, semantic correctness of changes.\n\n'));
|
|
176
|
-
// Tier 1.1/0.2 · be explicit about which kind of test ✓ this is. A re-run is
|
|
177
|
-
// ground truth (exit-code observed); a scrape is the agent's own claim. The
|
|
178
|
-
// skeptic deserves to know which one they're looking at.
|
|
179
|
-
const tests = session.artifacts?.tests;
|
|
180
|
-
if (tests) {
|
|
181
|
-
if (tests.observed) {
|
|
182
|
-
process.stdout.write(colors.green(`${symbols.success} tests RE-RUN by codehere`) +
|
|
183
|
-
colors.dim(` (${tests.command || 'test suite'}, exit ${tests.exitCode}) · observed, not just reported\n\n`));
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
process.stdout.write(colors.dim('Note: test counts above are AS REPORTED by the agent · codehere did not re-run the suite, so the exit code is not independently confirmed.\n\n'));
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
// R13/M3 · the typed completeness invariant — every verdict input is either
|
|
190
|
-
// present or carries the explicit honest reason it isn't. This block is what
|
|
191
|
-
// makes "the verdict said X" auditable: the reader sees exactly what fed it
|
|
192
|
-
// and what couldn't (e.g. "couldn't observe — no test runner detected"),
|
|
193
|
-
// never a silent blank.
|
|
194
|
-
const completeness = describeCompleted({
|
|
195
|
-
sessionId: session.sessionId,
|
|
196
|
-
agent: session.agent,
|
|
197
|
-
status: session.status,
|
|
198
|
-
completedAt: session.completedAt,
|
|
199
|
-
durationMs: session.durationMs,
|
|
200
|
-
steps: session.steps,
|
|
201
|
-
artifacts: session.artifacts,
|
|
202
|
-
finalReport: session.finalReport,
|
|
203
|
-
});
|
|
204
|
-
process.stdout.write(colors.bold('What fed this verdict\n'));
|
|
205
|
-
const inputLine = (label, input) => 'present' in input
|
|
206
|
-
? ` ${colors.green(symbols.success)} ${label}\n`
|
|
207
|
-
: ` ${colors.dim(symbols.dot)} ${label} ${colors.dim('· ' + input.absent)}\n`;
|
|
208
|
-
process.stdout.write(inputLine('file changes', completeness.files));
|
|
209
|
-
// A trust surface must say WHICH kind of test ✓ this is: a codehere re-run
|
|
210
|
-
// (exit-code observed) or the agent's own scrape (a claim, not ground truth).
|
|
211
|
-
const testsLabel = 'present' in completeness.tests
|
|
212
|
-
? completeness.tests.present.observed
|
|
213
|
-
? 'test result (re-run by codehere)'
|
|
214
|
-
: 'test result (agent-reported, not re-run)'
|
|
215
|
-
: 'test result';
|
|
216
|
-
process.stdout.write(inputLine(testsLabel, completeness.tests));
|
|
217
|
-
process.stdout.write(inputLine('written report', completeness.report));
|
|
218
|
-
process.stdout.write(inputLine('audit reconciliation', completeness.audit));
|
|
219
|
-
process.stdout.write('\n');
|
|
220
|
-
}
|
|
221
|
-
function verdictLine(result) {
|
|
222
|
-
switch (result.status) {
|
|
223
|
-
case 'verified':
|
|
224
|
-
return colors.green(`${symbols.success} Verified`) + colors.dim(' ' + result.summary);
|
|
225
|
-
case 'partial':
|
|
226
|
-
return colors.warning(`${symbols.warning} Partial`) + colors.dim(' ' + result.summary);
|
|
227
|
-
case 'broken':
|
|
228
|
-
return colors.error(`${symbols.error} Broken`) + colors.dim(' ' + result.summary);
|
|
229
|
-
case 'no-report':
|
|
230
|
-
return colors.gray(`${symbols.dot} No report`) + colors.dim(' ' + result.summary);
|
|
231
|
-
case 'nothing-to-verify':
|
|
232
|
-
// Honest neutral, not a green pass — the report made no checkable claim.
|
|
233
|
-
return colors.gray(`${symbols.dot} Nothing to verify`) + colors.dim(' ' + result.summary);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
function severityTag(sev) {
|
|
237
|
-
switch (sev) {
|
|
238
|
-
case 'fail':
|
|
239
|
-
return colors.error('fail');
|
|
240
|
-
case 'warn':
|
|
241
|
-
return colors.warning('warn');
|
|
242
|
-
case 'info':
|
|
243
|
-
return colors.info('info');
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
247
|
-
/* JSON serialization */
|
|
248
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
249
|
-
function serializeResult(session, result) {
|
|
250
|
-
return {
|
|
251
|
-
sessionId: session.sessionId,
|
|
252
|
-
agent: session.agent,
|
|
253
|
-
prompt: session.prompt,
|
|
254
|
-
completedAt: session.completedAt,
|
|
255
|
-
durationMs: session.durationMs,
|
|
256
|
-
hasFinalReport: Boolean(session.finalReport),
|
|
257
|
-
verdict: {
|
|
258
|
-
status: result.status,
|
|
259
|
-
summary: result.summary,
|
|
260
|
-
findings: result.findings.map((f) => ({
|
|
261
|
-
severity: f.severity,
|
|
262
|
-
kind: f.kind,
|
|
263
|
-
claim: f.claim,
|
|
264
|
-
reality: f.reality,
|
|
265
|
-
message: f.message,
|
|
266
|
-
})),
|
|
267
|
-
},
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
271
|
-
/* Small utilities */
|
|
272
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
273
|
-
function formatRelativeTime(ts) {
|
|
274
|
-
if (!ts || ts <= 0)
|
|
275
|
-
return 'unknown';
|
|
276
|
-
const deltaMs = Date.now() - ts;
|
|
277
|
-
if (deltaMs < 0)
|
|
278
|
-
return 'just now';
|
|
279
|
-
const seconds = Math.floor(deltaMs / 1000);
|
|
280
|
-
if (seconds < 60)
|
|
281
|
-
return `${seconds}s ago`;
|
|
282
|
-
const minutes = Math.floor(seconds / 60);
|
|
283
|
-
if (minutes < 60)
|
|
284
|
-
return `${minutes}m ago`;
|
|
285
|
-
const hours = Math.floor(minutes / 60);
|
|
286
|
-
if (hours < 24)
|
|
287
|
-
return `${hours}h ago`;
|
|
288
|
-
const days = Math.floor(hours / 24);
|
|
289
|
-
if (days < 30)
|
|
290
|
-
return `${days}d ago`;
|
|
291
|
-
return new Date(ts).toISOString().split('T')[0];
|
|
292
|
-
}
|
|
293
|
-
function truncate(s, max) {
|
|
294
|
-
if (s.length <= max)
|
|
295
|
-
return s;
|
|
296
|
-
return s.slice(0, max - 1).trimEnd() + '…';
|
|
297
|
-
}
|
|
298
|
-
//# sourceMappingURL=verify-command.js.map
|