Skip to content

Commit a441d17

Browse files
authored
release: v0.9.3 (#1041)
* fix(review): v0.9.3 pre-release review findings - Stamp `Installation.VERSION` into `engine.cliVersion` on the signed verdict envelope so auditors reconstructing a stored verdict months later can identify which policy version generated it. The version is included in the canonical body, so tampering breaks the HMAC signature. - Add `staleManifest` boolean to the envelope, populated when a change- affecting source file has been modified after the manifest was written. Renames the internal `warnIfStale` to `detectStaleManifest` and removes the `opts.head` gate so the local working-tree workflow (compile once, edit for an hour, then review) also surfaces the signal instead of silently under-warning. - Emit `tierReasons[]` on the envelope whenever the classifier lands on `full` tier, not only when `--explain-tier` / `--force-tier` / `pathTokenConfigError` fires. A REQUEST_CHANGES on a schema.yml diff now carries the "why" in the PR-comment blockquote by default; `trivial` / `lite` stay quiet to avoid noise on approvals. - Document `--explain-tier`, `--force-tier`, `riskTierPathTokens`, and manifest auto-discovery in `docs/docs/usage/dbt-pr-review.md`. * test(tui): skip flaky phase-label e2e when CI env is set The `Discovering tools` assertion depends on the `bootstrap.resolve-tools` span (~30-100ms while listing MCP tools) outlasting the PTY harness's 50ms poll interval. On a cold CI runner the MCP listing can complete faster than the harness samples, dropping the label between polls. A local sample observed 4/5 passes; the flake mode is real and would intermittently gate main. Gate on the `CI` env so cold runners skip the test while local dev still runs it. * release: v0.9.3 CHANGELOG entry and adversarial regression suite for the v0.9.3 review skill changes (grain-key detector, tier promotion, --explain-tier / --force-tier flags, manifest auto-discovery, bootstrap phase labels) and the pre-release review fixes (envelope cliVersion + staleManifest, tier-reasons rendered in PR comment for full-tier verdicts, stale- manifest signal on local working-tree reviews). * fix(review): PR #1041 pre-merge review findings Bot review on the v0.9.3 release PR caught three correctness gaps and a CI-blocking branding-audit trip. All fixed here so the release stays clean: - **`cliVersion` default in `reviewPullRequest`.** Only the CLI command forwarded `Installation.VERSION`; the agent-invoked `dbt_pr_review` tool wrapper never did, so tool-path verdicts silently dropped `engine.cliVersion` and defeated the audit-trail promise the field was added for. Default the option to `Installation.VERSION` inside `reviewPullRequest` so both entry points populate it. (cubic P2 conf 9) - **`staleManifest` deletion false-negative.** `detectStaleManifest` previously took `changedPaths: string[]` and tried to `stat` each — a deletion of a manifest-affecting model reported CLEAN because the file no longer exists on disk. The signed envelope then falsely certified a verdict against ghost models. Pass the full `ChangedFile[]` through instead and treat `deleted` / `renamed` manifest-affecting entries as unconditionally stale (they can't be stat'd but the manifest still references them, so the metadata is by definition out of date). Rename covers both sides so a rename-away and a rename-to both trip. (cubic P2 conf 8) - **Docs boundary-matching wording + non-match example.** The `riskTierPathTokens` doc claimed "case-insensitive substrings" but `compilePathTokenResolver` actually anchors tokens at path/word/digit boundaries — `cost` fires on `mrt_cost_daily.sql` but not on `broadcaster.sql`. Correct the wording and add the non-match example so users don't configure tokens that don't match their intent. (cubic P2 conf 5 + coderabbit) - **Branding-audit trip in `script/release-preflight.ts:609`.** The operational instruction telling the operator how to add the upstream remote (`git remote add upstream https://github.com/anomalyco/opencode.git`) legitimately names the upstream URL. The CI branding audit doesn't know operational text from branding drift; wrap the block in `altimate_change` markers so the audit skips it. * test(review): PR #1041 adversarial coverage for fixes + repair vacuous tests Two new positive tests pin the fixes from the previous commit: - `reviewPullRequest defaults cliVersion so the dbt_pr_review tool path gets audit provenance too` — invokes `reviewPullRequest` WITHOUT passing `cliVersion`, asserts `env.engine.cliVersion` is non-empty. - `deleted manifest-affecting file trips staleness even though it can't be stat'd` — deletion of `models/orders.sql` with the manifest present must set `env.staleManifest === true`. Two vacuous tests from the original v0.9.3 adversarial file are repaired per cubic-review PR #1041: - Grandparent walk-past regression test now plants an actual `grand/ target/manifest.json` (a poisoned grandparent DAG), then invokes review from `grand/inner/subdir` where `inner/dbt_project.yml` has no compiled artifact. The assertion `env.manifestHash` must remain undefined AND the stderr breadcrumb must NOT reference the grandparent path — a regressed walker would announce a hit there. Previously the fixture just made an unrelated sibling directory, so the walker never had a real manifest to consider skipping. - Symlink-loop test is removed. Its `looped -> tmp` fixture was a one-hop parent alias, not a cycle; `fs.realpath` resolves it in one step, so the test proved nothing about the try/catch fallback in `run.ts`. A real ELOOP fixture is filesystem-dependent and the fallback pattern is a straight try/catch obvious from a code read; cheaper to delete than to churn on a cross-platform cycle. --------- Co-authored-by: Haider <haider@altimate.ai>
1 parent 39d7d94 commit a441d17

9 files changed

Lines changed: 577 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.9.3] - 2026-07-24
9+
10+
Focused polish on the `altimate review` dbt PR reviewer — plus TUI startup UX
11+
and audit-trail hardening on the signed verdict envelope.
12+
13+
### Added
14+
15+
- **Grain-key `not_null` completeness detector.** For every column named in a `unique_combination_of_columns` test (or the `dbt_utils` equivalent), the reviewer now checks that a `not_null` test or constraint exists on the same column — without it, a NULL grain value silently passes the uniqueness guardrail. Supports contracted models (`constraints:` with `contract.enforced: true`), column-level `data_tests:` / `tests:`, and model-level primary-key constraints. Case-folds column names for Snowflake compatibility. Delivered +11 real findings on the internal 5-PR corpus. (#1029)
16+
- **`--explain-tier` flag.** Pass `--explain-tier` to surface the classifier's tier-reason list on the verdict envelope so you can see why a diff was rated `trivial`, `lite`, or `full`. Full-tier verdicts also now include the reason line in the PR comment by default (no flag required), so a `REQUEST_CHANGES` on a schema.yml diff is never unexplained. (#1027)
17+
- **`--force-tier` flag.** *[EXPERIMENTAL / bench debug]* Bypass the classifier with `--force-tier=trivial|lite|full`. The signed envelope carries `tierForced: true` and the classifier's original decision, so an auditor always sees the bypass — the flag can't be used silently. (#1027)
18+
- **`riskTierPathTokens` config in `.altimate/review.yml`.** Configure named token categories (including `preset:finops` for the built-in cost/billing keyword list) to promote matching files to full review tier. Previously the FinOps preset was hardcoded and always on; it is now opt-in via this config. A typo in a category value surfaces in both stderr and the verdict envelope `tierReasons[]` (and the PR comment) so it can't kill your opt-in silently. (#1028)
19+
- **`engine.cliVersion` in the signed verdict envelope.** The altimate-code release that generated each verdict is now stamped into `engine.cliVersion` and covered by the HMAC signature — auditors reconstructing decisions from stored envelopes months later can identify which policy version applied.
20+
- **`staleManifest` flag on the verdict envelope.** When a change-affecting source file has been modified after the manifest was written, the signed envelope now carries `staleManifest: true`. A stderr warning alone is easy for CI to swallow; the envelope field is durable and part of the signed body.
21+
- **Bootstrap phase labels in the TUI.** While the agent initializes, the busy spinner now shows what it is actually doing ("Loading config…", "Discovering tools…", "Thinking…") matching the convention users already know from other agent UIs. Phase events are also emitted as trace spans for server-side latency profiling. (#1002)
22+
23+
### Changed
24+
25+
- **Risky dbt metadata surfaces never auto-approve.** PRs that touch `data_tests:`, `tests:`, `constraints:`, `contract:`, or `unique_combination_of_columns` in schema.yml under `models/marts/` — or files under any configured `riskTierPathTokens` category — are always promoted to `full` review tier. A small metadata diff can no longer slip through on `trivial` classification. Grounded in a corpus study where two PRs auto-approved with 8–11 human-visible findings each. **CI in `gate` mode may see exit-code changes on affected PRs.** (#1028)
26+
- **`--no-ai` flag now actually disables the LLM lane.** A yargs `boolean-negation` conflict caused a bare `--no-ai` to silently fall into the help path (exit 0, no review ran). The flag is fixed; CI scripts using `--no-ai` since v0.8.x should verify behavior on upgrade — those runs may have been executing the AI lane and getting billed. (#1027)
27+
- **`altimate review` auto-discovers `target/manifest.json`.** When `--manifest` is not passed and the config-relative path doesn't resolve, the reviewer walks up from `cwd` to the nearest `dbt_project.yml` and uses its adjacent `target/manifest.json`. Discovery is logged to stderr; explicit `--manifest` still wins. Removes the most common "review did nothing" support case for mono-repo invocations. (#1027)
28+
- **schema.yml test-removal detection is now column-aware.** The detector parses both sides of the diff structurally (YAML parse rather than diff-line pattern) and keys removals by `(model, column, test)` — eliminating false cancellations where a sibling column re-added the same test type. (#1027)
29+
- **Stale-manifest detection now runs for local working-tree reviews too.** Previously gated behind `--head`, so the local workflow "`dbt compile` once, edit for an hour, then `altimate review`" — where staleness bites in practice — silently under-warned. The check is limited to files that could materially change the manifest (models, schema.yml, seeds, macros, dbt config) so noise stays bounded.
30+
831
## [0.9.2] - 2026-07-20
932

1033
### Added

docs/docs/usage/dbt-pr-review.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,13 @@ Options:
106106
|------|-------------|
107107
| `--base <ref>` | Base git ref. Defaults to the merge-base with `origin/main`. |
108108
| `--head <ref>` | Head git ref. Omit to review the working tree. |
109-
| `--manifest <path>` | Path to the compiled `manifest.json` (default `target/manifest.json`). |
109+
| `--manifest <path>` | Path to the compiled `manifest.json`. When omitted, the reviewer walks up from the current directory to find the nearest `dbt_project.yml` and uses its adjacent `target/manifest.json`; the discovered path is logged to stderr. |
110110
| `--mode comment\|gate` | `comment` never blocks; `gate` exits non-zero on `REQUEST_CHANGES`. |
111111
| `--severity <level>` | Minimum severity to surface: `critical`, `warning`, `suggestion`. |
112112
| `--post` | Post the verdict to the GitHub PR (uses `GITHUB_TOKEN` + the Actions event). |
113113
| `--no-ai` | Disable the advisory LLM reviewer lane (no model calls / cost) — deterministic-only. |
114+
| `--explain-tier` | Emit the classifier's tier-reason list on the verdict envelope so you can see why a diff was rated `trivial`, `lite`, or `full`. Reasons already surface in the PR comment for `full`-tier runs — this flag adds them to `trivial`/`lite` for debugging. |
115+
| `--force-tier <tier>` | **[EXPERIMENTAL / bench debug]** Bypass the classifier and force `trivial` / `lite` / `full`. The verdict envelope carries `tierForced: true` and the classifier's original decision for audit. |
114116
| `--json` / `--output <file>` | Emit the verdict envelope as JSON. |
115117

116118
> **Full vs lint-only.** With a compiled `manifest.json` present, the reviewer
@@ -232,6 +234,9 @@ dataDiff: # OFF by default — see "Data-diff in CI" below
232234
warehouse: "" # connection name; empty = default connection
233235
exclude:
234236
- models/legacy/**
237+
riskTierPathTokens: # OFF by default — see "Risk-tier path tokens" below
238+
finops: [preset:finops] # promote any FinOps-named path to `full` tier
239+
pii: [ssn, email, phone] # or supply your own case-insensitive tokens
235240
rubric:
236241
blockOn: [lineage_breakage, contract_violation, pii_exposure, semantic_change]
237242
warningPatternThreshold: 3
@@ -242,6 +247,21 @@ rubric:
242247
skipNonProdModels: true
243248
```
244249
250+
### Risk-tier path tokens
251+
252+
Named categories under `riskTierPathTokens` promote any diff touching a matching
253+
path to `full` review tier, so those areas never auto-approve on `trivial`
254+
classification. Tokens are matched **case-insensitively at path, word, or digit
255+
boundaries** — not as arbitrary substrings. For example, the token `cost` fires
256+
on `models/marts/mrt_cost_daily.sql` (bounded by `_` and `_`) but **not** on
257+
`models/broadcaster.sql` (the substring lives inside a longer word). The
258+
`preset:finops` marker expands to the built-in FinOps keyword list (cost,
259+
billing, spend, revenue, etc.). Before v0.9.3 the FinOps list was hardcoded and
260+
always on; it is now opt-in via this config. When a category value is invalid
261+
the CLI logs a stderr warning AND surfaces the error in the verdict envelope's
262+
`tierReasons[]` (and in the PR comment), so a typo can't silently kill your
263+
opt-in.
264+
245265
## Data-diff in CI
246266

247267
Static equivalence proves a refactor *can't* change results. **Data-diff** goes

packages/opencode/src/altimate/review/orchestrate.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@ export interface OrchestrateInput {
180180
manifestHash?: string
181181
coreVersion?: string
182182
modelVersion?: string
183+
/** altimate-code CLI release recorded in engine.cliVersion for audit reconstruction. */
184+
cliVersion?: string
183185
/**
184186
* Optional LLM reviewer lane. Injected (not imported) so the orchestrator
185187
* stays pure/unit-testable: production wires `runAiReview` (harness LLM);
@@ -194,6 +196,8 @@ export interface OrchestrateInput {
194196
explainTier?: boolean
195197
/** G2 — override the classifier's tier decision. Envelope carries tierForced:true. */
196198
forceTier?: "trivial" | "lite" | "full"
199+
/** Manifest was stale relative to change-affecting files (see run.ts::detectStaleManifest). */
200+
staleManifest?: boolean
197201
}
198202

199203
/** Derive the dbt model name from a model file path. */
@@ -1423,17 +1427,19 @@ export async function runReview(input: OrchestrateInput): Promise<VerdictEnvelop
14231427
tier,
14241428
mode: input.mode,
14251429
rubric: input.rubric,
1426-
engine: { core: input.coreVersion, model: input.modelVersion },
1430+
engine: { core: input.coreVersion, model: input.modelVersion, cliVersion: input.cliVersion },
14271431
manifestHash: input.manifestHash,
1432+
staleManifest: input.staleManifest,
14281433
generatedAt: input.generatedAt,
14291434
degraded,
14301435
// Include tierReasons whenever `--explain-tier` / `--force-tier` is set,
1431-
// OR when a riskTierPathTokens config error was caught above — the error
1432-
// must surface in the envelope (and downstream PR comment) even in a
1433-
// normal `comment`/`gate` run, otherwise a config typo silently kills
1434-
// the user's opt-in with only a stderr trace they'll never see
1435-
// (coderabbit review, PR #1028 orchestrate.ts:1129).
1436-
tierReasons: input.explainTier || tierForced || pathTokenConfigError ? tierReasons : undefined,
1436+
// when a riskTierPathTokens config error was caught above, OR when the
1437+
// classifier lands on `full` tier — a naturally-full run is a customer-
1438+
// visible policy call ("why did my YAML-only diff get REQUEST_CHANGES?")
1439+
// and the reason list is what makes the verdict debuggable without the
1440+
// customer having to re-run with --explain-tier. `trivial`/`lite` runs
1441+
// stay silent to avoid noise on approvals.
1442+
tierReasons: input.explainTier || tierForced || pathTokenConfigError || tier === "full" ? tierReasons : undefined,
14371443
tierForced: tierForced ? true : undefined,
14381444
tierClassified: tierForced ? classifiedTier : undefined,
14391445
})

packages/opencode/src/altimate/review/run.ts

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import path from "node:path"
22
import { readFile, realpath, stat, access } from "node:fs/promises"
3+
import { Installation } from "../../installation"
34
import { loadReviewConfig, resolveRubric } from "./config"
45
import type { Severity } from "./finding"
56
import { collectChangedFiles, makeContentResolver, defaultBaseRef, gitRepoRoot, manifestHash } from "./git"
@@ -37,6 +38,8 @@ export interface ReviewPullRequestOptions {
3738
/** Model identifier recorded in the envelope. */
3839
modelVersion?: string
3940
coreVersion?: string
41+
/** altimate-code CLI release recorded in engine.cliVersion for audit reconstruction. */
42+
cliVersion?: string
4043
/** Disable the LLM reviewer lane (default: enabled; self-degrades if no model). */
4144
noAi?: boolean
4245
/** PR metadata for the AI reviewer's intent check. */
@@ -129,7 +132,7 @@ async function autoDiscoverManifest(cwd: string): Promise<{ path: string; projec
129132
* docs markdown blocks) or top-level dbt config. `README.md` at repo root,
130133
* `.github/`, `package.json`, etc. are excluded so their mtimes don't
131134
* trigger a stale warning. Exported for tests — see review-run-stale.test.ts.
132-
* Referenced by `warnIfStale`. */
135+
* Referenced by `detectStaleManifest`. */
133136
export function isManifestAffecting(rel: string): boolean {
134137
// Code / schema / seed CSV live under any dbt source directory.
135138
if (/(^|\/)(models|seeds|snapshots|macros|tests|analyses)\/.*\.(sql|py|yml|yaml|csv)$/i.test(rel)) return true
@@ -144,37 +147,63 @@ export function isManifestAffecting(rel: string): boolean {
144147
return false
145148
}
146149

147-
/** Warn to stderr when the manifest looks stale relative to changed files.
148-
* Compares the manifest's mtime against every change-affecting path (per
149-
* `isManifestAffecting`); a single change newer than the manifest is
150-
* enough to fire the warning once and return. Non-fatal — the review
151-
* proceeds against the (possibly stale) manifest. */
152-
async function warnIfStale(manifestAbs: string, changedPaths: string[], fsRoot: string): Promise<void> {
150+
/** Detect a stale manifest and warn to stderr. Returns `true` when a
151+
* change-affecting file was modified after the manifest — signalling the
152+
* verdict may have been computed against out-of-date metadata. Non-fatal;
153+
* the review proceeds against the (possibly stale) manifest and the
154+
* caller mirrors the flag onto the signed envelope so a downstream
155+
* auditor can distinguish stale-manifest verdicts from clean ones
156+
* (stderr alone is easy for CI to swallow).
157+
*
158+
* A deleted or renamed manifest-affecting file is treated as unconditionally
159+
* stale: the file's mtime cannot be checked (the path is gone) but the
160+
* manifest still references it, so the metadata is by definition out of date
161+
* (cubic-review PR #1041 — the prior path-only signature silently reported
162+
* clean for deletions and could false-certify verdicts against ghost models). */
163+
async function detectStaleManifest(manifestAbs: string, changed: ChangedFile[], fsRoot: string): Promise<boolean> {
153164
try {
154165
const manifestMtime = (await stat(manifestAbs)).mtimeMs
155-
for (const rel of changedPaths) {
156-
if (!isManifestAffecting(rel)) continue
157-
// `changedPaths` are repo-root relative (from `git diff --name-status`),
166+
for (const f of changed) {
167+
// For renamed files, the OLD path is what the manifest still knows about;
168+
// check both sides so both a rename-away and a rename-to a
169+
// manifest-affecting shape trigger the signal.
170+
const paths: string[] = [f.path]
171+
if (f.status === "renamed" && f.oldPath) paths.push(f.oldPath)
172+
if (!paths.some(isManifestAffecting)) continue
173+
174+
// Deleted / renamed files no longer exist at their old path on disk, so
175+
// stat would fail. The manifest still references them though, so the
176+
// metadata is stale by definition — fire immediately.
177+
if (f.status === "deleted" || f.status === "renamed") {
178+
process.stderr.write(
179+
`⚠️ manifest ${manifestAbs} appears stale — ${f.status} \`${f.path}\` is a manifest-affecting path. ` +
180+
`Re-run \`dbt compile\` (or \`dbt build\`) to refresh before reviewing.\n`,
181+
)
182+
return true
183+
}
184+
185+
// `changed` paths are repo-root relative (from `git diff --name-status`),
158186
// so root at the git top-level rather than the caller's cwd. When the
159187
// CLI is invoked from a subdir the naive path.join(cwd, rel) points at
160188
// a non-existent path and the stale check silently no-ops.
161-
const abs = path.isAbsolute(rel) ? rel : path.join(fsRoot, rel)
189+
const abs = path.isAbsolute(f.path) ? f.path : path.join(fsRoot, f.path)
162190
try {
163191
const changedMtime = (await stat(abs)).mtimeMs
164192
if (changedMtime > manifestMtime) {
165193
process.stderr.write(
166-
`⚠️ manifest ${manifestAbs} appears stale — ${rel} was modified after the manifest was written. ` +
194+
`⚠️ manifest ${manifestAbs} appears stale — ${f.path} was modified after the manifest was written. ` +
167195
`Re-run \`dbt compile\` (or \`dbt build\`) to refresh before reviewing.\n`,
168196
)
169-
return
197+
return true
170198
}
171199
} catch {
172-
/* file not on disk (e.g. removed by the change) — skip */
200+
/* file not on disk for an added/modified entry — skip (rare race) */
173201
}
174202
}
175203
} catch {
176204
/* manifest unreadable → detectDialect will have already returned undefined */
177205
}
206+
return false
178207
}
179208

180209
export async function reviewPullRequest(opts: ReviewPullRequestOptions): Promise<VerdictEnvelope> {
@@ -241,11 +270,17 @@ export async function reviewPullRequest(opts: ReviewPullRequestOptions): Promise
241270
}
242271
}
243272
}
244-
// Freshness check: warn (don't fail) when the manifest predates changed files.
245-
// Skip when we're diffing against the working tree (mtime signal is noisy
246-
// during active edits) — only warn when the caller explicitly pinned a head
247-
// ref, which is the CI / bench shape where a stale manifest is a real risk.
248-
if (opts.head) await warnIfStale(manifestAbs, changedFiles.map((f) => f.path), gitRoot)
273+
// Freshness check: detect (and warn — non-fatal) when the manifest predates
274+
// change-affecting files. Runs for both the pinned-head CI shape AND the
275+
// working-tree local shape — the local scenario "dbt compile once, edit for
276+
// an hour, then altimate review" is where staleness actually bites in
277+
// practice, so gating this behind `--head` (the prior behavior) silently
278+
// under-warned the most common developer workflow. mtime granularity on
279+
// the working tree can be noisy during a live edit session, but the check
280+
// is limited to files that would materially change the manifest
281+
// (`isManifestAffecting`), so noise is bounded. Return value is stamped
282+
// into the signed envelope so downstream auditors see it too.
283+
const staleManifest = await detectStaleManifest(manifestAbs, changedFiles, gitRoot)
249284

250285
// Resolve the SQL dialect: explicit config wins; otherwise auto-detect from
251286
// the dbt manifest's `adapter_type` (so a BigQuery/Redshift project isn't
@@ -306,10 +341,17 @@ export async function reviewPullRequest(opts: ReviewPullRequestOptions): Promise
306341
manifestHash: mhash,
307342
modelVersion: opts.modelVersion,
308343
coreVersion: opts.coreVersion,
344+
// Default cliVersion so agent-invoked callers (dbt_pr_review tool) that
345+
// don't thread the version get the audit-trail field populated too.
346+
// Only the CLI command explicitly forwards Installation.VERSION today,
347+
// so a missing default silently regresses envelope provenance for tool-
348+
// path verdicts (cubic-review PR #1041).
349+
cliVersion: opts.cliVersion ?? Installation.VERSION,
309350
aiReview: opts.noAi || config.ai === false ? undefined : runAiReview,
310351
prTitle: opts.prTitle,
311352
prBody: opts.prBody,
312353
explainTier: opts.explainTier,
313354
forceTier: opts.forceTier,
355+
staleManifest,
314356
})
315357
}

0 commit comments

Comments
 (0)