Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gittensory.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -1130,3 +1130,13 @@ settings:
# reviewRecap:
# enabled: true # Bool. Default: false (no recap is ever built or posted).
# cadenceDays: 7 # Positive integer. Days of activity each recap covers. Default: 7 (weekly).

# Cross-repo maintainer recap digest (#1963, #2250): config-as-code override for the CRON-scheduled digest
# that folds gate-precision + outcome-calibration across every scanned repo into one report (distinct from
# the single-repo reviewRecap above). Operator-level, not per-repo -- only meaningful on the gittensory
# self-repo's own manifest (the repo this instance identifies as); a present block there wins over the
# GITTENSORY_MAINTAINER_RECAP / GITTENSORY_RECAP_CADENCE env vars, which stay the fallback when absent.
# maintainerRecap:
# enabled: true # Bool. Default: false (env vars decide instead).
# cadence: weekly # daily | weekly. Default: weekly. Invalid values fall back to weekly.
# channel: discord # discord (only supported channel today). Invalid values fall back to discord.
10 changes: 10 additions & 0 deletions config/examples/gittensory.full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1143,3 +1143,13 @@ settings:
# reviewRecap:
# enabled: true # Bool. Default: false (no recap is ever built or posted).
# cadenceDays: 7 # Positive integer. Days of activity each recap covers. Default: 7 (weekly).

# Cross-repo maintainer recap digest (#1963, #2250): config-as-code override for the CRON-scheduled digest
# that folds gate-precision + outcome-calibration across every scanned repo into one report (distinct from
# the single-repo reviewRecap above). Operator-level, not per-repo -- only meaningful on the gittensory
# self-repo's own manifest (the repo this instance identifies as); a present block there wins over the
# GITTENSORY_MAINTAINER_RECAP / GITTENSORY_RECAP_CADENCE env vars, which stay the fallback when absent.
# maintainerRecap:
# enabled: true # Bool. Default: false (env vars decide instead).
# cadence: weekly # daily | weekly. Default: weekly. Invalid values fall back to weekly.
# channel: discord # discord (only supported channel today). Invalid values fall back to discord.
62 changes: 61 additions & 1 deletion packages/gittensory-engine/src/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,25 @@ export type FocusManifestReviewRecapConfig = {
cadenceDays: number;
};

/**
* Config-as-code override for the CROSS-repo maintainer recap digest's cron knobs (#1963, #2250), declared
* under `maintainerRecap:`. Distinct from `reviewRecap:` above (that is the single-repo digest's own window/
* enable knob); this instead overrides the GITTENSORY_MAINTAINER_RECAP / GITTENSORY_RECAP_CADENCE env vars
* that gate the cron-scheduled cross-repo digest (buildMaintainerRecap, #2239 / #2248) — read from the
* gittensory self-repo's manifest (resolveGittensorySelfRepoFullName), since the digest is an operator-level
* setting, not a per-contributor-repo one. Mirrors `reviewRecap:` exactly: no DB-backed counterpart, so the
* parsed value (or the default below when unset) IS the effective value. Not present (or present with no
* fields set) ⇒ the caller falls back to the env vars, byte-identical to before this override existed.
*/
export type FocusManifestMaintainerRecapConfig = {
present: boolean;
enabled: boolean;
cadence: "daily" | "weekly";
/** Delivery channel for the digest. Discord-only for now (mirrors deliverRecapToDiscord, #2245) — Slack
* delivery for this cross-repo digest is a follow-up, so any other value falls back to "discord". */
channel: "discord";
};

/**
* Generic repository-settings override declared in `.gittensory.yml` under `settings:`. A partial of
* {@link RepositorySettings} — every behaviour a maintainer can toggle in the dashboard can be set here
Expand Down Expand Up @@ -822,6 +841,7 @@ export type FocusManifest = {
contentLane: FocusManifestContentLaneConfig;
repoDocGeneration: FocusManifestRepoDocGenerationConfig;
reviewRecap: FocusManifestReviewRecapConfig;
maintainerRecap: FocusManifestMaintainerRecapConfig;
warnings: string[];
};

Expand Down Expand Up @@ -943,6 +963,16 @@ const EMPTY_REVIEW_RECAP_CONFIG: FocusManifestReviewRecapConfig = {
cadenceDays: DEFAULT_REVIEW_RECAP_CADENCE_DAYS,
};

const DEFAULT_MAINTAINER_RECAP_CADENCE: "daily" | "weekly" = "weekly";
const DEFAULT_MAINTAINER_RECAP_CHANNEL: "discord" = "discord";

const EMPTY_MAINTAINER_RECAP_CONFIG: FocusManifestMaintainerRecapConfig = {
present: false,
enabled: false,
cadence: DEFAULT_MAINTAINER_RECAP_CADENCE,
channel: DEFAULT_MAINTAINER_RECAP_CHANNEL,
};

const EMPTY_MANIFEST: FocusManifest = {
present: false,
source: "none",
Expand All @@ -960,6 +990,7 @@ const EMPTY_MANIFEST: FocusManifest = {
contentLane: { ...EMPTY_CONTENT_LANE_CONFIG },
repoDocGeneration: { ...EMPTY_REPO_DOC_GENERATION_CONFIG },
reviewRecap: { ...EMPTY_REVIEW_RECAP_CONFIG },
maintainerRecap: { ...EMPTY_MAINTAINER_RECAP_CONFIG },
warnings: [],
};

Expand Down Expand Up @@ -990,6 +1021,7 @@ function emptyManifest(source: FocusManifestSource, warnings: string[] = []): Fo
contentLane: { ...EMPTY_CONTENT_LANE_CONFIG },
repoDocGeneration: { ...EMPTY_REPO_DOC_GENERATION_CONFIG },
reviewRecap: { ...EMPTY_REVIEW_RECAP_CONFIG },
maintainerRecap: { ...EMPTY_MAINTAINER_RECAP_CONFIG },
};
}

Expand Down Expand Up @@ -1573,6 +1605,32 @@ export function reviewRecapConfigToJson(config: FocusManifestReviewRecapConfig):
return { enabled: config.enabled, cadenceDays: config.cadenceDays };
}

/**
* Parse the optional `maintainerRecap:` mapping (#1963, #2250). Mirrors {@link parseReviewRecapConfig}: every
* field has a concrete default (no DB layer to overlay onto), so the parsed value IS the effective value. An
* invalid `cadence`/`channel` falls back to its default via {@link normalizeEnum} (with a warning) rather than
* silently firing more often or targeting an unsupported channel.
*/
function parseMaintainerRecapConfig(value: JsonValue | undefined, warnings: string[]): FocusManifestMaintainerRecapConfig {
if (value === undefined || value === null) return { ...EMPTY_MAINTAINER_RECAP_CONFIG };
if (typeof value !== "object" || Array.isArray(value)) {
warnings.push('Manifest field "maintainerRecap" must be a mapping; ignoring it.');
return { ...EMPTY_MAINTAINER_RECAP_CONFIG };
}
const record = value as Record<string, JsonValue>;
const enabled = normalizeOptionalBoolean(record.enabled, "maintainerRecap.enabled", warnings) ?? false;
const cadence = normalizeEnum<"daily" | "weekly">(record.cadence, "maintainerRecap.cadence", ["daily", "weekly"], DEFAULT_MAINTAINER_RECAP_CADENCE, warnings);
const channel = normalizeEnum<"discord">(record.channel, "maintainerRecap.channel", ["discord"], DEFAULT_MAINTAINER_RECAP_CHANNEL, warnings);
return { present: true, enabled, cadence, channel };
}

/** Serialize a maintainerRecap config back into the parse-compatible shape so a cached snapshot round-trips
* through {@link parseMaintainerRecapConfig} unchanged. Returns null when nothing is configured. */
export function maintainerRecapConfigToJson(config: FocusManifestMaintainerRecapConfig): JsonValue {
if (!config.present) return null;
return { enabled: config.enabled, cadence: config.cadence, channel: config.channel };
}

function normalizeOptionalEnum<T extends string>(value: JsonValue | undefined, field: string, allowed: readonly T[], warnings: string[]): T | null {
if (value === undefined || value === null) return null;
if (typeof value === "string" && (allowed as readonly string[]).includes(value)) return value as T;
Expand Down Expand Up @@ -2931,6 +2989,7 @@ export function parseFocusManifest(raw: unknown, source?: FocusManifestSource):
contentLane: parseContentLaneConfig(record.contentLane, warnings),
repoDocGeneration: parseRepoDocGenerationConfig(record.repoDocGeneration, warnings),
reviewRecap: parseReviewRecapConfig(record.reviewRecap, warnings),
maintainerRecap: parseMaintainerRecapConfig(record.maintainerRecap, warnings),
warnings,
};
if (
Expand All @@ -2947,7 +3006,8 @@ export function parseFocusManifest(raw: unknown, source?: FocusManifestSource):
!manifest.features.present &&
!manifest.contentLane.present &&
!manifest.repoDocGeneration.present &&
!manifest.reviewRecap.present
!manifest.reviewRecap.present &&
!manifest.maintainerRecap.present
) {
warnings.push("Manifest contained no recognized focus fields; falling back to deterministic signals.");
manifest.present = false;
Expand Down
2 changes: 2 additions & 0 deletions packages/gittensory-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ export {
repoDocGenerationConfigToJson,
reviewConfigToJson,
reviewRecapConfigToJson,
maintainerRecapConfigToJson,
settingsOverrideToJson,
MAX_FOCUS_MANIFEST_BYTES,
CONVERGED_FEATURE_KEYS,
Expand Down Expand Up @@ -367,6 +368,7 @@ export {
type FocusManifestRepoDocGenerationScope,
type FocusManifestReviewConfig,
type FocusManifestReviewRecapConfig,
type FocusManifestMaintainerRecapConfig,
type FocusManifestSettings,
type FocusManifestSource,
type LabelingRule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ function normalizeMapping(input: unknown, index: number, warnings: string[]): Li
warnings.push(`settings.linkedIssueLabelPropagation.mappings[${index}].trustMaintainerAuthoredIssue must be a boolean; ignoring it.`);
}
}
// Same parse contract as trustMaintainerAuthoredIssue just above (#priority-reward-maintainer-trust).
// Mirrors `src/review/linked-issue-label-propagation.ts`'s copy of this normalizer.
// Same parse contract as trustMaintainerAuthoredIssue just above (#priority-reward-maintainer-trust):
// malformed is warned-and-defaulted to undefined/strict, never silently coerced, never a reason to drop
// an otherwise-valid mapping.
let trustMaintainerAuthoredIssueForReward: boolean | undefined;
if (record.trustMaintainerAuthoredIssueForReward !== undefined) {
if (typeof record.trustMaintainerAuthoredIssueForReward === "boolean") {
Expand Down
21 changes: 13 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { processDlqBatch } from "./queue/dlq";
import { processJob } from "./queue/processors";
import { isOrbBrokerEnabled } from "./orb/broker";
import { isOpsEnabled } from "./review/ops-wire";
import { isRecapEnabled, shouldFireMaintainerRecap } from "./review/maintainer-recap-wire";
import { isRecapEnabled, resolveMaintainerRecapManifestOverride, shouldFireMaintainerRecap } from "./review/maintainer-recap-wire";
import { isSweepWatchdogEnabled } from "./review/sweep-watchdog";
import { isPrReconciliationEnabled } from "./review/pr-reconciliation";
import { isRagEnabled } from "./review/rag-wire";
Expand Down Expand Up @@ -216,13 +216,18 @@ async function enqueueScheduledJobs(env: Env, controller: ScheduledController):
if (isHourly && hour === 9 && selfHostedReviews) {
jobs.push({ type: "repo-doc-refresh-sweep", requestedBy: "schedule" });
}
// Maintainer recap digest (#1963, #2248; flag GITTENSORY_MAINTAINER_RECAP). Cross-repo RecapReport delivered
// to Discord on a configurable cadence (GITTENSORY_RECAP_CADENCE=daily|weekly, default weekly) at the
// configured hour/day-of-week (GITTENSORY_RECAP_HOUR / GITTENSORY_RECAP_DAY). Enqueued ONLY when the flag is
// ON and this tick matches the configured cadence -- flag-OFF (default) this job is never created, so the
// cron tick does ZERO new work and the enqueued set is byte-identical to today.
if (selfHostedReviews && isRecapEnabled(env) && isHourly && shouldFireMaintainerRecap(env, hour, scheduledAt.getUTCDay())) {
jobs.push({ type: "generate-maintainer-recap", requestedBy: "schedule" });
// Maintainer recap digest (#1963, #2248/#2250; flag GITTENSORY_MAINTAINER_RECAP). Cross-repo RecapReport
// delivered to Discord on a configurable cadence (GITTENSORY_RECAP_CADENCE=daily|weekly, default weekly) at
// the configured hour/day-of-week (GITTENSORY_RECAP_HOUR / GITTENSORY_RECAP_DAY). Enable/cadence can ALSO be
// set as code via the gittensory self-repo's `.gittensory.yml maintainerRecap:` block (config-as-code parity,
// #2250) -- a present manifest block wins over the env vars; absent, the env vars decide exactly as before.
// Enqueued ONLY when this tick matches the resolved cadence -- disabled (the default) this job is never
// created, so the cron tick does ZERO new work and the enqueued set is byte-identical to today.
if (selfHostedReviews && isHourly) {
const maintainerRecapOverride = await resolveMaintainerRecapManifestOverride(env);
if (isRecapEnabled(env, maintainerRecapOverride) && shouldFireMaintainerRecap(env, hour, scheduledAt.getUTCDay(), maintainerRecapOverride)) {
jobs.push({ type: "generate-maintainer-recap", requestedBy: "schedule" });
}
}
if (isFullSyncWindow) {
jobs.push({ type: "generate-signal-snapshots", requestedBy: "schedule" });
Expand Down
14 changes: 8 additions & 6 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ import { DEFAULT_UNLINKED_ISSUE_GUARDRAIL } from "../review/unlinked-issue-guard
import { resolveUnlinkedIssueMatchDisposition } from "../review/unlinked-issue-guardrail";
import { DEFAULT_SCREENSHOT_TABLE_GATE, evaluateScreenshotTableGate } from "../review/screenshot-table-gate";
import { isOpsEnabled, runOpsAlerts } from "../review/ops-wire";
import { isRecapEnabled, runMaintainerRecapJob } from "../review/maintainer-recap-wire";
import { isRecapEnabled, resolveMaintainerRecapManifestOverride, runMaintainerRecapJob } from "../review/maintainer-recap-wire";
import { isSweepWatchdogEnabled, runSweepLivenessWatchdog } from "../review/sweep-watchdog";
import { isPrReconciliationEnabled, runOpenPrReconciliation } from "../review/pr-reconciliation";
import { isSelfTuneEnabled, runSelfTune } from "../review/selftune-wire";
Expand Down Expand Up @@ -1110,12 +1110,14 @@ export async function processJob(env: Env, message: JobMessage): Promise<void> {
case "generate-review-recap":
await runReviewRecapJob(env, message.repoFullName, message.windowDays);
return;
case "generate-maintainer-recap":
// Convergence (maintainer recap digest, flag GITTENSORY_MAINTAINER_RECAP, #1963/#2248). Defense-in-depth:
// the cron only ENQUEUES this when the flag is ON, but a stale in-flight job that lands after a flag-flip
// must still no-op, so flag-OFF does zero work here too.
if (isRecapEnabled(env)) await runMaintainerRecapJob(env, message.windowDays);
case "generate-maintainer-recap": {
// Convergence (maintainer recap digest, flag GITTENSORY_MAINTAINER_RECAP, #1963/#2248, config-as-code
// override #2250). Defense-in-depth: the cron only ENQUEUES this when enabled, but a stale in-flight job
// that lands after a flag-flip (env OR manifest) must still no-op, so disabled does zero work here too.
const maintainerRecapOverride = await resolveMaintainerRecapManifestOverride(env);
if (isRecapEnabled(env, maintainerRecapOverride)) await runMaintainerRecapJob(env, message.windowDays);
return;
}
case "agent-regate-sweep":
if (!message.repoFullName && message.requestedBy !== "test") {
await fanOutAgentRegateSweepJobs(env, message.requestedBy);
Expand Down
Loading
Loading