Gradle plugin and engine architecture
This page describes how STIG command execution is layered across front-ends (Gradle plugin, CLI, IDE) and the shared stig-engine.
High-level flow
A Gradle task collects CLI options and converts them to strongly typed Gradle properties (
RegularFileProperty,DirectoryProperty, enums, etc.).The task action builds a request object and calls the engine.
The engine validates inputs and performs the command.
The engine returns a structured result (and also logs useful diagnostics).
Even when a command is “just logging stuff” today, keeping request/result types makes it easy to evolve the API without breaking call sites.
Input validation
The engine is the ultimate authority for validation, because it is shared by multiple front-ends:
Gradle plugin
CLI
future IDE plugin
Front-ends may still do UX validation (early, helpful error messages), but the engine must remain the final gatekeeper.
Engine responsibilities (authoritative)
The engine should:
Check required paths exist (element directory, interface directory, etc.).
Validate types (file vs directory).
Validate formats (e.g., parse JSON/YAML and fail with a clear message if invalid).
Return structured errors (exceptions with stable messages, or sealed result types in the future).
Gradle plugin responsibilities
The Gradle plugin should focus on Gradle correctness and UX:
Declare inputs/outputs properly (
@InputFile,@InputDirectory,@OutputDirectory, etc.) so the task can be incremental/config-cache friendly.Convert CLI strings into typed properties (enums, files/dirs).
Provide discoverability (
help --task ...) and good option descriptions.
Frontend event logging
stig-engine remains backend-neutral and may use kotlin-logging for engine diagnostics. A frontend decides how those diagnostics and its own progress are presented: the CLI supplies an SLF4J backend, and the IDEA plugin uses the IntelliJ Platform logger.
Gradle task output has a stricter boundary. STIG-owned task actions emit a typed StigEventRecord through their single StigTaskReporter; only the reporter renders to Gradle's logger. Direct task logger.lifecycle(...), other direct Gradle logger calls, and bare println are forbidden by a source-level regression test. The only approved direct Gradle logger sites are the reporter renderer and dependency-free fallbacks used by the problem and symbol reporters. Dokka's model transformer uses Dokka's own logger and is outside the Gradle-task sink.
Every console event begins with the stable context:
The area is one of the canonical P1–P7 phases or a named non-pipeline area such as docs, model, or toolchain. The operation is stable and independent of the Gradle task path; for example, stigAnalysisP6RunAnalyzer maps to P6 | analyzer-run. Event kinds such as START, PROGRESS, VALIDATION, PROBLEM, ARTIFACT, TOOL_START, TOOL_DONE, and SUMMARY state what happened without embedding another task vocabulary in message text.
The message field is human prose: render a single line after the context, or a multiline block below it without quotes, escaping, indentation, or repeated prefixes. Other fields retain key=value rendering. JSONL remains the machine-readable contract and always escapes embedded newlines. Keep command construction in the producing task; the console renderer preserves the supplied text.
Logging level, event kind, and problem severity are separate dimensions. Use StigTaskReporter for routine events and diagnostic visibility. Use GradleStigProblemReporter for canonical findings and threshold enforcement; it also emits PROBLEM events. A warn or error log alone neither records a canonical problem nor fails the task. consoleTone assigns green only to passed VALIDATION events and explicit successful SUMMARY results, while successful TOOL_DONE events remain neutral. Failures and warnings take precedence over success. The public console guide explains the full appearance and visibility policy.
Emit timings as numeric durationMillis, durationSeconds, elapsedMillis, elapsedSeconds, estimatedSeconds, or planningSeconds fields. The console renderer uses the shared formatStigDuration policy and labels them duration, elapsed, estimated, or planning with explicit units. Native events transport these numbers as strings; render them by the same contract and retain their original JSONL representation. Human timing messages must use the same formatter; do not preformat numeric measurements or use a wall-clock formatter for elapsed time.
The same record is written as schema-versioned JSONL in the Gradle task's temporary directory. Schema v1 includes the UTC timestamp, elapsed duration, level, stable event ID, area, operation, task path, project path, activity, and typed field map. This stream is frontend telemetry, not durable safety evidence; task-owned reports and canonical problem records remain the evidence sources. A task reuses one reporter without reading Task.extensions during execution, so event logging preserves Gradle configuration-cache compatibility.
External Bazel and LLVM output is not rewritten. A task preserves its established streaming or capture behavior and adds typed TOOL_START/TOOL_DONE envelopes with the exit code and elapsed duration. The native P6 analyzer emits the same small event grammar for long-running solver progress; the Gradle P6 task parses those lines and forwards them into the same console and JSONL event model. Exceptions retain their concrete task name because Gradle presents them outside the event renderer.
Example: stigImportModel
The command stigImportModel is implemented as:
Explicit root and module Gradle tasks that update Kotlin sources without invoking compilation or reporting.
Ordered root file readers for SYML and native Highway JSON, canonical model bindings, and KotlinPoet source generation.
A module code reader that calls
StigEngine.importComponent(request)with an existing compiled interface inventory. Produce that inventory separately withstigExtractAnalysisInterfaceInventory, using the exact P3 compile context.A versioned renderer-neutral inventory consumed independently by the KTE harness renderer and KotlinPoet Context renderer; generated harness files are never scanner inputs.
Existing module and Kotlin identities determine destinations. Missing modules are reported with source paths; initialization remains a separate
stigInitElementcommand.
See Import and export models for persistent input mappings, metadata updates, source ownership and native schema export.