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.
Files changed (163) hide show
  1. package/README.md +36 -192
  2. package/dist/application/use-cases/check-claimed-tests.js +81 -0
  3. package/dist/application/use-cases/detect-test-weakening.js +237 -0
  4. package/dist/application/use-cases/rerun-tests.js +110 -7
  5. package/dist/index.js +38 -1212
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -323
  7. package/dist/presentation/cli/commands/check-command.js +178 -23
  8. package/dist/presentation/cli/commands/help-command.js +21 -285
  9. package/dist/ui.js +5 -236
  10. package/dist/utils/version.js +0 -6
  11. package/package.json +9 -38
  12. package/dist/application/services/verdict-service.js +0 -154
  13. package/dist/application/use-cases/assess-cross-vendor.js +0 -126
  14. package/dist/application/use-cases/build-cross-vendor-verdict.js +0 -118
  15. package/dist/application/use-cases/domain-trust.js +0 -110
  16. package/dist/application/use-cases/export-aat.js +0 -107
  17. package/dist/application/use-cases/generate-acceptance-criteria.js +0 -223
  18. package/dist/application/use-cases/generate-final-report.js +0 -259
  19. package/dist/application/use-cases/generate-narration.js +0 -130
  20. package/dist/application/use-cases/generate-thread-title.js +0 -181
  21. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  22. package/dist/application/use-cases/plan-auto-review.js +0 -57
  23. package/dist/application/use-cases/reconcile-audit.js +0 -100
  24. package/dist/application/use-cases/search-sessions.js +0 -70
  25. package/dist/application/use-cases/should-auto-review.js +0 -71
  26. package/dist/application/use-cases/thread-export.js +0 -245
  27. package/dist/application/use-cases/verify-claim.js +0 -456
  28. package/dist/application/use-cases/write-pass.js +0 -277
  29. package/dist/domain/audit.js +0 -11
  30. package/dist/domain/services/codebase-detector.js +0 -85
  31. package/dist/domain/task.js +0 -91
  32. package/dist/error-handler.js +0 -139
  33. package/dist/infrastructure/completion/bash-completion.js +0 -29
  34. package/dist/infrastructure/completion/fish-completion.js +0 -17
  35. package/dist/infrastructure/completion/zsh-completion.js +0 -25
  36. package/dist/infrastructure/config/config-manager.js +0 -174
  37. package/dist/infrastructure/config/product-context.js +0 -49
  38. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  39. package/dist/infrastructure/context/adapters/figma-source.js +0 -198
  40. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -160
  41. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  42. package/dist/infrastructure/context/context-resolver.js +0 -144
  43. package/dist/infrastructure/context/context-source.js +0 -114
  44. package/dist/infrastructure/context/github-api.js +0 -110
  45. package/dist/infrastructure/context/github-device-flow.js +0 -65
  46. package/dist/infrastructure/context/oauth-flow.js +0 -207
  47. package/dist/infrastructure/context/oauth-store.js +0 -122
  48. package/dist/infrastructure/context/registry-bootstrap.js +0 -95
  49. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  50. package/dist/infrastructure/cost/budget-config.js +0 -114
  51. package/dist/infrastructure/cost/cost-router.js +0 -97
  52. package/dist/infrastructure/cost/spend-aggregator.js +0 -222
  53. package/dist/infrastructure/cost/team-ledger.js +0 -355
  54. package/dist/infrastructure/discovery/discovery-log.js +0 -85
  55. package/dist/infrastructure/hooks/audit-tip-signature.js +0 -336
  56. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -1018
  57. package/dist/infrastructure/hooks/hook-mode.js +0 -87
  58. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -49
  59. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  60. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  61. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  62. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  63. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  64. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -68
  65. package/dist/infrastructure/orchestration/aider-adapter.js +0 -611
  66. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  67. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -592
  68. package/dist/infrastructure/orchestration/codex-adapter.js +0 -574
  69. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -416
  70. package/dist/infrastructure/orchestration/context-handoff.js +0 -162
  71. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  72. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  73. package/dist/infrastructure/orchestration/detect-ollama.js +0 -75
  74. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  75. package/dist/infrastructure/orchestration/failover-config.js +0 -67
  76. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  77. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -663
  78. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -689
  79. package/dist/infrastructure/orchestration/graphify-context.js +0 -209
  80. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  81. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -388
  82. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  83. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  84. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  85. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -580
  86. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -906
  87. package/dist/infrastructure/orchestration/readiness.js +0 -103
  88. package/dist/infrastructure/orchestration/session-manager.js +0 -1590
  89. package/dist/infrastructure/orchestration/session-persistence.js +0 -394
  90. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  91. package/dist/infrastructure/orchestration/write-pass-store.js +0 -38
  92. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  93. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  94. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -3935
  95. package/dist/infrastructure/protocols/mcp-tools.js +0 -401
  96. package/dist/infrastructure/protocols/probe-port.js +0 -36
  97. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  98. package/dist/infrastructure/protocols/ui/app-script-artifacts.js +0 -121
  99. package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +0 -44
  100. package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +0 -56
  101. package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +0 -588
  102. package/dist/infrastructure/protocols/ui/app-script-context-sources.js +0 -168
  103. package/dist/infrastructure/protocols/ui/app-script-conversation.js +0 -67
  104. package/dist/infrastructure/protocols/ui/app-script-drawer.js +0 -115
  105. package/dist/infrastructure/protocols/ui/app-script-failover.js +0 -133
  106. package/dist/infrastructure/protocols/ui/app-script-github.js +0 -113
  107. package/dist/infrastructure/protocols/ui/app-script-observer.js +0 -104
  108. package/dist/infrastructure/protocols/ui/app-script-onboarding.js +0 -80
  109. package/dist/infrastructure/protocols/ui/app-script-overlay.js +0 -203
  110. package/dist/infrastructure/protocols/ui/app-script-recovery.js +0 -136
  111. package/dist/infrastructure/protocols/ui/app-script-render.js +0 -166
  112. package/dist/infrastructure/protocols/ui/app-script-share.js +0 -78
  113. package/dist/infrastructure/protocols/ui/app-script-sidebar.js +0 -194
  114. package/dist/infrastructure/protocols/ui/app-script-tour.js +0 -78
  115. package/dist/infrastructure/protocols/ui/app-script-wpass.js +0 -312
  116. package/dist/infrastructure/protocols/ui/app-script.js +0 -2998
  117. package/dist/infrastructure/protocols/ui/font-faces.js +0 -7
  118. package/dist/infrastructure/protocols/ui/styles.js +0 -2147
  119. package/dist/infrastructure/protocols/ui/tokens.js +0 -96
  120. package/dist/infrastructure/protocols/ui-page-v2.js +0 -320
  121. package/dist/infrastructure/protocols/verify-page.js +0 -1677
  122. package/dist/infrastructure/security/file-collection.js +0 -118
  123. package/dist/infrastructure/security/fix-code.js +0 -144
  124. package/dist/infrastructure/security/fix-secret.js +0 -89
  125. package/dist/infrastructure/security/permissions-config.js +0 -235
  126. package/dist/infrastructure/security/scan-types.js +0 -10
  127. package/dist/infrastructure/security/ship-scan.js +0 -195
  128. package/dist/infrastructure/security/trust-scorer.js +0 -724
  129. package/dist/infrastructure/security/vibe-checks.js +0 -99
  130. package/dist/infrastructure/storage/atomic-write.js +0 -86
  131. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  132. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  133. package/dist/infrastructure/storage/project-thread-mirror.js +0 -671
  134. package/dist/infrastructure/storage/project-thread-reader.js +0 -409
  135. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  136. package/dist/monitoring.js +0 -212
  137. package/dist/presentation/cli/commands/audit-command.js +0 -201
  138. package/dist/presentation/cli/commands/budget-command.js +0 -79
  139. package/dist/presentation/cli/commands/config-command.js +0 -125
  140. package/dist/presentation/cli/commands/context-source-command.js +0 -217
  141. package/dist/presentation/cli/commands/hook-command.js +0 -265
  142. package/dist/presentation/cli/commands/init-command.js +0 -590
  143. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  144. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  145. package/dist/presentation/cli/commands/permission-command.js +0 -105
  146. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  147. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  148. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  149. package/dist/presentation/cli/commands/report-command.js +0 -403
  150. package/dist/presentation/cli/commands/scan-command.js +0 -52
  151. package/dist/presentation/cli/commands/scan-tool-command.js +0 -110
  152. package/dist/presentation/cli/commands/search-command.js +0 -51
  153. package/dist/presentation/cli/commands/setup-command.js +0 -330
  154. package/dist/presentation/cli/commands/status-command.js +0 -614
  155. package/dist/presentation/cli/commands/team-command.js +0 -256
  156. package/dist/presentation/cli/commands/thread-command.js +0 -370
  157. package/dist/presentation/cli/commands/trust-command.js +0 -328
  158. package/dist/presentation/cli/commands/verify-command.js +0 -298
  159. package/dist/presentation/cli/error-display.js +0 -328
  160. package/dist/presentation/cli/repl.js +0 -805
  161. package/dist/presentation/cli/task-deliverable.js +0 -153
  162. package/dist/testing/cassette.js +0 -125
  163. package/dist/testing/replay-adapter.js +0 -147
@@ -1,247 +0,0 @@
1
- /**
2
- * F3 Task 4 — repo_map.json builder (v0.5 TS-native MVP).
3
- *
4
- * v0.5 SCOPE:
5
- * - Walks `git ls-files` (respects .gitignore via git itself).
6
- * - For .ts/.tsx/.js/.jsx/.py files, regex-extracts top-level public-surface
7
- * symbols (export function/class/const/interface/type, def/class for Python).
8
- * - Reference count via `git grep -c` per symbol name.
9
- * - Writes JSON to .codehere/memory/episodic/repo_map.json.
10
- *
11
- * v0.6 PLAN:
12
- * - Replace the regex extractor with proper tree-sitter parsing
13
- * (Aider's repomap.py MIT port — Python helper + TS shell-out, OR
14
- * a TS-native tree-sitter library evaluation).
15
- * - Add PageRank-style symbol importance (currently raw reference count).
16
- * - Incremental rebuild via the per-file `hash` field already in the schema.
17
- *
18
- * Schema is forward-compatible — only the extractor implementation changes.
19
- *
20
- * Why regex MVP for v0.5:
21
- * - Zero new deps (tree-sitter would mean Python + 5 packages OR a 30+ MB
22
- * TS dep). codehere is bootstrap-zero-cost; every dep is friction.
23
- * - Closes the F3 episodic/repo_map.json layer enough for spawned agents
24
- * to read it as a starting context block.
25
- * - Honest about limitations — see schema._note in the output JSON.
26
- */
27
- import { spawnSync } from 'child_process';
28
- import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'fs';
29
- import { createHash } from 'crypto';
30
- import { join, dirname, extname } from 'path';
31
- const SUPPORTED_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.py']);
32
- /**
33
- * Per-language extractors. v0.5 regex-based — line-anchored to avoid
34
- * matching identifiers inside string literals. Returns 1-indexed line numbers.
35
- *
36
- * v0.6 will swap these for AST-based extractors. The interface stays the
37
- * same: (body) → RepoMapSymbol[].
38
- */
39
- function extractTsLikeSymbols(body) {
40
- const lines = body.split('\n');
41
- const symbols = [];
42
- // export function NAME( | export async function NAME(
43
- // export class NAME
44
- // export const NAME = | export let NAME = | export var NAME =
45
- // export interface NAME { | export type NAME =
46
- const PATTERNS = [
47
- [/^export\s+(?:async\s+)?function\s+(\w+)\s*[<(]/, 'function'],
48
- [/^export\s+(?:abstract\s+)?class\s+(\w+)/, 'class'],
49
- [/^export\s+(?:const|let|var)\s+(\w+)\s*[:=]/, 'const'],
50
- [/^export\s+interface\s+(\w+)/, 'interface'],
51
- [/^export\s+type\s+(\w+)\s*=/, 'type'],
52
- [/^export\s+enum\s+(\w+)/, 'enum'],
53
- ];
54
- for (let i = 0; i < lines.length; i++) {
55
- for (const [re, kind] of PATTERNS) {
56
- const m = lines[i].match(re);
57
- if (m) {
58
- symbols.push({ kind, name: m[1], line: i + 1 });
59
- break;
60
- }
61
- }
62
- }
63
- return symbols;
64
- }
65
- function extractPythonSymbols(body) {
66
- const lines = body.split('\n');
67
- const symbols = [];
68
- // Top-level (column 0) def NAME( | class NAME( | class NAME:
69
- const PATTERNS = [
70
- [/^def\s+(\w+)\s*\(/, 'def'],
71
- [/^class\s+(\w+)\s*[(:]/, 'class'],
72
- ];
73
- for (let i = 0; i < lines.length; i++) {
74
- for (const [re, kind] of PATTERNS) {
75
- const m = lines[i].match(re);
76
- if (m) {
77
- symbols.push({ kind, name: m[1], line: i + 1 });
78
- break;
79
- }
80
- }
81
- }
82
- return symbols;
83
- }
84
- function extractSymbols(filePath, body) {
85
- const ext = extname(filePath);
86
- if (ext === '.py')
87
- return extractPythonSymbols(body);
88
- if (SUPPORTED_EXTENSIONS.has(ext))
89
- return extractTsLikeSymbols(body);
90
- return [];
91
- }
92
- /**
93
- * Hash file content for incremental-rebuild support in v0.6.
94
- * Format: `sha256:<64-hex>` so the algorithm is self-describing.
95
- */
96
- function hashContent(body) {
97
- return 'sha256:' + createHash('sha256').update(body).digest('hex');
98
- }
99
- /**
100
- * `git ls-files` returns paths relative to the repo root. Filters out
101
- * non-supported extensions in one pass. If `git` isn't a repo, returns null
102
- * so the caller can refuse with a clear message.
103
- */
104
- function gitListFiles(repoRoot) {
105
- const r = spawnSync('git', ['ls-files', '-z'], { cwd: repoRoot, encoding: 'utf-8' });
106
- if (r.status !== 0)
107
- return null;
108
- return r.stdout
109
- .split('\0')
110
- .filter((p) => p.length > 0)
111
- .filter((p) => SUPPORTED_EXTENSIONS.has(extname(p)));
112
- }
113
- /**
114
- * Count references to a symbol name across the repo via git grep.
115
- * `-c` returns "<file>:<count>" per matching file; sum the counts.
116
- *
117
- * Uses word-boundary regex so `popular` doesn't match `unpopular`. The grep
118
- * finds DEFINITIONS too, so subtract the definition count to get pure refs.
119
- */
120
- function countReferences(repoRoot, symbolName) {
121
- // -F = fixed string, -w = word boundary. Combined with grep itself.
122
- const r = spawnSync('git', ['grep', '-c', '-w', '-F', '--', symbolName], { cwd: repoRoot, encoding: 'utf-8' });
123
- if (r.status !== 0 && r.status !== 1)
124
- return 0;
125
- let total = 0;
126
- for (const line of r.stdout.split('\n')) {
127
- const m = line.match(/:(\d+)$/);
128
- if (m)
129
- total += parseInt(m[1], 10);
130
- }
131
- // Subtract 1 for the definition line itself (best-effort — undercounts if
132
- // the symbol is defined in multiple files; v0.6 with AST will be accurate).
133
- return Math.max(0, total - 1);
134
- }
135
- /**
136
- * Build the repo_map for the repo at `repoRoot`. Returns the JSON object —
137
- * caller writes to disk. Pure-ish (still spawns git), no codehere-specific
138
- * paths inside.
139
- *
140
- * The `_stats` block in the returned RepoMap persists to disk so downstream
141
- * agents know whether the map is incomplete (e.g. >1MB files skipped during
142
- * walk).
143
- */
144
- export function buildRepoMap(repoRoot) {
145
- if (!existsSync(join(repoRoot, '.git'))) {
146
- return { error: 'not a git repo (run `git init` first)' };
147
- }
148
- const files = gitListFiles(repoRoot);
149
- if (files === null) {
150
- return { error: 'git ls-files failed' };
151
- }
152
- const fileMap = {};
153
- const allSymbols = [];
154
- let skippedLargeFiles = 0;
155
- for (const rel of files) {
156
- const abs = join(repoRoot, rel);
157
- // Skip giant files (>1MB) — surfaced in the result so users see the count.
158
- try {
159
- if (statSync(abs).size > 1_000_000) {
160
- skippedLargeFiles++;
161
- continue;
162
- }
163
- }
164
- catch {
165
- continue;
166
- }
167
- let body;
168
- try {
169
- body = readFileSync(abs, 'utf-8');
170
- }
171
- catch {
172
- // File deleted between ls-files and read — skip.
173
- continue;
174
- }
175
- const symbols = extractSymbols(rel, body);
176
- fileMap[rel] = {
177
- hash: hashContent(body),
178
- symbols,
179
- };
180
- for (const s of symbols) {
181
- allSymbols.push({ name: s.name, file: rel, line: s.line });
182
- }
183
- }
184
- // Rank by reference count.
185
- //
186
- // codex Round 1 fix: the previous version applied .slice(0, 200) BEFORE
187
- // grepping, which meant "first 200 discovered names" not "top 200
188
- // candidates." Symbols defined in files traversed late silently got
189
- // references=0 and dropped out of topSymbols, regardless of how heavily
190
- // they were referenced. Now we grep ALL unique names. O(N) git greps is
191
- // acceptable for v0.5 — codehere's own 249-symbol repo runs in ~5s.
192
- // v0.6's tree-sitter port will swap raw grep for AST-aware reference
193
- // resolution + PageRank, which is faster AND more accurate.
194
- const uniqueNames = Array.from(new Set(allSymbols.map((s) => s.name)));
195
- const refCounts = new Map();
196
- for (const name of uniqueNames) {
197
- refCounts.set(name, countReferences(repoRoot, name));
198
- }
199
- const topSymbols = allSymbols
200
- .map((s) => ({ ...s, references: refCounts.get(s.name) ?? 0 }))
201
- // Stable sort: references DESC, then name ASC for tie-breaking.
202
- .sort((a, b) => b.references - a.references || a.name.localeCompare(b.name))
203
- .slice(0, 50);
204
- return {
205
- version: 'v0.5',
206
- generatedAt: new Date().toISOString(),
207
- rootPath: repoRoot,
208
- _note: 'TS-native regex MVP · KNOWN MISSES (v0.5, addressed in v0.6 tree-sitter port): ' +
209
- '`export default function/class` (no name on the export line), ' +
210
- '`export { foo, bar }` blocks, `export { foo } from "..."` re-exports, ' +
211
- '`export * from "..."` star re-exports, multi-line `export { ... }` blocks, ' +
212
- '`export declare`, CommonJS `module.exports` / `exports.X` assignments, ' +
213
- 'symbols not at column 0 in Python. Reference counts via word-bounded ' +
214
- 'git grep undercount when a name is defined in multiple files. ' +
215
- 'Read `_stats.skippedLargeFiles` to detect files left out of the walk.',
216
- _stats: { skippedLargeFiles },
217
- files: fileMap,
218
- topSymbols,
219
- };
220
- }
221
- /**
222
- * Disk-side wrapper: build + write to .codehere/memory/episodic/repo_map.json.
223
- *
224
- * `_stats` is preserved in the on-disk JSON (codex Round 2 fix) so spawned
225
- * agents reading the map can tell "no symbols in this file" from "this file
226
- * was skipped during the walk." The CLI also surfaces it on the return value.
227
- */
228
- export function writeRepoMap(repoRoot, codehereDir, options = {}) {
229
- const targetPath = join(codehereDir, 'memory/episodic/repo_map.json');
230
- if (existsSync(targetPath) && !options.rebuild) {
231
- return { result: 'exists', path: targetPath };
232
- }
233
- const built = buildRepoMap(repoRoot);
234
- if ('error' in built) {
235
- return { result: 'failed', path: targetPath, error: built.error };
236
- }
237
- mkdirSync(dirname(targetPath), { recursive: true });
238
- writeFileSync(targetPath, JSON.stringify(built, null, 2) + '\n');
239
- return {
240
- result: 'written',
241
- path: targetPath,
242
- fileCount: Object.keys(built.files).length,
243
- symbolCount: Object.values(built.files).reduce((acc, f) => acc + f.symbols.length, 0),
244
- skippedLargeFiles: built._stats.skippedLargeFiles,
245
- };
246
- }
247
- //# sourceMappingURL=repo-map-builder.js.map
@@ -1,403 +0,0 @@
1
- /**
2
- * CLI Command: report
3
- *
4
- * Generates a shareable audit narrative — not just a score.
5
- * The viral artifact: something developers post in PRs, tweet, and put in READMEs.
6
- *
7
- * Usage:
8
- * codehere report # scan current dir, print markdown
9
- * codehere report src/ # scan a subdirectory
10
- * codehere report --out report.md # write to file
11
- * codehere report --json # machine-readable JSON
12
- * codehere report --diff HEAD # scan only changed files
13
- *
14
- * Output is a markdown document with:
15
- * - Executive summary with score and grade
16
- * - Per-severity finding breakdowns with context
17
- * - Actionable recommendations in plain English
18
- * - A shareable badge and attribution
19
- */
20
- import { writeFileSync, readFileSync, existsSync } from 'fs';
21
- import { basename, relative, join, isAbsolute } from 'path';
22
- import { colors, success, error, newline, info } from '../../../ui.js';
23
- import { scoreFile, aggregateTrust } from '../../../infrastructure/security/trust-scorer.js';
24
- import { collectCodeFiles } from '../../../infrastructure/security/file-collection.js';
25
- import { loadProductContext, adjustRecommendation } from '../../../infrastructure/config/product-context.js';
26
- // ── Markdown generation ───────────────────────────────────────────────────────
27
- // S19 glyph language: grades carry the sanctioned geometric set, never emoji.
28
- function gradeGlyph(grade) {
29
- return grade === 'A' ? '✓' : grade === 'B' ? '·' : grade === 'C' ? '!' : grade === 'D' ? '✕' : '✕✕';
30
- }
31
- function severityLabel(severity) {
32
- return severity === 'critical'
33
- ? '✕ Critical'
34
- : severity === 'high'
35
- ? '! High'
36
- : severity === 'medium'
37
- ? '· Medium'
38
- : '· Low';
39
- }
40
- function badgeUrl(score, grade) {
41
- const color = grade === 'A'
42
- ? 'brightgreen'
43
- : grade === 'B'
44
- ? 'green'
45
- : grade === 'C'
46
- ? 'yellow'
47
- : grade === 'D'
48
- ? 'orange'
49
- : 'red';
50
- return `https://img.shields.io/badge/codehere%20trust-${score}%25%20${grade}-${color}?logo=shield&logoColor=white`;
51
- }
52
- function narrativeSummary(report) {
53
- const { overallScore, criticalCount, highCount, mediumCount, files } = report;
54
- if (criticalCount === 0 && highCount === 0) {
55
- if (overallScore >= 90) {
56
- return (`Clean codebase. ${files.length} file${files.length !== 1 ? 's' : ''} scanned, nothing blocking a merge. ` +
57
- (mediumCount > 0
58
- ? `${mediumCount} medium-severity pattern${mediumCount !== 1 ? 's' : ''} worth reviewing in a future sprint.`
59
- : 'No significant findings.'));
60
- }
61
- return `Solid codebase with some medium/low findings. ${overallScore}/100 · worth a review pass but not blocking.`;
62
- }
63
- if (criticalCount > 0) {
64
- return (`${criticalCount} critical issue${criticalCount !== 1 ? 's' : ''} found that should be resolved before this merges. ` +
65
- `${highCount > 0 ? `Also ${highCount} high-severity pattern${highCount !== 1 ? 's' : ''} to address.` : ''}`);
66
- }
67
- return `No critical issues, but ${highCount} high-severity pattern${highCount !== 1 ? 's' : ''} worth addressing before merge.`;
68
- }
69
- function recommendationsMarkdown(report) {
70
- const { criticalCount, highCount, mediumCount, lowCount } = report;
71
- const lines = [];
72
- if (criticalCount > 0) {
73
- lines.push(`1. **Resolve all ${criticalCount} critical issue${criticalCount !== 1 ? 's' : ''} before merging.** These represent active security risks (credentials in code, eval injection, SQL without parameterization, etc.). They're usually quick fixes.`);
74
- }
75
- if (highCount > 0) {
76
- lines.push(`${lines.length + 1}. **Address the ${highCount} high-severity pattern${highCount !== 1 ? 's' : ''} in this sprint.** Not blocking, but worth fixing while the code is fresh.`);
77
- }
78
- if (mediumCount > 0) {
79
- lines.push(`${lines.length + 1}. **${mediumCount} medium-severity item${mediumCount !== 1 ? 's' : ''} can be queued for a future cleanup pass.** Mostly defensive hygiene · redirect hardening, input validation gaps.`);
80
- }
81
- if (lowCount > 0) {
82
- lines.push(`${lines.length + 1}. **${lowCount} low-severity note${lowCount !== 1 ? 's' : ''} for informational purposes.** These rarely cause issues but keep the score from being perfect.`);
83
- }
84
- if (lines.length === 0) {
85
- lines.push('1. **Nothing blocking.** Keep it up · this is what a healthy codebase looks like.');
86
- }
87
- return lines.join('\n\n');
88
- }
89
- function findingsMarkdown(files) {
90
- const sections = [];
91
- for (const severity of ['critical', 'high', 'medium', 'low']) {
92
- const allFindings = [];
93
- for (const f of files) {
94
- for (const finding of f.findings.filter((x) => x.severity === severity)) {
95
- allFindings.push({ filepath: f.filepath, ...finding });
96
- }
97
- }
98
- if (allFindings.length === 0)
99
- continue;
100
- sections.push(`### ${severityLabel(severity)}\n`);
101
- // Cap per-severity at 10 most impactful findings
102
- for (const f of allFindings.slice(0, 10)) {
103
- const loc = f.line ? `:${f.line}` : '';
104
- const cwe = f.cwe ? ` · ${f.cwe}` : '';
105
- const matched = f.matched ? `\n> Matched: \`${f.matched.substring(0, 80)}\`` : '';
106
- sections.push(`**${f.category}** · \`${f.filepath}${loc}\`${cwe} \n> ${f.description}${matched}\n`);
107
- }
108
- if (allFindings.length > 10) {
109
- sections.push(`*... and ${allFindings.length - 10} more ${severity}-severity findings*\n`);
110
- }
111
- }
112
- return sections.length > 0 ? sections.join('\n') : '*No findings.*\n';
113
- }
114
- function generateMarkdown(report, target, cwd) {
115
- const { overallScore, grade, files, criticalCount, highCount, mediumCount, lowCount, scannedAt } = report;
116
- const targetLabel = basename(target) || '.';
117
- const dateStr = new Date(scannedAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
118
- const badge = badgeUrl(overallScore, grade);
119
- const summary = narrativeSummary(report);
120
- const gradeIcon = gradeGlyph(grade);
121
- const productCtx = loadProductContext(cwd);
122
- const contextLine = productCtx
123
- ? `\nProject: **${productCtx.name}** · Risk level: **${productCtx.riskLevel}**${productCtx.productionApp ? ' · Production' : ''}\n`
124
- : '';
125
- const recommendation = adjustRecommendation(report.recommendation, criticalCount, highCount, overallScore, productCtx);
126
- return `# Security Audit Report · ${targetLabel}
127
-
128
- ${gradeIcon} **Score: ${overallScore}/100 · Grade ${grade}**
129
- Generated ${dateStr} by [codehere](https://github.com/codehere-dev/codehere)
130
- ${contextLine}
131
- > ${recommendation}
132
-
133
- ---
134
-
135
- ## Summary
136
-
137
- ${summary}
138
-
139
- | | |
140
- |---|---|
141
- | Files scanned | ${files.length} |
142
- | Critical | ${criticalCount > 0 ? `**${criticalCount}** ✕` : criticalCount} |
143
- | High | ${highCount > 0 ? `**${highCount}** !` : highCount} |
144
- | Medium | ${mediumCount} |
145
- | Low | ${lowCount} |
146
-
147
- ---
148
-
149
- ## Findings
150
-
151
- ${findingsMarkdown(files)}
152
-
153
- ---
154
-
155
- ## Recommendations
156
-
157
- ${recommendationsMarkdown(report)}
158
-
159
- ---
160
-
161
- ## Add to your README
162
-
163
- \`\`\`markdown
164
- ![codehere trust](${badge})
165
- \`\`\`
166
-
167
- Renders as: ![codehere trust](${badge})
168
-
169
- ---
170
-
171
- *Generated with [codehere](https://github.com/codehere-dev/codehere) — every AI action, traced. Every agent, verified.*
172
- `;
173
- }
174
- // ── Command ───────────────────────────────────────────────────────────────────
175
- // ── Simple (non-technical) output ────────────────────────────────────────────
176
- function generateSimpleReport(report, target) {
177
- const { overallScore, grade, criticalCount, highCount, mediumCount, files } = report;
178
- const targetLabel = basename(target) || 'your project';
179
- const lines = [];
180
- // Plain English headline
181
- if (criticalCount > 0) {
182
- lines.push(`Your AI assistant found ${criticalCount} serious problem${criticalCount !== 1 ? 's' : ''} in ${targetLabel} that need${criticalCount === 1 ? 's' : ''} fixing before you share this code.`);
183
- }
184
- else if (highCount > 0) {
185
- lines.push(`${targetLabel} looks mostly safe. There ${highCount === 1 ? 'is' : 'are'} ${highCount} thing${highCount !== 1 ? 's' : ''} worth reviewing, but nothing that needs urgent attention.`);
186
- }
187
- else if (overallScore >= 90) {
188
- lines.push(`${targetLabel} is clean. Your AI assistant found nothing concerning. You're good to go.`);
189
- }
190
- else {
191
- lines.push(`${targetLabel} is in reasonable shape. A few minor things to be aware of, but nothing blocking.`);
192
- }
193
- lines.push('');
194
- // Score in human terms
195
- const gradeMeaning = grade === 'A'
196
- ? 'Excellent'
197
- : grade === 'B'
198
- ? 'Good'
199
- : grade === 'C'
200
- ? 'Fair · some issues to address'
201
- : grade === 'D'
202
- ? 'Needs work'
203
- : 'Urgent attention needed';
204
- lines.push(`Security score: ${overallScore}/100 (${gradeMeaning})`);
205
- lines.push(`Files checked: ${files.length}`);
206
- lines.push('');
207
- // What was found, in plain terms
208
- if (criticalCount > 0 || highCount > 0 || mediumCount > 0) {
209
- lines.push('What was found:');
210
- if (criticalCount > 0) {
211
- lines.push(` • ${criticalCount} serious issue${criticalCount !== 1 ? 's' : ''} · things like exposed passwords, unsafe commands, or code that could be exploited`);
212
- }
213
- if (highCount > 0) {
214
- lines.push(` • ${highCount} notable issue${highCount !== 1 ? 's' : ''} · worth fixing, but not an emergency`);
215
- }
216
- if (mediumCount > 0) {
217
- lines.push(` • ${mediumCount} minor thing${mediumCount !== 1 ? 's' : ''} · good to know about, low urgency`);
218
- }
219
- lines.push('');
220
- }
221
- // Plain English what to do
222
- lines.push('What to do:');
223
- if (criticalCount > 0) {
224
- lines.push(` 1. Ask your developer to look at the ${criticalCount} serious issue${criticalCount !== 1 ? 's' : ''} listed in the full report.`);
225
- lines.push(' 2. Do not share or deploy this code until those are resolved.');
226
- if (highCount > 0) {
227
- lines.push(` 3. The ${highCount} notable issue${highCount !== 1 ? 's' : ''} can be addressed in your next sprint.`);
228
- }
229
- }
230
- else if (highCount > 0) {
231
- lines.push(` 1. Ask your developer to review the ${highCount} notable issue${highCount !== 1 ? 's' : ''} when they get a chance.`);
232
- lines.push(' 2. This code is safe to continue working on in the meantime.');
233
- }
234
- else {
235
- lines.push(' 1. Keep going · this code is in good shape.');
236
- lines.push(' 2. Run this scan again before your next release.');
237
- }
238
- lines.push('');
239
- lines.push('---');
240
- lines.push('Generated by codehere · know what your AI agent did before your tech lead asks.');
241
- return lines.join('\n');
242
- }
243
- /**
244
- * Write report to the requested path. If the path is unwritable (EROFS,
245
- * EACCES, EPERM, ENOENT on a parent dir we can't create), fall back to
246
- * stdout so the user still gets the report instead of a stack trace.
247
- *
248
- * Returns true if the file was written, false if we fell back to stdout.
249
- *
250
- * Provoked by rc.25 UAT: `codehere report --simple` from `/` crashed with
251
- * `EROFS: read-only file system, open 'codehere-audit.md'`. A user running
252
- * the command from any unwritable cwd should still get their report.
253
- */
254
- function writeReportWithFallback(targetPath, content, label) {
255
- try {
256
- writeFileSync(targetPath, content);
257
- console.log(success(`${label} written to ${targetPath}`));
258
- return true;
259
- }
260
- catch (e) {
261
- const code = e.code ?? '';
262
- if (['EROFS', 'EACCES', 'EPERM', 'ENOENT'].includes(code)) {
263
- console.error(error(`Could not write to ${targetPath} (${code}). Printing to stdout instead · pass --out <path> to a writable location.`));
264
- process.stdout.write(content + '\n');
265
- return false;
266
- }
267
- throw e;
268
- }
269
- }
270
- export async function executeReportCommand(target, options = {}) {
271
- const cwd = process.cwd();
272
- const resolved = target ? (isAbsolute(target) ? target : join(cwd, target)) : cwd;
273
- // rc.18: explicit mutex on combinations Codex flagged as silently
274
- // ambiguous. Two pairs:
275
- // format {--json, --simple} — pick at most one output format
276
- // dest {--stdout, --out} — pick at most one destination
277
- // Previously the second flag silently lost; now we error so users know.
278
- if (options.json && options.simple) {
279
- console.error(error('--json and --simple cannot be used together. Pick one output format.'));
280
- process.exit(2);
281
- }
282
- if (options.stdout && options.out) {
283
- console.error(error('--stdout and --out cannot be used together. Pick one destination.'));
284
- process.exit(2);
285
- }
286
- // Default routing rules (after mutex check):
287
- // --json → stdout (pipeable to jq, every CLI convention).
288
- // --simple / md → file (user expects a shareable artifact, not a wall of markdown).
289
- // --out <path> → wins over the default (explicit beats default).
290
- // --stdout → forces stdout for non-json formats.
291
- // Rationale: rc.15 — `report --json` writing to disk silently broke pipelines
292
- // (`codehere report --json | jq` produced empty stdout). Stdout for machine-
293
- // readable formats matches `kubectl get -o json`, `docker inspect`, etc.
294
- const defaultOut = options.json
295
- ? undefined // stdout
296
- : 'codehere-audit.md';
297
- const effectiveOut = options.stdout ? undefined : (options.out ?? defaultOut);
298
- if (!existsSync(resolved)) {
299
- console.error(error(`Not found: ${resolved}`));
300
- process.exit(1);
301
- }
302
- // Collect and scan
303
- const paths = collectCodeFiles(resolved);
304
- if (paths.length === 0) {
305
- console.log(info('No code files found to scan.'));
306
- return;
307
- }
308
- // Show progress for large codebases
309
- if (!effectiveOut && paths.length > 20 && process.stderr.isTTY) {
310
- process.stderr.write(colors.dim(` Scanning ${paths.length} files...`));
311
- }
312
- // S8 · same read semantics as `trust`: unreadable files are skipped, not
313
- // crashed on — a different read-set was the other way the report could see
314
- // a different score than the badge for the same target.
315
- const results = [];
316
- for (const p of paths) {
317
- let code;
318
- try {
319
- code = readFileSync(p, 'utf-8');
320
- }
321
- catch {
322
- continue;
323
- }
324
- results.push(scoreFile(relative(cwd, p), code));
325
- }
326
- // Clear progress line
327
- if (!effectiveOut && paths.length > 20 && process.stderr.isTTY) {
328
- process.stderr.write('\r\x1b[K');
329
- }
330
- // S8 · score + grade come from the ONE shared aggregation (trust-scorer's
331
- // aggregateTrust) — this block used to duplicate trust-command's reduce-and-
332
- // cap and the badge could disagree with the report for the same target.
333
- const { overallScore, grade, criticalCount, highCount, mediumCount, lowCount, totalFindings } = aggregateTrust(results);
334
- // Same rule as trust-command: a score aggregated over zero files is not a clean scan.
335
- const scanned = results.length > 0;
336
- const recommendation = criticalCount > 0
337
- ? `${criticalCount} critical issue${criticalCount !== 1 ? 's' : ''} need${criticalCount === 1 ? 's' : ''} attention before this merges.`
338
- : highCount > 0
339
- ? `Nothing critical, but ${highCount} high-severity pattern${highCount !== 1 ? 's' : ''} worth addressing.`
340
- : !scanned
341
- ? 'No files scanned — nothing was graded.'
342
- : overallScore >= 75
343
- ? 'Clean scan. Nothing blocking a merge.'
344
- : 'A few things to look at · nothing critical.';
345
- const report = {
346
- files: results,
347
- scanned,
348
- overallScore,
349
- grade,
350
- totalFindings,
351
- criticalCount,
352
- highCount,
353
- mediumCount,
354
- lowCount,
355
- recommendation,
356
- scannedAt: new Date().toISOString(),
357
- };
358
- if (paths.length > 20) {
359
- process.stderr.write(' '.repeat(40) + '\r');
360
- }
361
- // JSON output
362
- if (options.json) {
363
- const output = JSON.stringify(report, null, 2);
364
- if (effectiveOut) {
365
- writeReportWithFallback(effectiveOut, output, 'Report');
366
- }
367
- else {
368
- console.log(output);
369
- }
370
- return;
371
- }
372
- // Simple (non-technical) output
373
- if (options.simple) {
374
- const simple = generateSimpleReport(report, resolved);
375
- if (effectiveOut) {
376
- writeReportWithFallback(effectiveOut, simple, 'Plain-English report');
377
- }
378
- else {
379
- process.stdout.write(simple + '\n');
380
- }
381
- return;
382
- }
383
- // Markdown output
384
- const markdown = generateMarkdown(report, resolved, cwd);
385
- if (effectiveOut) {
386
- if (!writeReportWithFallback(effectiveOut, markdown, 'Report'))
387
- return;
388
- newline();
389
- console.log(colors.dim(` ${paths.length} files · score ${overallScore}/100 · grade ${grade}`));
390
- if (criticalCount > 0) {
391
- console.log(colors.red(` ${criticalCount} critical issue${criticalCount !== 1 ? 's' : ''} · check the report`));
392
- }
393
- newline();
394
- console.log(colors.dim(' Share it: paste it in your PR, add the badge to your README.'));
395
- console.log(colors.dim(' Use --stdout to pipe the report instead of writing a file.'));
396
- newline();
397
- }
398
- else {
399
- // --stdout: print to stdout (can be piped)
400
- process.stdout.write(markdown);
401
- }
402
- }
403
- //# sourceMappingURL=report-command.js.map