@oddessentials/odd-ai-reviewers 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -0
- package/dist/__tests__/hermetic-setup.d.ts +55 -0
- package/dist/__tests__/hermetic-setup.d.ts.map +1 -0
- package/dist/__tests__/hermetic-setup.js +62 -0
- package/dist/__tests__/hermetic-setup.js.map +1 -0
- package/dist/__tests__/test-utils/hermetic.d.ts +84 -0
- package/dist/__tests__/test-utils/hermetic.d.ts.map +1 -0
- package/dist/__tests__/test-utils/hermetic.js +147 -0
- package/dist/__tests__/test-utils/hermetic.js.map +1 -0
- package/dist/agents/ai_semantic_review.d.ts +12 -0
- package/dist/agents/ai_semantic_review.d.ts.map +1 -0
- package/dist/agents/ai_semantic_review.js +317 -0
- package/dist/agents/ai_semantic_review.js.map +1 -0
- package/dist/agents/control_flow/budget.d.ts +162 -0
- package/dist/agents/control_flow/budget.d.ts.map +1 -0
- package/dist/agents/control_flow/budget.js +331 -0
- package/dist/agents/control_flow/budget.js.map +1 -0
- package/dist/agents/control_flow/cfg-builder.d.ts +26 -0
- package/dist/agents/control_flow/cfg-builder.d.ts.map +1 -0
- package/dist/agents/control_flow/cfg-builder.js +776 -0
- package/dist/agents/control_flow/cfg-builder.js.map +1 -0
- package/dist/agents/control_flow/cfg-types.d.ts +186 -0
- package/dist/agents/control_flow/cfg-types.d.ts.map +1 -0
- package/dist/agents/control_flow/cfg-types.js +114 -0
- package/dist/agents/control_flow/cfg-types.js.map +1 -0
- package/dist/agents/control_flow/finding-generator.d.ts +118 -0
- package/dist/agents/control_flow/finding-generator.d.ts.map +1 -0
- package/dist/agents/control_flow/finding-generator.js +354 -0
- package/dist/agents/control_flow/finding-generator.js.map +1 -0
- package/dist/agents/control_flow/index.d.ts +39 -0
- package/dist/agents/control_flow/index.d.ts.map +1 -0
- package/dist/agents/control_flow/index.js +270 -0
- package/dist/agents/control_flow/index.js.map +1 -0
- package/dist/agents/control_flow/logger.d.ts +333 -0
- package/dist/agents/control_flow/logger.d.ts.map +1 -0
- package/dist/agents/control_flow/logger.js +607 -0
- package/dist/agents/control_flow/logger.js.map +1 -0
- package/dist/agents/control_flow/mitigation-detector.d.ts +207 -0
- package/dist/agents/control_flow/mitigation-detector.d.ts.map +1 -0
- package/dist/agents/control_flow/mitigation-detector.js +625 -0
- package/dist/agents/control_flow/mitigation-detector.js.map +1 -0
- package/dist/agents/control_flow/mitigation-patterns.d.ts +53 -0
- package/dist/agents/control_flow/mitigation-patterns.d.ts.map +1 -0
- package/dist/agents/control_flow/mitigation-patterns.js +620 -0
- package/dist/agents/control_flow/mitigation-patterns.js.map +1 -0
- package/dist/agents/control_flow/path-analyzer.d.ts +287 -0
- package/dist/agents/control_flow/path-analyzer.d.ts.map +1 -0
- package/dist/agents/control_flow/path-analyzer.js +695 -0
- package/dist/agents/control_flow/path-analyzer.js.map +1 -0
- package/dist/agents/control_flow/pattern-validator.d.ts +132 -0
- package/dist/agents/control_flow/pattern-validator.d.ts.map +1 -0
- package/dist/agents/control_flow/pattern-validator.js +420 -0
- package/dist/agents/control_flow/pattern-validator.js.map +1 -0
- package/dist/agents/control_flow/timeout-regex.d.ts +144 -0
- package/dist/agents/control_flow/timeout-regex.d.ts.map +1 -0
- package/dist/agents/control_flow/timeout-regex.js +339 -0
- package/dist/agents/control_flow/timeout-regex.js.map +1 -0
- package/dist/agents/control_flow/types.d.ts +782 -0
- package/dist/agents/control_flow/types.d.ts.map +1 -0
- package/dist/agents/control_flow/types.js +428 -0
- package/dist/agents/control_flow/types.js.map +1 -0
- package/dist/agents/control_flow/vulnerability-detector.d.ts +85 -0
- package/dist/agents/control_flow/vulnerability-detector.d.ts.map +1 -0
- package/dist/agents/control_flow/vulnerability-detector.js +493 -0
- package/dist/agents/control_flow/vulnerability-detector.js.map +1 -0
- package/dist/agents/date-utils.d.ts +19 -0
- package/dist/agents/date-utils.d.ts.map +1 -0
- package/dist/agents/date-utils.js +29 -0
- package/dist/agents/date-utils.js.map +1 -0
- package/dist/agents/index.d.ts +25 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +50 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/json-utils.d.ts +34 -0
- package/dist/agents/json-utils.d.ts.map +1 -0
- package/dist/agents/json-utils.js +62 -0
- package/dist/agents/json-utils.js.map +1 -0
- package/dist/agents/local_llm.d.ts +24 -0
- package/dist/agents/local_llm.d.ts.map +1 -0
- package/dist/agents/local_llm.js +566 -0
- package/dist/agents/local_llm.js.map +1 -0
- package/dist/agents/metadata.d.ts +57 -0
- package/dist/agents/metadata.d.ts.map +1 -0
- package/dist/agents/metadata.js +45 -0
- package/dist/agents/metadata.js.map +1 -0
- package/dist/agents/opencode.d.ts +18 -0
- package/dist/agents/opencode.d.ts.map +1 -0
- package/dist/agents/opencode.js +364 -0
- package/dist/agents/opencode.js.map +1 -0
- package/dist/agents/path-filter.d.ts +25 -0
- package/dist/agents/path-filter.d.ts.map +1 -0
- package/dist/agents/path-filter.js +43 -0
- package/dist/agents/path-filter.js.map +1 -0
- package/dist/agents/pr_agent.d.ts +3 -0
- package/dist/agents/pr_agent.d.ts.map +1 -0
- package/dist/agents/pr_agent.js +312 -0
- package/dist/agents/pr_agent.js.map +1 -0
- package/dist/agents/retry.d.ts +12 -0
- package/dist/agents/retry.d.ts.map +1 -0
- package/dist/agents/retry.js +65 -0
- package/dist/agents/retry.js.map +1 -0
- package/dist/agents/reviewdog.d.ts +24 -0
- package/dist/agents/reviewdog.d.ts.map +1 -0
- package/dist/agents/reviewdog.js +259 -0
- package/dist/agents/reviewdog.js.map +1 -0
- package/dist/agents/security.d.ts +49 -0
- package/dist/agents/security.d.ts.map +1 -0
- package/dist/agents/security.js +302 -0
- package/dist/agents/security.js.map +1 -0
- package/dist/agents/semgrep.d.ts +8 -0
- package/dist/agents/semgrep.d.ts.map +1 -0
- package/dist/agents/semgrep.js +157 -0
- package/dist/agents/semgrep.js.map +1 -0
- package/dist/agents/types.d.ts +450 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +127 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/budget.d.ts +59 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +82 -0
- package/dist/budget.js.map +1 -0
- package/dist/cache/key.d.ts +49 -0
- package/dist/cache/key.d.ts.map +1 -0
- package/dist/cache/key.js +71 -0
- package/dist/cache/key.js.map +1 -0
- package/dist/cache/store.d.ts +47 -0
- package/dist/cache/store.d.ts.map +1 -0
- package/dist/cache/store.js +328 -0
- package/dist/cache/store.js.map +1 -0
- package/dist/cli/commands/check.d.ts +60 -0
- package/dist/cli/commands/check.d.ts.map +1 -0
- package/dist/cli/commands/check.js +163 -0
- package/dist/cli/commands/check.js.map +1 -0
- package/dist/cli/commands/index.d.ts +12 -0
- package/dist/cli/commands/index.d.ts.map +1 -0
- package/dist/cli/commands/index.js +12 -0
- package/dist/cli/commands/index.js.map +1 -0
- package/dist/cli/commands/local-review.d.ts +149 -0
- package/dist/cli/commands/local-review.d.ts.map +1 -0
- package/dist/cli/commands/local-review.js +755 -0
- package/dist/cli/commands/local-review.js.map +1 -0
- package/dist/cli/config-wizard.d.ts +87 -0
- package/dist/cli/config-wizard.d.ts.map +1 -0
- package/dist/cli/config-wizard.js +240 -0
- package/dist/cli/config-wizard.js.map +1 -0
- package/dist/cli/dependencies/catalog.d.ts +44 -0
- package/dist/cli/dependencies/catalog.d.ts.map +1 -0
- package/dist/cli/dependencies/catalog.js +89 -0
- package/dist/cli/dependencies/catalog.js.map +1 -0
- package/dist/cli/dependencies/checker.d.ts +42 -0
- package/dist/cli/dependencies/checker.d.ts.map +1 -0
- package/dist/cli/dependencies/checker.js +240 -0
- package/dist/cli/dependencies/checker.js.map +1 -0
- package/dist/cli/dependencies/index.d.ts +16 -0
- package/dist/cli/dependencies/index.d.ts.map +1 -0
- package/dist/cli/dependencies/index.js +16 -0
- package/dist/cli/dependencies/index.js.map +1 -0
- package/dist/cli/dependencies/messages.d.ts +58 -0
- package/dist/cli/dependencies/messages.d.ts.map +1 -0
- package/dist/cli/dependencies/messages.js +183 -0
- package/dist/cli/dependencies/messages.js.map +1 -0
- package/dist/cli/dependencies/platform.d.ts +25 -0
- package/dist/cli/dependencies/platform.d.ts.map +1 -0
- package/dist/cli/dependencies/platform.js +42 -0
- package/dist/cli/dependencies/platform.js.map +1 -0
- package/dist/cli/dependencies/schemas.d.ts +65 -0
- package/dist/cli/dependencies/schemas.d.ts.map +1 -0
- package/dist/cli/dependencies/schemas.js +42 -0
- package/dist/cli/dependencies/schemas.js.map +1 -0
- package/dist/cli/dependencies/types.d.ts +112 -0
- package/dist/cli/dependencies/types.d.ts.map +1 -0
- package/dist/cli/dependencies/types.js +6 -0
- package/dist/cli/dependencies/types.js.map +1 -0
- package/dist/cli/dependencies/version.d.ts +67 -0
- package/dist/cli/dependencies/version.d.ts.map +1 -0
- package/dist/cli/dependencies/version.js +125 -0
- package/dist/cli/dependencies/version.js.map +1 -0
- package/dist/cli/git-context.d.ts +105 -0
- package/dist/cli/git-context.d.ts.map +1 -0
- package/dist/cli/git-context.js +313 -0
- package/dist/cli/git-context.js.map +1 -0
- package/dist/cli/interactive-prompts.d.ts +126 -0
- package/dist/cli/interactive-prompts.d.ts.map +1 -0
- package/dist/cli/interactive-prompts.js +128 -0
- package/dist/cli/interactive-prompts.js.map +1 -0
- package/dist/cli/options/index.d.ts +7 -0
- package/dist/cli/options/index.d.ts.map +1 -0
- package/dist/cli/options/index.js +11 -0
- package/dist/cli/options/index.js.map +1 -0
- package/dist/cli/options/local-review-options.d.ts +221 -0
- package/dist/cli/options/local-review-options.d.ts.map +1 -0
- package/dist/cli/options/local-review-options.js +332 -0
- package/dist/cli/options/local-review-options.js.map +1 -0
- package/dist/cli/output/colors.d.ts +154 -0
- package/dist/cli/output/colors.d.ts.map +1 -0
- package/dist/cli/output/colors.js +255 -0
- package/dist/cli/output/colors.js.map +1 -0
- package/dist/cli/output/errors.d.ts +157 -0
- package/dist/cli/output/errors.d.ts.map +1 -0
- package/dist/cli/output/errors.js +266 -0
- package/dist/cli/output/errors.js.map +1 -0
- package/dist/cli/output/index.d.ts +12 -0
- package/dist/cli/output/index.d.ts.map +1 -0
- package/dist/cli/output/index.js +15 -0
- package/dist/cli/output/index.js.map +1 -0
- package/dist/cli/output/progress.d.ts +237 -0
- package/dist/cli/output/progress.d.ts.map +1 -0
- package/dist/cli/output/progress.js +405 -0
- package/dist/cli/output/progress.js.map +1 -0
- package/dist/cli/signals.d.ts +145 -0
- package/dist/cli/signals.d.ts.map +1 -0
- package/dist/cli/signals.js +223 -0
- package/dist/cli/signals.js.map +1 -0
- package/dist/cli/validation-report.d.ts +106 -0
- package/dist/cli/validation-report.d.ts.map +1 -0
- package/dist/cli/validation-report.js +108 -0
- package/dist/cli/validation-report.js.map +1 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +12 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/mitigation-config.d.ts +94 -0
- package/dist/config/mitigation-config.d.ts.map +1 -0
- package/dist/config/mitigation-config.js +430 -0
- package/dist/config/mitigation-config.js.map +1 -0
- package/dist/config/providers.d.ts +118 -0
- package/dist/config/providers.d.ts.map +1 -0
- package/dist/config/providers.js +229 -0
- package/dist/config/providers.js.map +1 -0
- package/dist/config/schemas.d.ts +278 -0
- package/dist/config/schemas.d.ts.map +1 -0
- package/dist/config/schemas.js +111 -0
- package/dist/config/schemas.js.map +1 -0
- package/dist/config/zero-config.d.ts +126 -0
- package/dist/config/zero-config.d.ts.map +1 -0
- package/dist/config/zero-config.js +243 -0
- package/dist/config/zero-config.js.map +1 -0
- package/dist/config.d.ts +110 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +302 -0
- package/dist/config.js.map +1 -0
- package/dist/diff.d.ts +224 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +832 -0
- package/dist/diff.js.map +1 -0
- package/dist/git-validators.d.ts +106 -0
- package/dist/git-validators.d.ts.map +1 -0
- package/dist/git-validators.js +224 -0
- package/dist/git-validators.js.map +1 -0
- package/dist/main.d.ts +61 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +704 -0
- package/dist/main.js.map +1 -0
- package/dist/phases/execute.d.ts +60 -0
- package/dist/phases/execute.d.ts.map +1 -0
- package/dist/phases/execute.js +168 -0
- package/dist/phases/execute.js.map +1 -0
- package/dist/phases/index.d.ts +9 -0
- package/dist/phases/index.d.ts.map +1 -0
- package/dist/phases/index.js +9 -0
- package/dist/phases/index.js.map +1 -0
- package/dist/phases/preflight.d.ts +40 -0
- package/dist/phases/preflight.d.ts.map +1 -0
- package/dist/phases/preflight.js +122 -0
- package/dist/phases/preflight.js.map +1 -0
- package/dist/phases/report.d.ts +51 -0
- package/dist/phases/report.d.ts.map +1 -0
- package/dist/phases/report.js +152 -0
- package/dist/phases/report.js.map +1 -0
- package/dist/policy.d.ts +33 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +34 -0
- package/dist/policy.js.map +1 -0
- package/dist/preflight.d.ts +181 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +627 -0
- package/dist/preflight.js.map +1 -0
- package/dist/report/ado.d.ts +53 -0
- package/dist/report/ado.d.ts.map +1 -0
- package/dist/report/ado.js +411 -0
- package/dist/report/ado.js.map +1 -0
- package/dist/report/agent-icons.d.ts +36 -0
- package/dist/report/agent-icons.d.ts.map +1 -0
- package/dist/report/agent-icons.js +46 -0
- package/dist/report/agent-icons.js.map +1 -0
- package/dist/report/base.d.ts +30 -0
- package/dist/report/base.d.ts.map +1 -0
- package/dist/report/base.js +64 -0
- package/dist/report/base.js.map +1 -0
- package/dist/report/formats.d.ts +206 -0
- package/dist/report/formats.d.ts.map +1 -0
- package/dist/report/formats.js +481 -0
- package/dist/report/formats.js.map +1 -0
- package/dist/report/github.d.ts +44 -0
- package/dist/report/github.d.ts.map +1 -0
- package/dist/report/github.js +409 -0
- package/dist/report/github.js.map +1 -0
- package/dist/report/line-resolver.d.ts +208 -0
- package/dist/report/line-resolver.d.ts.map +1 -0
- package/dist/report/line-resolver.js +578 -0
- package/dist/report/line-resolver.js.map +1 -0
- package/dist/report/resolution.d.ts +158 -0
- package/dist/report/resolution.d.ts.map +1 -0
- package/dist/report/resolution.js +272 -0
- package/dist/report/resolution.js.map +1 -0
- package/dist/report/sanitize.d.ts +32 -0
- package/dist/report/sanitize.d.ts.map +1 -0
- package/dist/report/sanitize.js +84 -0
- package/dist/report/sanitize.js.map +1 -0
- package/dist/report/terminal.d.ts +440 -0
- package/dist/report/terminal.d.ts.map +1 -0
- package/dist/report/terminal.js +840 -0
- package/dist/report/terminal.js.map +1 -0
- package/dist/reviewignore.d.ts +125 -0
- package/dist/reviewignore.d.ts.map +1 -0
- package/dist/reviewignore.js +335 -0
- package/dist/reviewignore.js.map +1 -0
- package/dist/security-logger.d.ts +178 -0
- package/dist/security-logger.d.ts.map +1 -0
- package/dist/security-logger.js +256 -0
- package/dist/security-logger.js.map +1 -0
- package/dist/telemetry/backends/console.d.ts +24 -0
- package/dist/telemetry/backends/console.d.ts.map +1 -0
- package/dist/telemetry/backends/console.js +54 -0
- package/dist/telemetry/backends/console.js.map +1 -0
- package/dist/telemetry/backends/jsonl.d.ts +31 -0
- package/dist/telemetry/backends/jsonl.d.ts.map +1 -0
- package/dist/telemetry/backends/jsonl.js +121 -0
- package/dist/telemetry/backends/jsonl.js.map +1 -0
- package/dist/telemetry/emitter.d.ts +43 -0
- package/dist/telemetry/emitter.d.ts.map +1 -0
- package/dist/telemetry/emitter.js +83 -0
- package/dist/telemetry/emitter.js.map +1 -0
- package/dist/telemetry/hook.d.ts +53 -0
- package/dist/telemetry/hook.d.ts.map +1 -0
- package/dist/telemetry/hook.js +118 -0
- package/dist/telemetry/hook.js.map +1 -0
- package/dist/telemetry/index.d.ts +58 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +143 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/telemetry/types.d.ts +139 -0
- package/dist/telemetry/types.d.ts.map +1 -0
- package/dist/telemetry/types.js +133 -0
- package/dist/telemetry/types.js.map +1 -0
- package/dist/trust.d.ts +65 -0
- package/dist/trust.d.ts.map +1 -0
- package/dist/trust.js +78 -0
- package/dist/trust.js.map +1 -0
- package/dist/types/assert-never.d.ts +30 -0
- package/dist/types/assert-never.d.ts.map +1 -0
- package/dist/types/assert-never.js +32 -0
- package/dist/types/assert-never.js.map +1 -0
- package/dist/types/branded.d.ts +172 -0
- package/dist/types/branded.d.ts.map +1 -0
- package/dist/types/branded.js +262 -0
- package/dist/types/branded.js.map +1 -0
- package/dist/types/errors.d.ts +320 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +551 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/index.d.ts +37 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +77 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/result.d.ts +323 -0
- package/dist/types/result.d.ts.map +1 -0
- package/dist/types/result.js +423 -0
- package/dist/types/result.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal Reporter Module
|
|
3
|
+
*
|
|
4
|
+
* Formats findings for terminal display with support for:
|
|
5
|
+
* - Pretty format (colors, boxes, code snippets)
|
|
6
|
+
* - JSON format (with schema_version for compatibility)
|
|
7
|
+
* - SARIF 2.1.0 format (for IDE integration)
|
|
8
|
+
*
|
|
9
|
+
* Follows the same processing pipeline as GitHub/ADO reporters.
|
|
10
|
+
*/
|
|
11
|
+
import { canonicalizeDiffFiles } from '../diff.js';
|
|
12
|
+
import { buildLineResolver, normalizeFindingsForDiff } from './line-resolver.js';
|
|
13
|
+
import { deduplicateFindings, deduplicatePartialFindings, sortFindings, countBySeverity, } from './formats.js';
|
|
14
|
+
import { ANSI, colorize, visibleLength, createColorizer, supportsUnicode, } from '../cli/output/colors.js';
|
|
15
|
+
import { formatDuration, getSeverityIndicator } from '../cli/output/progress.js';
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Constants
|
|
18
|
+
// =============================================================================
|
|
19
|
+
/** Current JSON output schema version */
|
|
20
|
+
export const JSON_SCHEMA_VERSION = '1.0.0';
|
|
21
|
+
/** SARIF schema URL */
|
|
22
|
+
export const SARIF_SCHEMA_URL = 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json';
|
|
23
|
+
/** Tool information URI */
|
|
24
|
+
export const TOOL_INFO_URI = 'https://github.com/oddessentials/odd-ai-reviewers';
|
|
25
|
+
/** Tool name for SARIF output */
|
|
26
|
+
export const TOOL_NAME = 'odd-ai-reviewers';
|
|
27
|
+
/** Default box width for findings */
|
|
28
|
+
const DEFAULT_BOX_WIDTH = 80;
|
|
29
|
+
/** Number of context lines for code snippets */
|
|
30
|
+
const CONTEXT_LINES = 3;
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// Box Drawing Utilities
|
|
33
|
+
// =============================================================================
|
|
34
|
+
/**
|
|
35
|
+
* Unicode box drawing characters
|
|
36
|
+
*/
|
|
37
|
+
export const BOX_CHARS = {
|
|
38
|
+
topLeft: '┌',
|
|
39
|
+
topRight: '┐',
|
|
40
|
+
bottomLeft: '└',
|
|
41
|
+
bottomRight: '┘',
|
|
42
|
+
horizontal: '─',
|
|
43
|
+
vertical: '│',
|
|
44
|
+
sectionDivider: '━',
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* ASCII fallback box drawing characters
|
|
48
|
+
*/
|
|
49
|
+
export const ASCII_BOX_CHARS = {
|
|
50
|
+
topLeft: '+',
|
|
51
|
+
topRight: '+',
|
|
52
|
+
bottomLeft: '+',
|
|
53
|
+
bottomRight: '+',
|
|
54
|
+
horizontal: '-',
|
|
55
|
+
vertical: '|',
|
|
56
|
+
sectionDivider: '=',
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Get box drawing characters based on unicode support
|
|
60
|
+
*
|
|
61
|
+
* @param useUnicode - Whether to use Unicode characters
|
|
62
|
+
* @returns Box drawing character set
|
|
63
|
+
*/
|
|
64
|
+
export function getBoxChars(useUnicode) {
|
|
65
|
+
return useUnicode ? BOX_CHARS : ASCII_BOX_CHARS;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Draw a horizontal line
|
|
69
|
+
*
|
|
70
|
+
* @param width - Line width in characters
|
|
71
|
+
* @param char - Character to use
|
|
72
|
+
* @returns Horizontal line string
|
|
73
|
+
*/
|
|
74
|
+
export function drawHorizontalLine(width, char) {
|
|
75
|
+
return char.repeat(width);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Draw a section divider
|
|
79
|
+
*
|
|
80
|
+
* @param width - Divider width
|
|
81
|
+
* @param useUnicode - Whether to use Unicode
|
|
82
|
+
* @returns Section divider string
|
|
83
|
+
*/
|
|
84
|
+
export function drawSectionDivider(width, useUnicode) {
|
|
85
|
+
const chars = getBoxChars(useUnicode);
|
|
86
|
+
return chars.sectionDivider.repeat(width);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Pad text to fit within a box
|
|
90
|
+
*
|
|
91
|
+
* @param text - Text to pad
|
|
92
|
+
* @param width - Target width
|
|
93
|
+
* @param align - Alignment ('left', 'center', 'right')
|
|
94
|
+
* @returns Padded text
|
|
95
|
+
*/
|
|
96
|
+
export function padToWidth(text, width, align = 'left') {
|
|
97
|
+
const visLen = visibleLength(text);
|
|
98
|
+
if (visLen >= width) {
|
|
99
|
+
return text;
|
|
100
|
+
}
|
|
101
|
+
const padding = width - visLen;
|
|
102
|
+
switch (align) {
|
|
103
|
+
case 'center': {
|
|
104
|
+
const leftPad = Math.floor(padding / 2);
|
|
105
|
+
const rightPad = padding - leftPad;
|
|
106
|
+
return ' '.repeat(leftPad) + text + ' '.repeat(rightPad);
|
|
107
|
+
}
|
|
108
|
+
case 'right':
|
|
109
|
+
return ' '.repeat(padding) + text;
|
|
110
|
+
case 'left':
|
|
111
|
+
default:
|
|
112
|
+
return text + ' '.repeat(padding);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Wrap text to fit within a maximum width
|
|
117
|
+
*
|
|
118
|
+
* @param text - Text to wrap
|
|
119
|
+
* @param maxWidth - Maximum width per line
|
|
120
|
+
* @returns Array of wrapped lines
|
|
121
|
+
*/
|
|
122
|
+
export function wrapText(text, maxWidth) {
|
|
123
|
+
if (maxWidth <= 0)
|
|
124
|
+
return [text];
|
|
125
|
+
const words = text.split(/\s+/);
|
|
126
|
+
const lines = [];
|
|
127
|
+
let currentLine = '';
|
|
128
|
+
for (const word of words) {
|
|
129
|
+
if (!word)
|
|
130
|
+
continue;
|
|
131
|
+
const wordLen = visibleLength(word);
|
|
132
|
+
const currentLen = visibleLength(currentLine);
|
|
133
|
+
if (currentLen === 0) {
|
|
134
|
+
// First word on line - always add it even if too long
|
|
135
|
+
currentLine = word;
|
|
136
|
+
}
|
|
137
|
+
else if (currentLen + 1 + wordLen <= maxWidth) {
|
|
138
|
+
// Word fits with space
|
|
139
|
+
currentLine += ' ' + word;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
// Word doesn't fit - start new line
|
|
143
|
+
lines.push(currentLine);
|
|
144
|
+
currentLine = word;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (currentLine) {
|
|
148
|
+
lines.push(currentLine);
|
|
149
|
+
}
|
|
150
|
+
return lines.length > 0 ? lines : [''];
|
|
151
|
+
}
|
|
152
|
+
// =============================================================================
|
|
153
|
+
// Code Snippet Extraction
|
|
154
|
+
// =============================================================================
|
|
155
|
+
/**
|
|
156
|
+
* Detect programming language from file extension
|
|
157
|
+
*
|
|
158
|
+
* @param filePath - File path
|
|
159
|
+
* @returns Language identifier or undefined
|
|
160
|
+
*/
|
|
161
|
+
export function detectLanguage(filePath) {
|
|
162
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
163
|
+
const langMap = {
|
|
164
|
+
ts: 'typescript',
|
|
165
|
+
tsx: 'typescript',
|
|
166
|
+
js: 'javascript',
|
|
167
|
+
jsx: 'javascript',
|
|
168
|
+
py: 'python',
|
|
169
|
+
rb: 'ruby',
|
|
170
|
+
go: 'go',
|
|
171
|
+
rs: 'rust',
|
|
172
|
+
java: 'java',
|
|
173
|
+
kt: 'kotlin',
|
|
174
|
+
swift: 'swift',
|
|
175
|
+
c: 'c',
|
|
176
|
+
cpp: 'cpp',
|
|
177
|
+
cc: 'cpp',
|
|
178
|
+
h: 'c',
|
|
179
|
+
hpp: 'cpp',
|
|
180
|
+
cs: 'csharp',
|
|
181
|
+
php: 'php',
|
|
182
|
+
sh: 'bash',
|
|
183
|
+
bash: 'bash',
|
|
184
|
+
zsh: 'bash',
|
|
185
|
+
yaml: 'yaml',
|
|
186
|
+
yml: 'yaml',
|
|
187
|
+
json: 'json',
|
|
188
|
+
md: 'markdown',
|
|
189
|
+
sql: 'sql',
|
|
190
|
+
css: 'css',
|
|
191
|
+
scss: 'scss',
|
|
192
|
+
less: 'less',
|
|
193
|
+
html: 'html',
|
|
194
|
+
xml: 'xml',
|
|
195
|
+
};
|
|
196
|
+
return ext ? langMap[ext] : undefined;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Extract code snippet from a diff patch for a specific line
|
|
200
|
+
*
|
|
201
|
+
* @param patch - Unified diff patch content
|
|
202
|
+
* @param targetLine - The line number to highlight (1-indexed in new file)
|
|
203
|
+
* @param contextLines - Number of context lines before/after
|
|
204
|
+
* @param filePath - File path for language detection
|
|
205
|
+
* @returns CodeSnippet or undefined if line not found
|
|
206
|
+
*/
|
|
207
|
+
export function extractCodeSnippet(patch, targetLine, contextLines = CONTEXT_LINES, filePath) {
|
|
208
|
+
if (!patch || !targetLine) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
const lines = patch.split('\n');
|
|
212
|
+
const snippetLines = [];
|
|
213
|
+
let currentNewLine = 0;
|
|
214
|
+
let foundTarget = false;
|
|
215
|
+
let highlightIndex = -1;
|
|
216
|
+
// Build a map of new file lines from the patch
|
|
217
|
+
const newFileContent = [];
|
|
218
|
+
for (const line of lines) {
|
|
219
|
+
// Parse hunk header: @@ -oldStart,oldCount +newStart,newCount @@
|
|
220
|
+
if (line.startsWith('@@')) {
|
|
221
|
+
const match = line.match(/@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
222
|
+
if (match) {
|
|
223
|
+
currentNewLine = parseInt(match[1] ?? '0', 10);
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
// Skip metadata lines
|
|
228
|
+
if (line.startsWith('diff ') ||
|
|
229
|
+
line.startsWith('index ') ||
|
|
230
|
+
line.startsWith('--- ') ||
|
|
231
|
+
line.startsWith('+++ ')) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const prefix = line[0];
|
|
235
|
+
if (prefix === '+') {
|
|
236
|
+
// Added line - exists in new file
|
|
237
|
+
newFileContent.push({
|
|
238
|
+
lineNumber: currentNewLine,
|
|
239
|
+
content: line.slice(1), // Remove prefix
|
|
240
|
+
});
|
|
241
|
+
currentNewLine++;
|
|
242
|
+
}
|
|
243
|
+
else if (prefix === '-') {
|
|
244
|
+
// Deleted line - does NOT exist in new file
|
|
245
|
+
// Don't increment currentNewLine
|
|
246
|
+
}
|
|
247
|
+
else if (prefix === ' ') {
|
|
248
|
+
// Context line - exists in new file
|
|
249
|
+
newFileContent.push({
|
|
250
|
+
lineNumber: currentNewLine,
|
|
251
|
+
content: line.slice(1), // Remove prefix
|
|
252
|
+
});
|
|
253
|
+
currentNewLine++;
|
|
254
|
+
}
|
|
255
|
+
else if (prefix === '\\') {
|
|
256
|
+
// "" marker - skip
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Find the target line and extract context
|
|
260
|
+
const targetIndex = newFileContent.findIndex((l) => l.lineNumber === targetLine);
|
|
261
|
+
if (targetIndex === -1) {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
foundTarget = true;
|
|
265
|
+
// Calculate range for context
|
|
266
|
+
const startIdx = Math.max(0, targetIndex - contextLines);
|
|
267
|
+
const endIdx = Math.min(newFileContent.length - 1, targetIndex + contextLines);
|
|
268
|
+
for (let i = startIdx; i <= endIdx; i++) {
|
|
269
|
+
const lineData = newFileContent[i];
|
|
270
|
+
if (lineData) {
|
|
271
|
+
const isHighlighted = lineData.lineNumber === targetLine;
|
|
272
|
+
if (isHighlighted) {
|
|
273
|
+
highlightIndex = snippetLines.length;
|
|
274
|
+
}
|
|
275
|
+
snippetLines.push({
|
|
276
|
+
lineNumber: lineData.lineNumber,
|
|
277
|
+
content: lineData.content,
|
|
278
|
+
isHighlighted,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (!foundTarget || snippetLines.length === 0) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
lines: snippetLines,
|
|
287
|
+
highlightLine: highlightIndex,
|
|
288
|
+
language: filePath ? detectLanguage(filePath) : undefined,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
// =============================================================================
|
|
292
|
+
// Finding Box Formatting
|
|
293
|
+
// =============================================================================
|
|
294
|
+
/**
|
|
295
|
+
* Format a code snippet for terminal display
|
|
296
|
+
*
|
|
297
|
+
* @param snippet - Code snippet to format
|
|
298
|
+
* @param colored - Whether to use colors
|
|
299
|
+
* @param boxWidth - Width of the containing box
|
|
300
|
+
* @returns Formatted code snippet lines
|
|
301
|
+
*/
|
|
302
|
+
export function formatCodeSnippet(snippet, colored, boxWidth) {
|
|
303
|
+
const result = [];
|
|
304
|
+
const c = createColorizer(colored);
|
|
305
|
+
// Calculate line number width
|
|
306
|
+
const maxLineNum = Math.max(...snippet.lines.map((l) => l.lineNumber));
|
|
307
|
+
const lineNumWidth = Math.max(String(maxLineNum).length, 3);
|
|
308
|
+
// Content area width (inside box, accounting for padding and line numbers)
|
|
309
|
+
const contentWidth = boxWidth - 4 - lineNumWidth - 3; // 4 for box, 3 for " | "
|
|
310
|
+
for (const line of snippet.lines) {
|
|
311
|
+
const lineNumStr = String(line.lineNumber).padStart(lineNumWidth, ' ');
|
|
312
|
+
const prefix = line.isHighlighted ? '▸' : ' ';
|
|
313
|
+
// Truncate long lines
|
|
314
|
+
let content = line.content;
|
|
315
|
+
if (content.length > contentWidth) {
|
|
316
|
+
content = content.slice(0, contentWidth - 3) + '...';
|
|
317
|
+
}
|
|
318
|
+
if (line.isHighlighted) {
|
|
319
|
+
// Highlight the target line
|
|
320
|
+
const formattedLine = colored
|
|
321
|
+
? `${c.cyan(prefix)} ${c.gray(lineNumStr)} ${c.gray('│')} ${colorize(content, ANSI.inverse, colored)}`
|
|
322
|
+
: `${prefix} ${lineNumStr} | ${content}`;
|
|
323
|
+
result.push(formattedLine);
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
const formattedLine = colored
|
|
327
|
+
? ` ${c.gray(lineNumStr)} ${c.gray('│')} ${c.dim(content)}`
|
|
328
|
+
: ` ${lineNumStr} | ${content}`;
|
|
329
|
+
result.push(formattedLine);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Format a single finding as a box
|
|
336
|
+
*
|
|
337
|
+
* @param finding - Finding to format
|
|
338
|
+
* @param context - Terminal context
|
|
339
|
+
* @param diffFiles - Diff files for code snippet extraction
|
|
340
|
+
* @param boxWidth - Width of the box
|
|
341
|
+
* @returns Formatted finding box as string
|
|
342
|
+
*/
|
|
343
|
+
export function formatFindingBox(finding, context, diffFiles, boxWidth = DEFAULT_BOX_WIDTH) {
|
|
344
|
+
const colored = context.colored;
|
|
345
|
+
const useUnicode = context.useUnicode;
|
|
346
|
+
const chars = getBoxChars(useUnicode);
|
|
347
|
+
const c = createColorizer(colored);
|
|
348
|
+
const lines = [];
|
|
349
|
+
// Get severity color and indicator
|
|
350
|
+
const severityIndicator = getSeverityIndicator(finding.severity, useUnicode);
|
|
351
|
+
const severityLabel = finding.severity.toUpperCase();
|
|
352
|
+
// Content width inside the box (accounting for borders and padding)
|
|
353
|
+
const contentWidth = boxWidth - 4; // 2 for borders, 2 for padding
|
|
354
|
+
// ─────────────── Header ───────────────
|
|
355
|
+
// Format: ┌─ file.ts:42 ──────────────────────── ERROR ─┐
|
|
356
|
+
const location = finding.line ? `${finding.file}:${finding.line}` : finding.file;
|
|
357
|
+
const locationFormatted = colored ? c.cyan(location) : location;
|
|
358
|
+
const severityFormatted = colored
|
|
359
|
+
? colorize(` ${severityIndicator} ${severityLabel} `, getSeverityColor(finding.severity), colored)
|
|
360
|
+
: ` ${severityIndicator} ${severityLabel} `;
|
|
361
|
+
// Calculate header spacing
|
|
362
|
+
const headerTextLen = visibleLength(location) + visibleLength(severityFormatted) + 4; // 4 for " ─ " and " ─"
|
|
363
|
+
const headerPadLen = Math.max(0, boxWidth - 2 - headerTextLen);
|
|
364
|
+
const topLine = `${chars.topLeft}${chars.horizontal} ${locationFormatted} ${drawHorizontalLine(headerPadLen, chars.horizontal)}${severityFormatted}${chars.horizontal}${chars.topRight}`;
|
|
365
|
+
lines.push(topLine);
|
|
366
|
+
// ─────────────── Message ───────────────
|
|
367
|
+
const messageLines = wrapText(finding.message, contentWidth);
|
|
368
|
+
for (const msgLine of messageLines) {
|
|
369
|
+
lines.push(`${chars.vertical} ${padToWidth(msgLine, contentWidth)} ${chars.vertical}`);
|
|
370
|
+
}
|
|
371
|
+
// Empty line after message
|
|
372
|
+
lines.push(`${chars.vertical} ${' '.repeat(contentWidth)} ${chars.vertical}`);
|
|
373
|
+
// ─────────────── Code Snippet ───────────────
|
|
374
|
+
if (finding.line && diffFiles) {
|
|
375
|
+
const file = diffFiles.find((f) => f.path === finding.file);
|
|
376
|
+
if (file?.patch) {
|
|
377
|
+
const snippet = extractCodeSnippet(file.patch, finding.line, CONTEXT_LINES, finding.file);
|
|
378
|
+
if (snippet) {
|
|
379
|
+
const snippetLines = formatCodeSnippet(snippet, colored, boxWidth);
|
|
380
|
+
for (const snippetLine of snippetLines) {
|
|
381
|
+
// Pad snippet lines to fit in box
|
|
382
|
+
const padding = contentWidth - visibleLength(snippetLine);
|
|
383
|
+
lines.push(`${chars.vertical} ${snippetLine}${' '.repeat(Math.max(0, padding))} ${chars.vertical}`);
|
|
384
|
+
}
|
|
385
|
+
// Empty line after code
|
|
386
|
+
lines.push(`${chars.vertical} ${' '.repeat(contentWidth)} ${chars.vertical}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// ─────────────── Suggestion ───────────────
|
|
391
|
+
if (finding.suggestion) {
|
|
392
|
+
const suggestionPrefix = '💡 ';
|
|
393
|
+
const suggestionContent = finding.suggestion;
|
|
394
|
+
const suggestionLines = wrapText(suggestionContent, contentWidth - 3);
|
|
395
|
+
for (let i = 0; i < suggestionLines.length; i++) {
|
|
396
|
+
const prefix = i === 0 ? suggestionPrefix : ' ';
|
|
397
|
+
const line = `${prefix}${suggestionLines[i] ?? ''}`;
|
|
398
|
+
const formattedLine = colored ? c.green(line) : line;
|
|
399
|
+
const padding = contentWidth - visibleLength(line);
|
|
400
|
+
lines.push(`${chars.vertical} ${formattedLine}${' '.repeat(Math.max(0, padding))} ${chars.vertical}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// ─────────────── Footer ───────────────
|
|
404
|
+
// Show source agent and rule ID if available
|
|
405
|
+
const footerParts = [];
|
|
406
|
+
if (finding.sourceAgent) {
|
|
407
|
+
footerParts.push(finding.sourceAgent);
|
|
408
|
+
}
|
|
409
|
+
if (finding.ruleId) {
|
|
410
|
+
footerParts.push(`[${finding.ruleId}]`);
|
|
411
|
+
}
|
|
412
|
+
if (footerParts.length > 0) {
|
|
413
|
+
const footerText = footerParts.join(' ');
|
|
414
|
+
const footerFormatted = colored ? c.gray(footerText) : footerText;
|
|
415
|
+
const footerLine = padToWidth(footerFormatted, contentWidth);
|
|
416
|
+
lines.push(`${chars.vertical} ${footerLine} ${chars.vertical}`);
|
|
417
|
+
}
|
|
418
|
+
// Bottom border
|
|
419
|
+
const bottomLine = `${chars.bottomLeft}${drawHorizontalLine(boxWidth - 2, chars.horizontal)}${chars.bottomRight}`;
|
|
420
|
+
lines.push(bottomLine);
|
|
421
|
+
return lines.join('\n');
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Get ANSI color code for severity
|
|
425
|
+
*/
|
|
426
|
+
function getSeverityColor(severity) {
|
|
427
|
+
switch (severity) {
|
|
428
|
+
case 'error':
|
|
429
|
+
return ANSI.red;
|
|
430
|
+
case 'warning':
|
|
431
|
+
return ANSI.yellow;
|
|
432
|
+
case 'info':
|
|
433
|
+
return ANSI.blue;
|
|
434
|
+
default:
|
|
435
|
+
return '';
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Format all findings as a list of boxes
|
|
440
|
+
*
|
|
441
|
+
* @param findings - Findings to format
|
|
442
|
+
* @param context - Terminal context
|
|
443
|
+
* @param diffFiles - Diff files for code snippet extraction
|
|
444
|
+
* @returns Formatted findings list
|
|
445
|
+
*/
|
|
446
|
+
export function formatFindingsList(findings, context, diffFiles) {
|
|
447
|
+
if (findings.length === 0) {
|
|
448
|
+
return '';
|
|
449
|
+
}
|
|
450
|
+
const boxes = findings.map((finding) => formatFindingBox(finding, context, diffFiles));
|
|
451
|
+
return boxes.join('\n\n');
|
|
452
|
+
}
|
|
453
|
+
// =============================================================================
|
|
454
|
+
// Summary Generation
|
|
455
|
+
// =============================================================================
|
|
456
|
+
/**
|
|
457
|
+
* Generate summary section with counts
|
|
458
|
+
*
|
|
459
|
+
* @param findings - Complete findings
|
|
460
|
+
* @param stats - Review statistics
|
|
461
|
+
* @param context - Terminal context
|
|
462
|
+
* @returns Formatted summary string
|
|
463
|
+
*/
|
|
464
|
+
export function generateSummary(findings, stats, context) {
|
|
465
|
+
const c = createColorizer(context.colored);
|
|
466
|
+
const counts = countBySeverity(findings);
|
|
467
|
+
const lines = [];
|
|
468
|
+
// Header
|
|
469
|
+
lines.push('');
|
|
470
|
+
lines.push(c.bold('📊 SUMMARY'));
|
|
471
|
+
lines.push('');
|
|
472
|
+
// Counts section
|
|
473
|
+
const errorLabel = counts.error === 0
|
|
474
|
+
? c.green(` Errors: ${counts.error}`)
|
|
475
|
+
: c.red(` Errors: ${counts.error}`);
|
|
476
|
+
const warningLabel = counts.warning === 0
|
|
477
|
+
? c.green(` Warnings: ${counts.warning}`)
|
|
478
|
+
: c.yellow(` Warnings: ${counts.warning}`);
|
|
479
|
+
const infoLabel = ` Suggestions: ${counts.info}`;
|
|
480
|
+
lines.push(errorLabel);
|
|
481
|
+
lines.push(warningLabel);
|
|
482
|
+
lines.push(context.colored ? c.blue(infoLabel) : infoLabel);
|
|
483
|
+
lines.push('');
|
|
484
|
+
// Stats section
|
|
485
|
+
lines.push(` Files: ${stats.filesAnalyzed} analyzed`);
|
|
486
|
+
if (context.showCost && stats.estimatedCostUsd > 0) {
|
|
487
|
+
// Clamp cost to non-negative (FR-REL-002)
|
|
488
|
+
const cost = Math.max(0, stats.estimatedCostUsd);
|
|
489
|
+
lines.push(` Cost: $${cost.toFixed(4)} (estimated)`);
|
|
490
|
+
}
|
|
491
|
+
lines.push(` Time: ${formatDuration(stats.executionTimeMs)}`);
|
|
492
|
+
lines.push('');
|
|
493
|
+
return lines.join('\n');
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Generate terminal summary (legacy interface)
|
|
497
|
+
*/
|
|
498
|
+
export function generateTerminalSummary(findings, partialFindings, executionTimeMs, estimatedCostUsd) {
|
|
499
|
+
// Create minimal context for summary generation
|
|
500
|
+
const context = {
|
|
501
|
+
colored: true,
|
|
502
|
+
useUnicode: supportsUnicode(),
|
|
503
|
+
verbose: false,
|
|
504
|
+
quiet: false,
|
|
505
|
+
format: 'pretty',
|
|
506
|
+
showProgress: true,
|
|
507
|
+
showCost: true,
|
|
508
|
+
};
|
|
509
|
+
const allFindings = [...findings, ...partialFindings];
|
|
510
|
+
return generateSummary(allFindings, {
|
|
511
|
+
filesAnalyzed: 0,
|
|
512
|
+
linesChanged: 0,
|
|
513
|
+
executionTimeMs,
|
|
514
|
+
estimatedCostUsd,
|
|
515
|
+
}, context);
|
|
516
|
+
}
|
|
517
|
+
// =============================================================================
|
|
518
|
+
// Header Generation
|
|
519
|
+
// =============================================================================
|
|
520
|
+
/**
|
|
521
|
+
* Generate header section
|
|
522
|
+
*
|
|
523
|
+
* @param context - Terminal context
|
|
524
|
+
* @param stats - Stats for header
|
|
525
|
+
* @returns Formatted header string
|
|
526
|
+
*/
|
|
527
|
+
export function generateHeader(context, stats) {
|
|
528
|
+
const c = createColorizer(context.colored);
|
|
529
|
+
const lines = [];
|
|
530
|
+
const version = context.version ?? 'unknown';
|
|
531
|
+
lines.push('');
|
|
532
|
+
lines.push(c.bold(`🔍 odd-ai-reviewers v${version}`));
|
|
533
|
+
lines.push(` Analyzing ${stats.fileCount} files (${stats.lineCount} lines changed)`);
|
|
534
|
+
if (context.configSource) {
|
|
535
|
+
const configDisplay = context.configSource.source === 'zero-config'
|
|
536
|
+
? '(zero-config defaults)'
|
|
537
|
+
: (context.configSource.path ?? '.ai-review.yml');
|
|
538
|
+
lines.push(` Config: ${configDisplay} ✓`);
|
|
539
|
+
}
|
|
540
|
+
if (context.baseRef) {
|
|
541
|
+
const baseSource = context.baseSource ?? 'auto-detected';
|
|
542
|
+
lines.push(` Base: ${context.baseRef} (${baseSource})`);
|
|
543
|
+
}
|
|
544
|
+
lines.push('');
|
|
545
|
+
return lines.join('\n');
|
|
546
|
+
}
|
|
547
|
+
// =============================================================================
|
|
548
|
+
// Output Format Functions
|
|
549
|
+
// =============================================================================
|
|
550
|
+
/**
|
|
551
|
+
* Generate JSON output
|
|
552
|
+
*
|
|
553
|
+
* @param findings - Complete findings
|
|
554
|
+
* @param partialFindings - Partial findings from failed agents
|
|
555
|
+
* @param context - Terminal context
|
|
556
|
+
* @param diffFiles - Diff files for stats
|
|
557
|
+
* @returns JSON string (not pretty-printed)
|
|
558
|
+
*/
|
|
559
|
+
export function generateJsonOutput(findings, partialFindings, context, diffFiles) {
|
|
560
|
+
const counts = countBySeverity(findings);
|
|
561
|
+
const totalLines = diffFiles.reduce((sum, f) => sum + f.additions + f.deletions, 0);
|
|
562
|
+
const output = {
|
|
563
|
+
schema_version: JSON_SCHEMA_VERSION,
|
|
564
|
+
version: context.version ?? '0.0.0',
|
|
565
|
+
timestamp: new Date().toISOString(),
|
|
566
|
+
summary: {
|
|
567
|
+
errorCount: counts.error,
|
|
568
|
+
warningCount: counts.warning,
|
|
569
|
+
infoCount: counts.info,
|
|
570
|
+
filesAnalyzed: diffFiles.length,
|
|
571
|
+
linesChanged: totalLines,
|
|
572
|
+
executionTimeMs: context.executionTimeMs ?? 0,
|
|
573
|
+
// Clamp to non-negative (FR-REL-002)
|
|
574
|
+
estimatedCostUsd: Math.max(0, context.estimatedCostUsd ?? 0),
|
|
575
|
+
},
|
|
576
|
+
findings,
|
|
577
|
+
partialFindings,
|
|
578
|
+
passes: [], // Would need pass information from execution context
|
|
579
|
+
config: context.configSource ?? { source: 'file' },
|
|
580
|
+
};
|
|
581
|
+
// Single-line JSON output (no pretty-printing)
|
|
582
|
+
return JSON.stringify(output);
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Map severity to SARIF level
|
|
586
|
+
*/
|
|
587
|
+
function mapSeverityToSarifLevel(severity) {
|
|
588
|
+
switch (severity) {
|
|
589
|
+
case 'error':
|
|
590
|
+
return 'error';
|
|
591
|
+
case 'warning':
|
|
592
|
+
return 'warning';
|
|
593
|
+
case 'info':
|
|
594
|
+
return 'note';
|
|
595
|
+
default:
|
|
596
|
+
return 'note';
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Generate SARIF 2.1.0 output
|
|
601
|
+
*
|
|
602
|
+
* @param findings - Complete findings
|
|
603
|
+
* @param context - Terminal context
|
|
604
|
+
* @returns SARIF JSON string
|
|
605
|
+
*/
|
|
606
|
+
export function generateSarifOutput(findings, context) {
|
|
607
|
+
const results = findings.map((finding) => {
|
|
608
|
+
const result = {
|
|
609
|
+
ruleId: finding.ruleId ?? finding.sourceAgent,
|
|
610
|
+
level: mapSeverityToSarifLevel(finding.severity),
|
|
611
|
+
message: { text: finding.message },
|
|
612
|
+
locations: [
|
|
613
|
+
{
|
|
614
|
+
physicalLocation: {
|
|
615
|
+
artifactLocation: { uri: finding.file },
|
|
616
|
+
region: {
|
|
617
|
+
startLine: finding.line ?? 1,
|
|
618
|
+
...(finding.endLine ? { endLine: finding.endLine } : {}),
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
],
|
|
623
|
+
properties: {
|
|
624
|
+
sourceAgent: finding.sourceAgent,
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
// Add fix if suggestion exists
|
|
628
|
+
if (finding.suggestion) {
|
|
629
|
+
result.fixes = [
|
|
630
|
+
{
|
|
631
|
+
description: { text: finding.suggestion },
|
|
632
|
+
},
|
|
633
|
+
];
|
|
634
|
+
}
|
|
635
|
+
return result;
|
|
636
|
+
});
|
|
637
|
+
const output = {
|
|
638
|
+
$schema: SARIF_SCHEMA_URL,
|
|
639
|
+
version: '2.1.0',
|
|
640
|
+
runs: [
|
|
641
|
+
{
|
|
642
|
+
tool: {
|
|
643
|
+
driver: {
|
|
644
|
+
name: TOOL_NAME,
|
|
645
|
+
version: context.version ?? '0.0.0',
|
|
646
|
+
informationUri: TOOL_INFO_URI,
|
|
647
|
+
rules: [], // Intentionally empty - AI agents don't have static rule IDs
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
results,
|
|
651
|
+
},
|
|
652
|
+
],
|
|
653
|
+
};
|
|
654
|
+
return JSON.stringify(output);
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Generate quiet mode output (errors only)
|
|
658
|
+
*
|
|
659
|
+
* @param findings - All findings
|
|
660
|
+
* @returns Minimal output string
|
|
661
|
+
*/
|
|
662
|
+
export function generateQuietOutput(findings) {
|
|
663
|
+
const errorCount = findings.filter((f) => f.severity === 'error').length;
|
|
664
|
+
if (errorCount === 0) {
|
|
665
|
+
return 'No errors found\n';
|
|
666
|
+
}
|
|
667
|
+
return `${errorCount} error${errorCount === 1 ? '' : 's'} found\n`;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Generate verbose mode output additions
|
|
671
|
+
*
|
|
672
|
+
* @param context - Terminal context with verbose info
|
|
673
|
+
* @param diffFiles - Diff files for details
|
|
674
|
+
* @returns Verbose additions string
|
|
675
|
+
*/
|
|
676
|
+
export function generateVerboseOutput(context, diffFiles) {
|
|
677
|
+
const c = createColorizer(context.colored);
|
|
678
|
+
const lines = [];
|
|
679
|
+
lines.push('');
|
|
680
|
+
lines.push(c.gray('─── Verbose Details ───'));
|
|
681
|
+
lines.push('');
|
|
682
|
+
// Git context details
|
|
683
|
+
if (context.baseRef) {
|
|
684
|
+
lines.push(c.gray(`Git base: ${context.baseRef}`));
|
|
685
|
+
}
|
|
686
|
+
// Config details
|
|
687
|
+
if (context.configSource) {
|
|
688
|
+
lines.push(c.gray(`Config: ${JSON.stringify(context.configSource)}`));
|
|
689
|
+
}
|
|
690
|
+
// File breakdown
|
|
691
|
+
lines.push(c.gray(''));
|
|
692
|
+
lines.push(c.gray('Files analyzed:'));
|
|
693
|
+
for (const file of diffFiles.slice(0, 10)) {
|
|
694
|
+
// Limit to first 10
|
|
695
|
+
lines.push(c.gray(` ${file.path} (+${file.additions}/-${file.deletions})`));
|
|
696
|
+
}
|
|
697
|
+
if (diffFiles.length > 10) {
|
|
698
|
+
lines.push(c.gray(` ... and ${diffFiles.length - 10} more`));
|
|
699
|
+
}
|
|
700
|
+
// Timing breakdown
|
|
701
|
+
if (context.executionTimeMs) {
|
|
702
|
+
lines.push(c.gray(''));
|
|
703
|
+
lines.push(c.gray(`Total execution time: ${formatDuration(context.executionTimeMs)}`));
|
|
704
|
+
}
|
|
705
|
+
lines.push('');
|
|
706
|
+
return lines.join('\n');
|
|
707
|
+
}
|
|
708
|
+
// =============================================================================
|
|
709
|
+
// Default Context
|
|
710
|
+
// =============================================================================
|
|
711
|
+
/**
|
|
712
|
+
* Create default terminal context
|
|
713
|
+
*/
|
|
714
|
+
export function createDefaultContext() {
|
|
715
|
+
return {
|
|
716
|
+
colored: true,
|
|
717
|
+
useUnicode: supportsUnicode(),
|
|
718
|
+
verbose: false,
|
|
719
|
+
quiet: false,
|
|
720
|
+
format: 'pretty',
|
|
721
|
+
showProgress: true,
|
|
722
|
+
showCost: true,
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
// =============================================================================
|
|
726
|
+
// Main Reporter Function
|
|
727
|
+
// =============================================================================
|
|
728
|
+
/**
|
|
729
|
+
* Report findings to terminal.
|
|
730
|
+
*
|
|
731
|
+
* Follows the same pipeline as GitHub/ADO reporters:
|
|
732
|
+
* 1. Canonicalize diff files
|
|
733
|
+
* 2. Build line resolver
|
|
734
|
+
* 3. Normalize findings
|
|
735
|
+
* 4. Deduplicate
|
|
736
|
+
* 5. Sort
|
|
737
|
+
* 6. Format and output
|
|
738
|
+
*
|
|
739
|
+
* @param findings - Complete findings
|
|
740
|
+
* @param partialFindings - Findings from interrupted agents
|
|
741
|
+
* @param context - Terminal context (colors, format, etc.)
|
|
742
|
+
* @param config - Review configuration
|
|
743
|
+
* @param diffFiles - Diff files for context
|
|
744
|
+
* @returns Report result
|
|
745
|
+
*/
|
|
746
|
+
export async function reportToTerminal(findings, partialFindings, context, config, diffFiles) {
|
|
747
|
+
try {
|
|
748
|
+
// 1. Canonicalize diff files
|
|
749
|
+
const canonicalFiles = canonicalizeDiffFiles(diffFiles);
|
|
750
|
+
// 2. Build line resolver
|
|
751
|
+
const resolver = buildLineResolver(canonicalFiles);
|
|
752
|
+
// 3. Normalize findings
|
|
753
|
+
const normalizedComplete = normalizeFindingsForDiff(findings, resolver);
|
|
754
|
+
const normalizedPartial = normalizeFindingsForDiff(partialFindings, resolver);
|
|
755
|
+
// 4. Deduplicate
|
|
756
|
+
const dedupedComplete = deduplicateFindings(normalizedComplete.findings);
|
|
757
|
+
const dedupedPartial = deduplicatePartialFindings(normalizedPartial.findings);
|
|
758
|
+
// 5. Sort
|
|
759
|
+
const sortedComplete = sortFindings(dedupedComplete);
|
|
760
|
+
const sortedPartial = sortFindings(dedupedPartial);
|
|
761
|
+
// For quiet mode, filter to errors only
|
|
762
|
+
const findingsToShow = context.quiet
|
|
763
|
+
? sortedComplete.filter((f) => f.severity === 'error')
|
|
764
|
+
: sortedComplete;
|
|
765
|
+
// Calculate stats
|
|
766
|
+
const totalLines = canonicalFiles.reduce((sum, f) => sum + f.additions + f.deletions, 0);
|
|
767
|
+
const stats = {
|
|
768
|
+
fileCount: canonicalFiles.length,
|
|
769
|
+
lineCount: totalLines,
|
|
770
|
+
filesAnalyzed: canonicalFiles.length,
|
|
771
|
+
linesChanged: totalLines,
|
|
772
|
+
executionTimeMs: context.executionTimeMs ?? 0,
|
|
773
|
+
estimatedCostUsd: context.estimatedCostUsd ?? 0,
|
|
774
|
+
};
|
|
775
|
+
// 6. Format and output based on format type
|
|
776
|
+
let output;
|
|
777
|
+
switch (context.format) {
|
|
778
|
+
case 'json':
|
|
779
|
+
output = generateJsonOutput(sortedComplete, sortedPartial, context, canonicalFiles);
|
|
780
|
+
break;
|
|
781
|
+
case 'sarif':
|
|
782
|
+
output = generateSarifOutput(sortedComplete, context);
|
|
783
|
+
break;
|
|
784
|
+
case 'pretty':
|
|
785
|
+
default: {
|
|
786
|
+
const parts = [];
|
|
787
|
+
// Quiet mode: minimal output
|
|
788
|
+
if (context.quiet) {
|
|
789
|
+
output = generateQuietOutput(sortedComplete);
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
// Header
|
|
793
|
+
parts.push(generateHeader(context, stats));
|
|
794
|
+
// Verbose details (before findings)
|
|
795
|
+
if (context.verbose) {
|
|
796
|
+
parts.push(generateVerboseOutput(context, canonicalFiles));
|
|
797
|
+
}
|
|
798
|
+
// Findings
|
|
799
|
+
if (findingsToShow.length > 0) {
|
|
800
|
+
parts.push(formatFindingsList(findingsToShow, context, canonicalFiles));
|
|
801
|
+
}
|
|
802
|
+
// Partial findings section
|
|
803
|
+
if (sortedPartial.length > 0 && !context.quiet) {
|
|
804
|
+
parts.push('');
|
|
805
|
+
parts.push(createColorizer(context.colored).yellow('⚠️ Partial Findings (from failed agents):'));
|
|
806
|
+
parts.push('');
|
|
807
|
+
parts.push(formatFindingsList(sortedPartial, context, canonicalFiles));
|
|
808
|
+
}
|
|
809
|
+
// Summary
|
|
810
|
+
parts.push(generateSummary(sortedComplete, stats, context));
|
|
811
|
+
output = parts.join('\n');
|
|
812
|
+
}
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
// Write to stdout
|
|
817
|
+
process.stdout.write(output + '\n');
|
|
818
|
+
return {
|
|
819
|
+
success: true,
|
|
820
|
+
findingsCount: sortedComplete.length,
|
|
821
|
+
partialFindingsCount: sortedPartial.length,
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
826
|
+
return {
|
|
827
|
+
success: false,
|
|
828
|
+
findingsCount: 0,
|
|
829
|
+
partialFindingsCount: 0,
|
|
830
|
+
error: errorMessage,
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Format a single finding for terminal display (legacy interface)
|
|
836
|
+
*/
|
|
837
|
+
export function formatFindingForTerminal(finding, context) {
|
|
838
|
+
return formatFindingBox(finding, context);
|
|
839
|
+
}
|
|
840
|
+
//# sourceMappingURL=terminal.js.map
|