diff --git a/.github/scripts/CrdSchemaUtils.java b/.github/scripts/CrdSchemaUtils.java
new file mode 100644
index 0000000..1a92152
--- /dev/null
+++ b/.github/scripts/CrdSchemaUtils.java
@@ -0,0 +1,274 @@
+import io.fabric8.kubernetes.api.model.Quantity;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Shared utilities for CRD schema introspection and resource path resolution.
+ *
+ *
Used by both {@code VerifyResourceLimits} and {@code VerifyDocumentedResources}
+ * to discover {@code ResourceRequirements} fields in CRD OpenAPI v3 schemas
+ * and resolve those paths against CR instances.
+ */
+public class CrdSchemaUtils {
+
+ private CrdSchemaUtils() { }
+
+ /**
+ * Extract the kind name from a CRD document.
+ */
+ static String extractCrdKind(Map crd) {
+ Map names = getNestedMap(crd, "spec", "names");
+ return names != null ? (String) names.get("kind") : null;
+ }
+
+ /**
+ * Extract the OpenAPI v3 schema from the first version of a CRD.
+ */
+ @SuppressWarnings("unchecked")
+ static Map extractCrdSchema(Map crd) {
+ Map spec = getMap(crd, "spec");
+ if (spec == null) return null;
+
+ Object versionsObj = spec.get("versions");
+ if (!(versionsObj instanceof List)) return null;
+
+ List> versions = (List>) versionsObj;
+ if (versions.isEmpty()) return null;
+
+ Object firstVersion = versions.get(0);
+ if (!(firstVersion instanceof Map)) return null;
+
+ return getNestedMap((Map) firstVersion, "schema", "openAPIV3Schema");
+ }
+
+ /**
+ * Recursively walk a CRD schema to find all ResourceRequirements fields.
+ * Records the JSON path for each field found.
+ *
+ * This method finds ALL ResourceRequirements fields without filtering.
+ * Callers that need to skip certain paths (e.g., pod-level overhead fields
+ * inside embedded PodTemplateSpecs) should filter the results using
+ * {@link #isPodSpecOverheadPath(String)}.
+ */
+ @SuppressWarnings("unchecked")
+ static void walkSchema(Map schemaNode, String currentPath, List result) {
+ if (schemaNode == null) return;
+
+ Map properties = getMap(schemaNode, "properties");
+ if (properties == null) return;
+
+ if (isResourceRequirements(properties)) {
+ result.add(currentPath);
+ return;
+ }
+
+ for (Map.Entry entry : properties.entrySet()) {
+ if (!(entry.getValue() instanceof Map)) continue;
+
+ Map childSchema = (Map) entry.getValue();
+ String childPath = currentPath + "." + entry.getKey();
+ String type = (String) childSchema.get("type");
+
+ if ("array".equals(type)) {
+ Map items = getMap(childSchema, "items");
+ if (items != null) {
+ walkSchema(items, childPath + "[]", result);
+ }
+ } else {
+ walkSchema(childSchema, childPath, result);
+ }
+ }
+ }
+
+ /**
+ * Detect a ResourceRequirements field by its OpenAPI schema signature.
+ * Must have "limits" and "requests" properties where both have
+ * additionalProperties with x-kubernetes-int-or-string: true.
+ */
+ static boolean isResourceRequirements(Map properties) {
+ if (!properties.containsKey("limits") || !properties.containsKey("requests")) {
+ return false;
+ }
+
+ return hasIntOrStringAdditionalProperties(properties.get("limits"))
+ && hasIntOrStringAdditionalProperties(properties.get("requests"));
+ }
+
+ @SuppressWarnings("unchecked")
+ private static boolean hasIntOrStringAdditionalProperties(Object fieldObj) {
+ if (!(fieldObj instanceof Map)) return false;
+ Map field = (Map) fieldObj;
+ Object addProps = field.get("additionalProperties");
+ if (!(addProps instanceof Map)) return false;
+ return Boolean.TRUE.equals(((Map) addProps).get("x-kubernetes-int-or-string"));
+ }
+
+ /**
+ * Check whether a ResourceRequirements path represents a pod-level
+ * overhead field embedded in a PodTemplateSpec, rather than a
+ * component-level resource requirement.
+ *
+ * Pod-level resources (added in k8s 1.30) appear as siblings of
+ * {@code containers} inside embedded PodSpec structures like
+ * {@code template.spec} or {@code podTemplateSpec.spec}. These are
+ * infrastructure overhead and should not be required in CR configs.
+ *
+ *
CRD-level resources (e.g., Prometheus {@code spec.resources}) that
+ * happen to be siblings of {@code containers} are NOT filtered by this
+ * method — they appear at the CRD spec level, not inside an embedded
+ * PodTemplateSpec.
+ *
+ * @param path the dot-separated path (e.g., ".spec.app.podTemplateSpec.spec.resources")
+ * @return true if this is a pod-level overhead path that should be skipped
+ */
+ static boolean isPodSpecOverheadPath(String path) {
+ return path.matches(".*\\.template\\.spec\\.resources$")
+ || path.matches(".*\\.podTemplateSpec\\.spec\\.resources$");
+ }
+
+ /**
+ * Resolve a path through a document, handling array segments (ending with []).
+ * Returns all leaf values reached along with their resolved paths.
+ */
+ @SuppressWarnings("unchecked")
+ static List resolvePath(Object current, String[] segments, int index, String pathSoFar) {
+ if (index >= segments.length) {
+ return List.of(new ResolvedNode(pathSoFar, current));
+ }
+
+ String segment = segments[index];
+
+ if (segment.endsWith("[]")) {
+ String key = segment.substring(0, segment.length() - 2);
+ if (!(current instanceof Map)) return List.of();
+ Object listObj = ((Map) current).get(key);
+ if (!(listObj instanceof List)) return List.of();
+
+ List> list = (List>) listObj;
+ List results = new ArrayList<>();
+ for (int i = 0; i < list.size(); i++) {
+ results.addAll(resolvePath(list.get(i), segments, index + 1,
+ pathSoFar + "." + key + "[" + i + "]"));
+ }
+ return results;
+ } else {
+ if (!(current instanceof Map)) return List.of();
+ Object child = ((Map) current).get(segment);
+ if (child == null) return List.of();
+ return resolvePath(child, segments, index + 1, pathSoFar + "." + segment);
+ }
+ }
+
+ // --- Kubernetes quantity parsing (delegates to Fabric8 Quantity) ---
+
+ private static final BigDecimal MILLIS_PER_CORE = BigDecimal.valueOf(1000);
+ private static final BigDecimal BYTES_PER_MIB = BigDecimal.valueOf(1_048_576);
+
+ /**
+ * Parse a Kubernetes CPU quantity to millicores.
+ *
+ * Handles all Kubernetes quantity formats via Fabric8 {@link Quantity},
+ * including millicore suffixes ({@code "500m"}), whole/fractional cores
+ * ({@code "1"}, {@code "0.5"}), and values parsed by SnakeYAML as
+ * {@link Integer} or {@link Double}.
+ *
+ * @param value the CPU quantity (String, Integer, or Double)
+ * @return the value in millicores
+ */
+ static long parseCpuMillis(Object value) {
+ Quantity q = Quantity.parse(String.valueOf(value));
+ return q.getNumericalAmount().multiply(MILLIS_PER_CORE).longValue();
+ }
+
+ /**
+ * Parse a Kubernetes memory quantity to MiB.
+ *
+ *
Handles all Kubernetes quantity formats via Fabric8 {@link Quantity},
+ * including binary suffixes ({@code Ki}, {@code Mi}, {@code Gi}, {@code Ti},
+ * {@code Pi}, {@code Ei}), decimal suffixes ({@code k}, {@code M}, {@code G},
+ * {@code T}, {@code P}, {@code E}), exponent notation, and plain byte counts.
+ *
+ * @param value the memory quantity (String, Integer, or Double)
+ * @return the value in MiB (rounded half-up)
+ */
+ static long parseMemoryMiB(Object value) {
+ Quantity q = Quantity.parse(String.valueOf(value));
+ BigDecimal bytes = Quantity.getAmountInBytes(q);
+ return bytes.divide(BYTES_PER_MIB, 0, RoundingMode.HALF_UP).longValue();
+ }
+
+ /**
+ * Check that {@code resources.requests} does not exceed {@code resources.limits}
+ * for both CPU and memory.
+ *
+ *
Uses numeric comparison via Fabric8 {@link Quantity} so semantically
+ * equal values in different formats (e.g., {@code "1"} vs {@code "1000m"})
+ * are treated as equal.
+ *
+ * @param resources the resources map (with "requests" and "limits" sub-maps)
+ * @param prefix a human-readable prefix for error messages
+ * @return list of invariant violation messages (empty if requests <= limits)
+ */
+ @SuppressWarnings("unchecked")
+ static List checkRequestsNotExceedLimits(Map resources, String prefix) {
+ List errors = new ArrayList<>();
+ if (resources == null) return errors;
+
+ Object requestsObj = resources.get("requests");
+ Object limitsObj = resources.get("limits");
+ if (!(requestsObj instanceof Map) || !(limitsObj instanceof Map)) return errors;
+
+ Map requests = (Map) requestsObj;
+ Map limits = (Map) limitsObj;
+
+ if (requests.containsKey("cpu") && limits.containsKey("cpu")) {
+ long reqCpu = parseCpuMillis(requests.get("cpu"));
+ long limCpu = parseCpuMillis(limits.get("cpu"));
+ if (reqCpu > limCpu) {
+ errors.add(prefix + " requests.cpu (" + reqCpu
+ + "m) > limits.cpu (" + limCpu + "m)");
+ }
+ }
+ if (requests.containsKey("memory") && limits.containsKey("memory")) {
+ long reqMem = parseMemoryMiB(requests.get("memory"));
+ long limMem = parseMemoryMiB(limits.get("memory"));
+ if (reqMem > limMem) {
+ errors.add(prefix + " requests.memory (" + reqMem
+ + "Mi) > limits.memory (" + limMem + "Mi)");
+ }
+ }
+
+ return errors;
+ }
+
+ // --- Utilities ---
+
+ @SuppressWarnings("unchecked")
+ static Map getMap(Map parent, String key) {
+ Object value = parent.get(key);
+ return value instanceof Map ? (Map) value : null;
+ }
+
+ static Map getNestedMap(Map root, String... keys) {
+ Map current = root;
+ for (String key : keys) {
+ current = getMap(current, key);
+ if (current == null) return null;
+ }
+ return current;
+ }
+
+ static class ResolvedNode {
+ final String path;
+ final Object value;
+
+ ResolvedNode(String path, Object value) {
+ this.path = path;
+ this.value = value;
+ }
+ }
+}
diff --git a/.github/scripts/ShowOverlayResources.java b/.github/scripts/ShowOverlayResources.java
new file mode 100644
index 0000000..2398cdc
--- /dev/null
+++ b/.github/scripts/ShowOverlayResources.java
@@ -0,0 +1,350 @@
+///usr/bin/env jbang "$0" "$@" ; exit $?
+//DEPS org.yaml:snakeyaml:2.6
+//DEPS io.fabric8:kubernetes-model-core:7.6.1
+//SOURCES ScriptUtils.java
+//SOURCES CrdSchemaUtils.java
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Show a per-component resource breakdown for an overlay and suggest
+ * frontmatter values for the overlay's documentation page.
+ *
+ * This is a developer helper tool — it always exits successfully
+ * (unless {@code kustomize} itself fails). It does not enforce any
+ * rules; use {@code VerifyResourceLimits} and
+ * {@code VerifyDocumentedResources} for CI verification.
+ *
+ *
Environment variables:
+ *
+ * - {@code OVERLAY} — overlay name (default: "core")
+ *
+ */
+public class ShowOverlayResources {
+
+ private static final String DEFAULT_OVERLAY = "core";
+
+ public static void main(String[] args) {
+ String overlay = System.getenv().getOrDefault("OVERLAY", DEFAULT_OVERLAY);
+ run(overlay);
+ }
+
+ /**
+ * Main logic, separated from {@code main()} for testability.
+ *
+ * @param overlay the overlay name
+ * @return the collected resource rows
+ */
+ static List run(String overlay) {
+ Path repoRoot = ScriptUtils.findRepoRoot();
+
+ System.out.println("=== Resource breakdown (overlay: " + overlay + ") ===");
+ System.out.println();
+
+ List