Skip to content

Commit f1bc35b

Browse files
Merge pull request #103 from CodeMonkeyCybersecurity/refactor/chat-archive-cross-platform
fix(inspect): harden Docker discovery with batched inspect, fix find, add tests
2 parents fe4da19 + 09df38f commit f1bc35b

77 files changed

Lines changed: 6206 additions & 640 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.githooks/pre-commit

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env bash
2+
# Pre-commit hook: fast gate (build + unit tests + race detection).
3+
# Full CI (integration, e2e, coverage thresholds) runs in CI pipeline.
4+
# Per DORA research: pre-commit should be <10s for fast feedback loops.
5+
6+
set -euo pipefail
7+
8+
echo "==> Pre-commit: build check"
9+
go build -o /dev/null ./cmd/
10+
11+
echo "==> Pre-commit: unit tests with race detection"
12+
go test -race ./pkg/chatarchive/... ./internal/chatarchivecmd/...
13+
14+
echo "Pre-commit checks passed."

.github/hooks/pre-commit

100644100755
File mode changed.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: Chat Archive Quality
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'pkg/chatarchive/**'
7+
- 'internal/chatarchivecmd/**'
8+
- 'cmd/create/chat_archive.go'
9+
- 'cmd/backup/chats.go'
10+
- 'test/e2e/**'
11+
- 'scripts/chatarchive-ci.sh'
12+
- 'package.json'
13+
- '.github/hooks/pre-commit'
14+
- '.github/hooks/setup-hooks.sh'
15+
- 'scripts/install-git-hooks.sh'
16+
- '.github/workflows/chatarchive-quality.yml'
17+
push:
18+
branches:
19+
- main
20+
- develop
21+
paths:
22+
- 'pkg/chatarchive/**'
23+
- 'internal/chatarchivecmd/**'
24+
- 'cmd/create/chat_archive.go'
25+
- 'cmd/backup/chats.go'
26+
- 'test/e2e/**'
27+
- 'scripts/chatarchive-ci.sh'
28+
- 'package.json'
29+
- '.github/hooks/pre-commit'
30+
- '.github/hooks/setup-hooks.sh'
31+
- 'scripts/install-git-hooks.sh'
32+
- '.github/workflows/chatarchive-quality.yml'
33+
34+
jobs:
35+
chatarchive-ci:
36+
name: Chat Archive CI (${{ matrix.os }})
37+
runs-on: ${{ matrix.os }}
38+
strategy:
39+
fail-fast: false
40+
matrix:
41+
os: [ubuntu-latest, macos-latest, windows-latest]
42+
43+
steps:
44+
- name: Checkout
45+
uses: actions/checkout@v4
46+
47+
- name: Set up Go
48+
uses: actions/setup-go@v5
49+
with:
50+
go-version-file: 'go.mod'
51+
52+
- name: Set up Node
53+
uses: actions/setup-node@v4
54+
with:
55+
node-version: '20'
56+
57+
- name: Download Go modules
58+
run: go mod download
59+
shell: bash
60+
61+
- name: Run chat archive CI
62+
run: npm run ci
63+
shell: bash
64+
65+
- name: Upload verification summary
66+
if: always()
67+
uses: actions/upload-artifact@v4
68+
with:
69+
name: chatarchive-summary-${{ matrix.os }}
70+
path: outputs/chatarchive-ci/
71+
72+
- name: Publish job summary
73+
if: always()
74+
shell: bash
75+
run: |
76+
if [[ -f outputs/chatarchive-ci/summary.txt ]]; then
77+
cat outputs/chatarchive-ci/summary.txt >> "$GITHUB_STEP_SUMMARY"
78+
fi

.github/workflows/ci.yml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ PY
161161
run: scripts/ci/preflight.sh
162162

163163
- name: Install golangci-lint
164-
run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.0.0
164+
run: bash scripts/ci/install-golangci-lint.sh
165165

166166
- name: Run lint lane
167167
env:
@@ -245,6 +245,10 @@ PY
245245
run: |
246246
test -f outputs/ci/unit/report.json && cat outputs/ci/unit/report.json || true
247247
248+
- name: Alert on unit lane report
249+
if: always()
250+
run: python3 scripts/ci/report-alert.py ci-unit outputs/ci/unit/report.json
251+
248252
ci-deps-unit:
249253
name: ci-deps-unit
250254
runs-on: ubuntu-latest
@@ -295,6 +299,10 @@ PY
295299
run: |
296300
test -f outputs/ci/deps-unit/report.json && cat outputs/ci/deps-unit/report.json || true
297301
302+
- name: Alert on dependency-focused unit lane report
303+
if: always()
304+
run: python3 scripts/ci/report-alert.py ci-deps-unit outputs/ci/deps-unit/report.json
305+
298306
ci-integration:
299307
name: ci-integration
300308
runs-on: ubuntu-latest
@@ -344,6 +352,10 @@ PY
344352
run: |
345353
test -f outputs/ci/integration/report.json && cat outputs/ci/integration/report.json || true
346354
355+
- name: Alert on integration lane report
356+
if: always()
357+
run: python3 scripts/ci/report-alert.py ci-integration outputs/ci/integration/report.json
358+
347359
ci-e2e-smoke:
348360
name: ci-e2e-smoke
349361
runs-on: ubuntu-latest
@@ -391,6 +403,10 @@ PY
391403
run: |
392404
test -f outputs/ci/e2e-smoke/report.json && cat outputs/ci/e2e-smoke/report.json || true
393405
406+
- name: Alert on e2e smoke lane report
407+
if: always()
408+
run: python3 scripts/ci/report-alert.py ci-e2e-smoke outputs/ci/e2e-smoke/report.json
409+
394410
ci-fuzz:
395411
name: ci-fuzz
396412
runs-on: ubuntu-latest
@@ -438,6 +454,10 @@ PY
438454
run: |
439455
test -f outputs/ci/fuzz/report.json && cat outputs/ci/fuzz/report.json || true
440456
457+
- name: Alert on fuzz lane report
458+
if: always()
459+
run: python3 scripts/ci/report-alert.py ci-fuzz outputs/ci/fuzz/report.json
460+
441461
ci-e2e-full:
442462
name: ci-e2e-full
443463
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
@@ -485,3 +505,7 @@ PY
485505
if: always()
486506
run: |
487507
test -f outputs/ci/e2e-full/report.json && cat outputs/ci/e2e-full/report.json || true
508+
509+
- name: Alert on e2e full lane report
510+
if: always()
511+
run: python3 scripts/ci/report-alert.py ci-e2e-full outputs/ci/e2e-full/report.json

.pre-commit-config.yaml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ repos:
4747
# Local hooks for custom checks
4848
- repo: local
4949
hooks:
50-
# Enforce local/CI parity lane via mage-compatible entrypoint
50+
# Enforce local/CI parity lane via npm wrapper
5151
- id: ci-debug-parity
52-
name: CI debug parity gate (magew ci:debug)
53-
entry: ./magew ci:debug
52+
name: CI debug parity gate (npm run ci:debug)
53+
entry: npm run ci:debug --silent
5454
language: system
5555
pass_filenames: false
5656
require_serial: true
@@ -80,13 +80,13 @@ repos:
8080
pass_filenames: false
8181
description: Ensures code compiles successfully
8282

83-
# Verify E2E tests have build tags
84-
- id: verify-e2e-build-tags
85-
name: Verify E2E build tags
86-
entry: bash -c 'for f in test/e2e/*_test.go; do head -1 "$f" | grep -q "//go:build e2e" || { echo "ERROR: $f missing //go:build e2e tag"; exit 1; }; done'
83+
# Verify environment-dependent tests are build-tagged
84+
- id: verify-test-build-tags
85+
name: Verify test build tags
86+
entry: bash scripts/ci/check-test-tags.sh
8787
language: system
8888
pass_filenames: false
89-
description: Ensures all E2E tests have proper build tags
89+
description: Ensures environment-dependent tests keep their explicit build tags
9090

9191
# Check for deprecated benchmark pattern
9292
- id: check-benchmark-pattern

cmd/create/chat_archive.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// cmd/create/chat_archive.go
2+
//
3+
// Thin orchestration layer for chat-archive. Business logic lives in
4+
// pkg/chatarchive/ per the cmd/ vs pkg/ enforcement rule.
5+
6+
package create
7+
8+
import (
9+
"github.com/CodeMonkeyCybersecurity/eos/internal/chatarchivecmd"
10+
eos "github.com/CodeMonkeyCybersecurity/eos/pkg/eos_cli"
11+
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_io"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
// CreateChatArchiveCmd copies and deduplicates chat transcripts.
16+
var CreateChatArchiveCmd = &cobra.Command{
17+
Use: "chat-archive",
18+
Short: "Copy and deduplicate chat transcripts into a local archive",
19+
Long: `Find transcript-like files (jsonl/json/html), copy unique files into one archive,
20+
and write an index manifest with duplicate mappings.
21+
22+
Examples:
23+
eos create chat-archive
24+
eos create chat-archive --source ~/.claude --source ~/dev
25+
eos create chat-archive --exclude conversation-api --exclude .cache
26+
eos create chat-archive --dry-run`,
27+
RunE: eos.Wrap(runCreateChatArchive),
28+
}
29+
30+
func init() {
31+
CreateCmd.AddCommand(CreateChatArchiveCmd)
32+
chatarchivecmd.BindFlags(CreateChatArchiveCmd)
33+
}
34+
35+
func runCreateChatArchive(rc *eos_io.RuntimeContext, cmd *cobra.Command, _ []string) error {
36+
return chatarchivecmd.Run(rc, cmd)
37+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
*Last Updated: 2026-03-21*
2+
3+
# pkg/inspect Follow-Up Issues
4+
5+
Issues discovered during adversarial review of `pkg/inspect/docker.go`.
6+
7+
## Issue 1: output.go / terraform_modular.go — 37 staticcheck warnings (P2)
8+
9+
**Problem**: `WriteString(fmt.Sprintf(...))` should be `fmt.Fprintf(...)` throughout output.go and terraform_modular.go.
10+
**Impact**: Performance (unnecessary string allocation) and lint noise.
11+
**Fix**: Replace all `tf.WriteString(fmt.Sprintf(...))` with `fmt.Fprintf(tf, ...)`.
12+
**Files**: `pkg/inspect/output.go`, `pkg/inspect/terraform_modular.go`
13+
**Effort**: ~30 min mechanical refactor
14+
15+
## Issue 2: services.go — unchecked filepath.Glob error (P2)
16+
17+
**Problem**: `pkg/inspect/services.go:381` ignores `filepath.Glob` error.
18+
**Impact**: Silent failure when glob patterns are invalid.
19+
**Fix**: Check and log the error.
20+
**Effort**: 5 min
21+
22+
## Issue 3: kvm.go — goconst violations (P3)
23+
24+
**Problem**: String constants `"active"`, `"UUID"` repeated without named constants.
25+
**Impact**: Violates P0 Rule #12 (no hardcoded values).
26+
**Fix**: Extract to constants in `kvm.go` or a `constants.go` file.
27+
**Effort**: 15 min
28+
29+
## Issue 4: Pre-existing lint issues across 30+ files on this branch (P1)
30+
31+
**Problem**: `npm run ci` fails due to 165 lint issues across the branch.
32+
**Impact**: Cannot merge until resolved.
33+
**Root cause**: Accumulated tech debt from many feature PRs merged without lint cleanup.
34+
**Fix**: Dedicated lint cleanup pass before PR merge.
35+
**Effort**: 2-4 hours
36+
37+
## Issue 5: Inspector lacks Docker SDK integration (P3)
38+
39+
**Problem**: All Docker operations use shell commands instead of the Docker SDK.
40+
**Impact**: Fragile parsing, no type safety, extra process spawns.
41+
**Fix**: Migrate to `github.com/docker/docker/client` SDK for container/image/network/volume operations.
42+
**Rationale**: CLAUDE.md P1 states "ALWAYS use Docker SDK" for container operations.
43+
**Effort**: 1-2 days
44+
45+
## Issue 6: Compose file search does not guard against TOCTOU (P3)
46+
47+
**Problem**: Between `os.Stat` size check and `os.ReadFile`, the file could be replaced.
48+
**Impact**: Theoretical DoS via race condition on symlink swap.
49+
**Fix**: Read file first, then check size of bytes read (simpler and race-free).
50+
**Effort**: 15 min

internal/chatarchivecmd/run.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package chatarchivecmd
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"strings"
7+
"time"
8+
9+
"github.com/CodeMonkeyCybersecurity/eos/pkg/chatarchive"
10+
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_io"
11+
"github.com/spf13/cobra"
12+
"github.com/uptrace/opentelemetry-go-extra/otelzap"
13+
"go.uber.org/zap"
14+
)
15+
16+
func BindFlags(cmd *cobra.Command) {
17+
cmd.Flags().StringSlice("source", chatarchive.DefaultSources(), "Source directories to scan")
18+
cmd.Flags().String("dest", chatarchive.DefaultDest(), "Destination archive directory")
19+
cmd.Flags().StringSlice("exclude", nil, "Path substrings to exclude from discovery (e.g. --exclude conversation-api)")
20+
cmd.Flags().Bool("dry-run", false, "Show what would be archived without copying files")
21+
}
22+
23+
func Run(rc *eos_io.RuntimeContext, cmd *cobra.Command) error {
24+
sources, _ := cmd.Flags().GetStringSlice("source")
25+
dest, _ := cmd.Flags().GetString("dest")
26+
excludes, _ := cmd.Flags().GetStringSlice("exclude")
27+
dryRun, _ := cmd.Flags().GetBool("dry-run")
28+
29+
result, err := chatarchive.Archive(rc, chatarchive.Options{
30+
Sources: sources,
31+
Dest: dest,
32+
Excludes: excludes,
33+
DryRun: dryRun,
34+
})
35+
if err != nil {
36+
return err
37+
}
38+
39+
logger := otelzap.Ctx(rc.Ctx)
40+
writeSummary(cmd.OutOrStdout(), result, dryRun, logger)
41+
logger.Info("Chat archive summary",
42+
zap.Int("sources_requested", result.SourcesRequested),
43+
zap.Int("sources_scanned", result.SourcesScanned),
44+
zap.Int("sources_missing", len(result.MissingSources)),
45+
zap.Int("skipped_symlinks", result.SkippedSymlinks),
46+
zap.Int("unreadable_entries", result.UnreadableEntries),
47+
zap.Int("unique_files", result.UniqueFiles),
48+
zap.Int("duplicates", result.Duplicates),
49+
zap.Int("already_archived", result.Skipped),
50+
zap.Int("empty_files", result.EmptyFiles),
51+
zap.Int("failures", result.FailureCount),
52+
zap.Duration("duration", result.Duration),
53+
zap.Bool("dry_run", dryRun))
54+
for _, failure := range result.Failures {
55+
logger.Warn("Chat archive file failure",
56+
zap.String("path", failure.Path),
57+
zap.String("stage", failure.Stage),
58+
zap.String("reason", failure.Reason))
59+
}
60+
61+
return nil
62+
}
63+
64+
func formatSummary(result *chatarchive.Result, dryRun bool) string {
65+
lines := []string{
66+
statusLine(dryRun),
67+
fmt.Sprintf("Sources scanned: %d/%d", result.SourcesScanned, result.SourcesRequested),
68+
fmt.Sprintf("Unique files: %d", result.UniqueFiles),
69+
fmt.Sprintf("Duplicates in this run: %d", result.Duplicates),
70+
fmt.Sprintf("Already archived: %d", result.Skipped),
71+
fmt.Sprintf("Empty files ignored: %d", result.EmptyFiles),
72+
fmt.Sprintf("File failures: %d", result.FailureCount),
73+
fmt.Sprintf("Unreadable entries skipped: %d", result.UnreadableEntries),
74+
fmt.Sprintf("Symlinks skipped: %d", result.SkippedSymlinks),
75+
fmt.Sprintf("Duration: %s", result.Duration.Round(10*time.Millisecond)),
76+
}
77+
78+
if result.ManifestPath != "" {
79+
lines = append(lines, fmt.Sprintf("Manifest: %s", result.ManifestPath))
80+
}
81+
if result.RecoveredManifestPath != "" {
82+
lines = append(lines, fmt.Sprintf("Recovered corrupt manifest: %s", result.RecoveredManifestPath))
83+
}
84+
if len(result.MissingSources) > 0 {
85+
lines = append(lines, fmt.Sprintf("Unavailable sources: %s", strings.Join(result.MissingSources, ", ")))
86+
}
87+
if result.FailureCount > len(result.Failures) {
88+
lines = append(lines, fmt.Sprintf("Additional failures not shown: %d", result.FailureCount-len(result.Failures)))
89+
}
90+
91+
return strings.Join(lines, "\n")
92+
}
93+
94+
func statusLine(dryRun bool) string {
95+
if dryRun {
96+
return "Dry run complete."
97+
}
98+
return "Archive complete."
99+
}
100+
101+
func writeSummary(w io.Writer, result *chatarchive.Result, dryRun bool, logger otelzap.LoggerWithCtx) {
102+
if _, err := fmt.Fprintln(w, formatSummary(result, dryRun)); err != nil {
103+
logger.Warn("Failed to write chat archive summary", zap.Error(err))
104+
}
105+
}

0 commit comments

Comments
 (0)