HAWK is a scheduled content sync service. Its current implementation focuses on Git-based sources, detecting file changes between two commits, and exporting the changed file pairs for downstream processing by another application.
At a high level, HAWK does this:
- Reads a YAML configuration file.
- Starts one worker per top-level config entry.
- Schedules each worker with cron.
- For each sync run, loads source handlers through an interface.
- For Git sources, fetches:
- the latest commit on a configured branch,
- a previous commit, currently hardcoded,
- the file-level diff between them.
- Exports only changed files to a shared volume in two trees:
OldNew
The exported content is intended to be consumed by another program, such as /app, for AI-based analysis or document comparison.
The Git pipeline is the most complete part of the project.
Implemented today:
- YAML config loading.
- Cron-triggered sync workers.
- Source abstraction with a
Sourceinterface. - Git authentication via mounted secret files.
- Latest commit snapshot generation.
- Previous commit snapshot generation.
- In-memory diff between commit snapshots.
- Export of changed file pairs into the shared volume.
Partially implemented or scaffolded:
- Confluence source type is only stubbed.
- Database integration is modeled in config structs but not yet used in code.
- Previous commit lookup is hardcoded for now instead of being loaded from state storage.
The program entry point is in main.go.
func main() {
fmt.Println("Initializing the hawk...")
pkg.Run()
}main() does only one thing: hand control to pkg.Run().
Responsibilities:
- Start the program.
- Delegate execution to the
pkgpackage.
Responsibilities:
- Define the main configuration structs.
- Read and unmarshal the YAML config file.
- Start one goroutine per top-level config item.
Responsibilities:
- Define the
Sourceinterface. - Construct the correct source handler from source config.
- Drive scheduled sync execution.
- Pass the shared volume path into source handlers.
Responsibilities:
- Implement the Git source.
- Authenticate with private Git repos.
- Build commit snapshots.
- Compare snapshots.
- Export changed files into the shared volume.
Responsibilities:
- Provide a skeleton implementation of a non-Git source.
- Conform to the same
Sourceinterface used by Git.
This file exists mainly to show how additional source types can be added under the same abstraction.
The YAML config is mapped to structs in pkg/run.go.
type ConfList []ConfigThis is the full YAML document. Each list item describes one independent sync job.
type Config struct {
Name string
Type string
ID string
Description string
Sync SyncConfig
}This is one logical sync workload.
Important fields:
Name: human-readable identifier.Type: workload type, such asdoc.ID: external or internal job ID.Description: free-form metadata.Sync: scheduling and source details.
type SyncConfig struct {
Enabled bool
Mode string
AgentType string `yaml:"agentType"`
SharedVolume sharedVolumeConfig `yaml:"sharedVolume"`
Schedule string
Sources []sourceConfig
Database databaseConfig
}This holds runtime behavior.
Important fields:
Enabled: turns the sync on or off.Mode: currentlylocal-agentis the implemented path.AgentType: metadata for downstream processing.SharedVolume.Path: root export location for changed files.Schedule: cron schedule.Sources: one or more input systems.Database: future persistence target.
type sourceConfig struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Git *GitCfg `yaml:"git,omitempty"`
Confluence *ConfluenceCfg `yaml:"confluence,omitempty"`
}This is a polymorphic source wrapper.
Only one concrete nested config should be set, based on Type.
Examples:
type: gitwithgit:populated.type: confluencewithconfluence:populated.
type GitCfg struct {
URL string `yaml:"url"`
Branch string `yaml:"branch"`
Dirs []string `yaml:"dirList"`
IgnoreDirs []string `yaml:"ignoreDirList"`
Credentials credentialsConfig `yaml:"credentials"`
}This struct controls Git sync behavior.
Important fields:
URL: repository URL.Branch: branch to inspect.Dirs: the only directory prefixes included in snapshots.IgnoreDirs: excluded prefixes, even if they sit underDirs.Credentials: location of secret-backed Git auth data.
type credentialsConfig struct {
Type string
Name string
Path string `yaml:"path"`
}This defines how credentials are resolved.
Current implementation expects:
Type = secretName = mounted secret folder or file namePath = base directory where the secret is mounted
- name: perfecto-doc
type: doc
id: perfecto-doc-12451
description: "this is a sample doc sync"
sync:
enabled: true
mode: local-agent
agentType: py-quant-ai
sharedVolume:
path: $HOME/Desktop/
schedule: "* * * * *"
sources:
- type: git
name: basic-demo
git:
url: "https://github.com/ImMnan/doc-test.git"
branch: main
dirList: ["content/Perfecto"]
ignoreDirList: ["content/Perfecto/rest-api"]
credentials:
type: secret
name: git-secret-token
path: $HOMEThe startup sequence is:
main()callspkg.Run().Run()callsinit_hawk().init_hawk()reads the YAML config file.Run()starts one goroutine perConfigitem.- Each goroutine calls
sync(cfg).
Simplified view:
main()
-> pkg.Run()
-> init_hawk()
-> for each config
-> go sync(config)
Each worker sets up a cron trigger.
Flow:
sync(c)verifies that sync is enabled.syncTrigger(c)creates a cron scheduler.- The cron job sends timestamps into a channel.
sync(c)loops over the trigger channel.- On each tick, all configured sources are processed.
This design decouples scheduling from source logic.
The Source interface is defined in pkg/sync.go:
type Source interface {
Validate() error
Fetch(sharedVolumePath string) error
}This is the contract every source type must satisfy.
Without an interface, sync() would need type-specific branching everywhere. That becomes messy as more sources are added.
With the interface:
sync()stays generic.- Source-specific logic lives in the source implementation.
- Adding a new source only requires:
- a new config type,
- a new handler,
- a branch in
newSource().
The interface is used in this sequence:
sync()iteratessyncCfg.Sources.newSource(sourceConfig)inspectssource.Type.- It returns a
Sourceimplementation. sync()calls:handler.Validate()handler.Fetch(syncCfg.SharedVolume.Path)
That means sync() does not need to know whether a handler is Git, Confluence, or another future source.
newSource() is the source factory.
Current behavior:
type: gitreturns agitSource.type: confluencereturns aconfluenceSource.- Any other type returns an error.
This pattern centralizes source creation and keeps the rest of the runtime code simpler.
The Git implementation lives in pkg/git.go.
type gitSource struct {
cfg GitCfg
sourceName string
}This is the concrete implementation of the Source interface for Git.
Why store both fields?
cfgcontains Git connection and filtering behavior.sourceNameretains the source identifier from YAML.
Even though the current export path no longer uses sourceName, it is still part of the source model and debug flow.
This checks only the minimum required config:
git.urlmust be present.git.branchmust be present.
The validation is intentionally light right now.
This is the main entry point for Git work.
It delegates to gitSync().
gitSync() is the coordinator for a Git source run.
Current sequence:
- Use a hardcoded previous commit SHA.
- Build a snapshot for the latest branch commit via
gitGetLatestCommit(). - Build a snapshot for the previous commit via
gitGetLastCommit(). - Diff both snapshots via
gitDiff(). - Export changed files via
writeChangedFilesFromCommits(). - Return a JSON-encoded diff result.
At the moment, previous-commit lookup is not integrated with a database or state store.
So this line is intentionally temporary:
lastCommitSHA := "87fd6b8d0f56e5f5bad2c887585e9288f2adae93"Later, that value can be replaced by a call to another function without rewriting the rest of the sync pipeline.
The snapshot structs are central to how diffing works.
type gitFileSnapshot struct {
Path string `json:"path"`
BlobSHA string `json:"blobSha"`
Mode string `json:"mode"`
}This represents one file inside one commit tree.
Why these fields matter:
Path: the logical file identity.BlobSHA: the file content identity.Mode: file mode metadata.
type gitCommitSnapshot struct {
RepoURL string `json:"repoUrl"`
Branch string `json:"branch"`
CommitSHA string `json:"commitSha"`
CommitTime string `json:"commitTime"`
Files []gitFileSnapshot `json:"files"`
}This is the serialized representation of a commit used by gitDiff().
Why encode snapshots as JSON bytes?
- It keeps the function boundaries simple.
- It makes the output portable.
- It allows snapshots to stay in memory for now.
- It can later be persisted or transmitted without redesigning the model.
gitGetLatestCommit(source GitCfg) ([]byte, error) does this:
- Resolve Git auth.
- Clone the repository into in-memory storage.
- Resolve the configured branch head.
- Read the commit tree.
- Filter files according to
dirListandignoreDirList. - Build a
gitCommitSnapshot. - Marshal it to JSON bytes.
Notable implementation detail:
- It uses
memory.NewStorage()andmemfs.New(), so no working tree is written to disk during clone.
gitGetLastCommit(source GitCfg, lastCommitId string) ([]byte, error) follows the same structure as latest-commit retrieval, but it loads a specific commit SHA instead of branch head.
This means both latest and previous commit data share the same snapshot format. That is why gitDiff() can compare them cleanly.
The Git implementation applies directory filters before files enter the snapshot.
Functions involved:
shouldIncludePath()pathMatchesAny()normalizeGitPath()
For a file to be included:
- It must match at least one
dirListprefix ifdirListis not empty. - It must not match any
ignoreDirListprefix.
normalizeGitPath() makes prefix matching deterministic by:
- converting backslashes to forward slashes,
- removing leading
./, - removing leading
/, - applying
path.Clean().
This prevents mismatches caused by path formatting differences.
gitDiff(lastCommitData, latestCommitData) compares two JSON snapshots in memory.
It works by:
- decoding both snapshots,
- building a path-to-blob map for the old snapshot,
- building a path-to-blob map for the new snapshot,
- identifying changed files,
- identifying deleted files,
- returning a
gitDiffResultas JSON.
type gitDiffResult struct {
BaseCommit string `json:"baseCommit"`
TargetCommit string `json:"targetCommit"`
ChangedFiles []string `json:"changedFiles"`
DeletedFiles []string `json:"deletedFiles"`
ExportedFiles []string `json:"exportedFiles,omitempty"`
ExportedOldFiles []string `json:"exportedOldFiles,omitempty"`
ExportedNewFiles []string `json:"exportedNewFiles,omitempty"`
}Interpretation:
ChangedFiles: files added or modified in the target commit.DeletedFiles: files present in the base commit but missing in target.ExportedOldFiles: actual old-version files written to disk.ExportedNewFiles: actual new-version files written to disk.ExportedFiles: combined list of both old and new exported paths.
Git auth is resolved by resolveGitAuth().
Current supported auth model:
- HTTPS
- Secret-backed credentials
- Username + token or username + password
Credential lookup uses:
credentials.typecredentials.namecredentials.path
The code expects either:
- mounted files:
<path>/<name>/username<path>/<name>/token<path>/<name>/password- or a bundled secret file at
<path>/<name>with key-value pairs.
Example mounted layout:
$HOME/git-secret-token/username
$HOME/git-secret-token/token
This is the most important output for the downstream application.
Current layout:
<sharedVolume.path>/<dirList>/<latestCommitSHA>/Old/<relative-path-within-dirList>
<sharedVolume.path>/<dirList>/<latestCommitSHA>/New/<relative-path-within-dirList>
With this config:
sharedVolume.path = $HOME/DesktopdirList = content/PerfectolatestCommitSHA = abc123
The exported directories become:
$HOME/Desktop/content/Perfecto/abc123/Old
$HOME/Desktop/content/Perfecto/abc123/New
If the changed file is:
content/Perfecto/guides/setup.md
Then HAWK writes:
$HOME/Desktop/content/Perfecto/abc123/Old/guides/setup.md
$HOME/Desktop/content/Perfecto/abc123/New/guides/setup.md
It satisfies two downstream needs:
- preserve the logical content grouping from
dirList, - preserve a stable pairing between old and new file versions.
The external application can walk the Old and New trees under a single commit folder and process file pairs by relative path.
writeChangedFilesFromCommits() does this:
- Expand the configured shared volume path.
- Clone the repo in memory.
- Load the base commit tree.
- Load the target commit tree.
- For each changed file:
- find the best matching
dirListprefix, - compute the path relative to that dirList,
- write old file content into
Old/if present, - write new file content into
New/if present.
Deleted files only appear under Old/.
Newly added files only appear under New/.
This helper exists because the export layout is based on dirList.
It returns:
- the matched
dirListprefix, - the file path relative to that prefix.
Example:
- file path:
content/Perfecto/guides/setup.md - matched dir:
content/Perfecto - relative path:
guides/setup.md
That is how HAWK avoids writing duplicate path segments like:
$HOME/Desktop/content/Perfecto/abc123/New/content/Perfecto/guides/setup.md
and instead writes:
$HOME/Desktop/content/Perfecto/abc123/New/guides/setup.md
The Confluence handler is currently a scaffold.
What it currently does:
- validates required config,
- implements the
Sourceinterface, - returns
nilfromconfluenceSync().
Why it matters now:
- It proves the interface design supports multiple source types.
- It shows where additional source-specific logic will be added later.
Database structs already exist in pkg/run.go:
databaseConfigdatabaseConnectionConfig
These are not yet used by runtime code.
Their likely future role is:
- persist the last processed commit,
- store sync metadata,
- store downstream processing results.
These are important to understand when extending the project.
- The config file path in
init_hawk()is currently hardcoded to a local development path, not/etc/hawk/configlist.yaml. - Previous commit SHA is hardcoded in
gitSync(). - Git export reclones the repository for export instead of reusing an existing in-memory repo handle.
- Confluence support is not implemented.
- Database integration is not implemented.
The natural next improvements are:
- Replace hardcoded previous commit lookup with a stored state provider.
- Persist run metadata and last processed commit to the database.
- Reuse repo state between snapshot generation and export to avoid repeated clone cost.
- Add a manifest file in each commit export folder describing old/new file pairs.
- Implement Confluence source fetching using the same
Sourceabstraction.
Starts the full runtime.
Loads and parses the YAML config.
Runs the scheduled processing loop for one Config item.
Creates the cron-backed trigger channel.
Constructs a concrete source handler from config.
Checks Git config validity.
Starts Git sync for one source.
Coordinates latest snapshot, previous snapshot, diff, and export.
Builds the filtered snapshot for the latest branch head.
Builds the filtered snapshot for a specific older commit.
Computes changed and deleted files between snapshots.
Writes old/new changed file content into the shared volume.
Builds HTTP auth from mounted secret data.
Canonicalizes paths for consistent matching and export logic.
Maps a changed file back to its matched dirList base and relative path.
The simplest mental model for HAWK today is:
- config defines what to watch,
- scheduler decides when to watch it,
- source handlers know how to read a source,
- Git handler turns commits into snapshots,
- snapshots are diffed in memory,
- only changed file pairs are exported for downstream AI processing.
That is the current architecture the rest of the project is building on.