@celilo/cli 0.3.28 → 0.3.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celilo/cli",
3
- "version": "0.3.28",
3
+ "version": "0.3.30",
4
4
  "description": "Celilo — home lab orchestration CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,6 +8,7 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
8
8
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
9
9
  import { tmpdir } from 'node:os';
10
10
  import { join } from 'node:path';
11
+ import { eq } from 'drizzle-orm';
11
12
  import { type DbClient, getDb } from '../../db/client';
12
13
  import { modules } from '../../db/schema';
13
14
  import { classifyVersionChange, upgradeOne } from './module-upgrade';
@@ -153,4 +154,62 @@ description: fixture
153
154
  if (quietResult.status !== 'success') return;
154
155
  expect(quietResult.newVersion).toBe('1.0.0');
155
156
  });
157
+
158
+ // Regression for the namecheap stale-findings problem: the upgrade
159
+ // path MUST overwrite manifestData with the new manifest — otherwise
160
+ // any subsequent audit (or any code path that reads from the DB)
161
+ // sees the OLD manifest's required vars even after the operator
162
+ // upgraded to a version that removed them. The user hit this on
163
+ // celilo-mgmt: namecheap@3.1.1+6 dropped the `domains` variable, but
164
+ // post-upgrade the audit still complained "required config 'domains'
165
+ // is not set" because the DB's manifestData hadn't been refreshed.
166
+ test('persists new manifest to modules.manifestData (not just .version)', async () => {
167
+ // Write a brand-new manifest.yml in srcDir that REMOVES a
168
+ // variable the old DB record had. After upgrade, the modules
169
+ // row's manifestData should reflect the removal.
170
+ writeFileSync(
171
+ join(srcDir, 'manifest.yml'),
172
+ `celilo_contract: "1.0"
173
+ id: testmod
174
+ name: Test Module Renamed
175
+ version: 2.0.0
176
+ description: fixture v2
177
+ variables:
178
+ owns: []
179
+ imports: []
180
+ `,
181
+ );
182
+ // Simulate the DB starting in a state where manifestData has a
183
+ // `variables.owns` array — the namecheap-3.1.0 → 3.1.1 case.
184
+ db.update(modules)
185
+ .set({
186
+ manifestData: {
187
+ celilo_contract: '1.0',
188
+ id: 'testmod',
189
+ name: 'Test Module',
190
+ version: '1.0.0',
191
+ variables: { owns: [{ name: 'domains', type: 'array', required: true }], imports: [] },
192
+ },
193
+ })
194
+ .where(eq(modules.id, 'testmod'))
195
+ .run();
196
+
197
+ const result = await upgradeOne(srcDir, db, {}, { quiet: true, displayVersion: '2.0.0+1' });
198
+ expect(result.status).toBe('success');
199
+
200
+ const row = db.select().from(modules).all()[0];
201
+ // version field updates (this part already worked):
202
+ expect(row.version).toBe('2.0.0+1');
203
+ expect(row.name).toBe('Test Module Renamed');
204
+ // manifestData reflects the NEW manifest — the dropped variable
205
+ // is gone. This is the regression assertion.
206
+ const manifest = row.manifestData as {
207
+ version: string;
208
+ name: string;
209
+ variables?: { owns: unknown[] };
210
+ };
211
+ expect(manifest.version).toBe('2.0.0');
212
+ expect(manifest.name).toBe('Test Module Renamed');
213
+ expect(manifest.variables?.owns ?? []).toEqual([]);
214
+ });
156
215
  });
@@ -189,12 +189,14 @@ async function buildAuditDeps(onProgress?: (msg: string) => void) {
189
189
 
190
190
  const installedConfigs = deployedModules.map((m) => ({
191
191
  id: m.id,
192
+ state: m.state,
192
193
  manifest: m.manifestData as ModuleManifest,
193
194
  configs: configsByModule.get(m.id) ?? {},
194
195
  }));
195
196
 
196
197
  const installedBackupInfo = deployedModules.map((m) => ({
197
198
  id: m.id,
199
+ state: m.state,
198
200
  manifest: m.manifestData as ModuleManifest,
199
201
  lastSuccessfulBackupAt: latestBackupByModule.get(m.id) ?? null,
200
202
  }));
@@ -402,6 +404,7 @@ async function buildAuditDeps(onProgress?: (msg: string) => void) {
402
404
  capabilityAbi: {
403
405
  modules: deployedModules.map((m) => ({
404
406
  id: m.id,
407
+ state: m.state,
405
408
  manifest: m.manifestData as ModuleManifest,
406
409
  })),
407
410
  },
@@ -386,7 +386,12 @@ function buildOps(registry: RegistryClient, wasDeployed: Set<string>): Orchestra
386
386
  function formatResult(result: SystemUpdateResult): string {
387
387
  const lines: string[] = [];
388
388
  lines.push('');
389
- lines.push(`update ${result.updateId} ${result.ok ? 'OK' : 'FAILED'}`);
389
+ // The updateId is for audit / journal correlation (see the
390
+ // `backups` table's updateId FK); operators don't read it during
391
+ // normal use, only when debugging a specific run via --json or
392
+ // `celilo backup list`. Surfacing it in human output added noise
393
+ // without paying for itself.
394
+ lines.push(`System update ${result.ok ? 'completed' : 'FAILED'}`);
390
395
  lines.push(` audit verdict: ${result.audit.verdict}`);
391
396
  lines.push(
392
397
  ` self-update: ${result.selfUpdate.performed ? `${result.selfUpdate.from} → ${result.selfUpdate.to}` : `(${result.selfUpdate.reason})`}`,
@@ -523,6 +528,7 @@ export async function handleSystemUpdate(
523
528
  capabilityAbi: {
524
529
  modules: upgradableModules.map((m) => ({
525
530
  id: m.id,
531
+ state: m.state,
526
532
  manifest: m.manifestData as ModuleManifest,
527
533
  })),
528
534
  },
@@ -546,6 +552,7 @@ export async function handleSystemUpdate(
546
552
  moduleConfigs: {
547
553
  modules: upgradableModules.map((m) => ({
548
554
  id: m.id,
555
+ state: m.state,
549
556
  manifest: m.manifestData as ModuleManifest,
550
557
  configs: configsByModule.get(m.id) ?? {},
551
558
  })),
@@ -554,6 +561,7 @@ export async function handleSystemUpdate(
554
561
  backups: {
555
562
  modules: upgradableModules.map((m) => ({
556
563
  id: m.id,
564
+ state: m.state,
557
565
  manifest: m.manifestData as ModuleManifest,
558
566
  lastSuccessfulBackupAt: latestBackupByModule.get(m.id) ?? null,
559
567
  })),
@@ -677,6 +685,21 @@ export async function handleSystemUpdate(
677
685
  onlyModule,
678
686
  });
679
687
 
688
+ // The audit baked into `result.audit` ran BEFORE the orchestrator
689
+ // upgraded any modules. Its findings are now stale for whatever
690
+ // we just upgraded — `module_versions` drift, `module_configs`
691
+ // referencing the OLD manifest's required vars, `capability_abi`
692
+ // checking pre-upgrade providers. Re-query the modules table and
693
+ // re-run the audit so the displayed findings reflect post-upgrade
694
+ // reality. Only do this on a successful run (a partial-failure
695
+ // run keeps its original audit so the operator sees what the
696
+ // orchestrator was reacting to).
697
+ const successfulModuleSteps = result.modules.filter((m) => m.step === 'done');
698
+ if (result.ok && successfulModuleSteps.length > 0) {
699
+ const refreshedAudit = await runAudit(rebuildAuditDepsForRerun(auditDeps, db));
700
+ result.audit = refreshedAudit;
701
+ }
702
+
680
703
  if (json) {
681
704
  if (result.ok) {
682
705
  return { success: true, message: JSON.stringify(result, null, 2), rawOutput: true };
@@ -692,3 +715,84 @@ export async function handleSystemUpdate(
692
715
  error: `${formatResult(result)}\n\none or more modules failed; see output above`,
693
716
  };
694
717
  }
718
+
719
+ type AuditDeps = Parameters<typeof runAudit>[0];
720
+
721
+ /**
722
+ * Build a fresh AuditDeps object whose module-state-dependent
723
+ * sub-deps are re-queried from the DB. The non-module deps
724
+ * (cliVersion fetcher, migrations, terraform plan runner, registry
725
+ * fetcher, etc.) are reused from the original deps because they
726
+ * don't change during a single system-update run.
727
+ *
728
+ * Used by the post-orchestrator re-audit so displayed findings
729
+ * reflect post-upgrade reality (e.g., a module_versions drift
730
+ * finding for a module we just upgraded is no longer reported).
731
+ */
732
+ export function rebuildAuditDepsForRerun(
733
+ original: AuditDeps,
734
+ db: ReturnType<typeof getDb>,
735
+ ): AuditDeps {
736
+ const installed = db.select().from(modules).all();
737
+ const upgradeEligibleStates = new Set(['INSTALLED', 'VERIFIED', 'IMPORTED']);
738
+ const upgradable = installed.filter((m) => upgradeEligibleStates.has(m.state));
739
+
740
+ const allConfigs = db.select().from(moduleConfigsTbl).all();
741
+ const configsByModule = new Map<string, Record<string, unknown>>();
742
+ for (const c of allConfigs) {
743
+ const m = configsByModule.get(c.moduleId) ?? {};
744
+ m[c.key] = c.valueJson ? JSON.parse(c.valueJson) : c.value;
745
+ configsByModule.set(c.moduleId, m);
746
+ }
747
+
748
+ // Backup recency is unchanged across an orchestrator run (only
749
+ // celilo-DB snapshots happen, not per-module backup writes), so
750
+ // we look up each module's prior lastSuccessfulBackupAt by id
751
+ // rather than re-querying the backups table.
752
+ const priorBackupByModule = new Map<string, number | null>();
753
+ for (const b of original.backups.modules) {
754
+ priorBackupByModule.set(b.id, b.lastSuccessfulBackupAt);
755
+ }
756
+
757
+ return {
758
+ ...original,
759
+ capabilityAbi: {
760
+ modules: upgradable.map((m) => ({
761
+ id: m.id,
762
+ state: m.state,
763
+ manifest: m.manifestData as ModuleManifest,
764
+ })),
765
+ },
766
+ moduleVersions: {
767
+ ...original.moduleVersions,
768
+ installed: upgradable.map((m) => ({ id: m.id, version: m.version })),
769
+ },
770
+ moduleConfigs: {
771
+ modules: upgradable.map((m) => ({
772
+ id: m.id,
773
+ state: m.state,
774
+ manifest: m.manifestData as ModuleManifest,
775
+ configs: configsByModule.get(m.id) ?? {},
776
+ })),
777
+ },
778
+ backups: {
779
+ ...original.backups,
780
+ modules: upgradable.map((m) => ({
781
+ id: m.id,
782
+ state: m.state,
783
+ manifest: m.manifestData as ModuleManifest,
784
+ lastSuccessfulBackupAt: priorBackupByModule.get(m.id) ?? null,
785
+ })),
786
+ },
787
+ undeployedModules: {
788
+ modules: installed.map((m) => ({ id: m.id, state: m.state })),
789
+ },
790
+ unconfiguredModules: {
791
+ modules: installed.map((m) => ({
792
+ id: m.id,
793
+ state: m.state,
794
+ configCount: Object.keys(configsByModule.get(m.id) ?? {}).length,
795
+ })),
796
+ },
797
+ };
798
+ }
@@ -24,7 +24,10 @@ function makeModule(
24
24
  hooks: opts.hasBackupHook ? { on_backup: { script: './backup.ts' } } : {},
25
25
  backup: opts.schedule ? { schedule: opts.schedule } : undefined,
26
26
  } as unknown as ModuleManifest;
27
- return { id, manifest, lastSuccessfulBackupAt: opts.lastSuccessfulBackupAt };
27
+ // Default to INSTALLED existing tests assert backup findings
28
+ // fire, which is the deployed-module behavior. Tests for the
29
+ // non-deployed-skip behavior override this explicitly.
30
+ return { id, state: 'INSTALLED', manifest, lastSuccessfulBackupAt: opts.lastSuccessfulBackupAt };
28
31
  }
29
32
 
30
33
  describe('auditBackups', () => {
@@ -230,4 +233,24 @@ describe('auditBackups', () => {
230
233
  expect(result).toHaveLength(2);
231
234
  expect(result.map((f) => f.subject).sort()).toEqual(['authentik', 'homebridge']);
232
235
  });
236
+
237
+ // The regression: backups were complaining about IMPORTED modules
238
+ // having no successful backup. There's no live state on an
239
+ // IMPORTED module, so there's nothing to back up — skip entirely
240
+ // rather than emit a misleading finding.
241
+ test('skips non-deployed (IMPORTED) modules entirely', async () => {
242
+ const importedModule = {
243
+ ...makeModule('authentik', {
244
+ hasBackupHook: true,
245
+ lastSuccessfulBackupAt: null,
246
+ schedule: 'daily',
247
+ }),
248
+ state: 'IMPORTED',
249
+ };
250
+ const result = await auditBackups({
251
+ modules: [importedModule],
252
+ now: () => NOW,
253
+ });
254
+ expect(result).toEqual([]);
255
+ });
233
256
  });
@@ -20,6 +20,8 @@ import type { DriftFinding } from './types';
20
20
 
21
21
  export interface InstalledModuleBackupInfo {
22
22
  id: string;
23
+ /** Lifecycle state — non-deployed modules have nothing to back up. */
24
+ state: string;
23
25
  manifest: ModuleManifest;
24
26
  /** Most recent successful backup timestamp (ms since epoch), or null. */
25
27
  lastSuccessfulBackupAt: number | null;
@@ -87,12 +89,20 @@ function formatAge(ms: number): string {
87
89
  return `${mins}m`;
88
90
  }
89
91
 
92
+ const DEPLOYED_STATES = new Set(['INSTALLED', 'VERIFIED']);
93
+
90
94
  export async function auditBackups(deps: BackupsAuditDeps): Promise<DriftFinding[]> {
91
95
  const now = (deps.now ?? Date.now)();
92
96
  const findings: DriftFinding[] = [];
93
97
 
94
98
  for (const m of deps.modules) {
95
99
  if (!moduleHasBackupHook(m.manifest)) continue;
100
+ // Non-deployed modules have no live state to back up. Surfacing
101
+ // a "no successful backup recorded" finding for an IMPORTED
102
+ // module is just noise — the operator hasn't deployed yet, so
103
+ // there's nothing to lose. Skip them entirely from the backup
104
+ // audit.
105
+ if (!DEPLOYED_STATES.has(m.state)) continue;
96
106
 
97
107
  if (m.lastSuccessfulBackupAt === null) {
98
108
  findings.push({
@@ -23,7 +23,10 @@ function makeModule(
23
23
  requires: opts.requires ? { capabilities: opts.requires } : { capabilities: [] },
24
24
  optional: opts.optional ? { capabilities: opts.optional } : undefined,
25
25
  } as unknown as ModuleManifest;
26
- return { id, manifest };
26
+ // Default INSTALLED so existing assertions keep their `blocked`
27
+ // semantics. Tests that exercise the IMPORTED-demotion path
28
+ // override the state explicitly.
29
+ return { id, state: 'INSTALLED', manifest };
27
30
  }
28
31
 
29
32
  const RUNTIME = {
@@ -26,9 +26,19 @@ import type { DriftFinding } from './types';
26
26
 
27
27
  export interface InstalledCapabilityModule {
28
28
  id: string;
29
+ /**
30
+ * Lifecycle state — used to demote ABI mismatches on IMPORTED-but-
31
+ * not-deployed modules from `blocked` to `todo`. The mismatch is
32
+ * still real (the operator will hit it at deploy time) but it
33
+ * doesn't block other module work, and gating system update on it
34
+ * makes refreshing unrelated modules harder than it should be.
35
+ */
36
+ state: string;
29
37
  manifest: ModuleManifest;
30
38
  }
31
39
 
40
+ const DEPLOYED_STATES = new Set(['INSTALLED', 'VERIFIED']);
41
+
32
42
  export interface CapabilityAbiAuditDeps {
33
43
  modules: InstalledCapabilityModule[];
34
44
  /** Override the framework registry — for tests. Defaults to `CAPABILITY_CONTRACT_VERSIONS`. */
@@ -120,7 +130,11 @@ export async function auditCapabilityAbi(deps: CapabilityAbiAuditDeps): Promise<
120
130
 
121
131
  findings.push({
122
132
  category: 'capability_abi',
123
- severity: 'blocked',
133
+ // Demoted to `todo` for non-deployed modules: the mismatch
134
+ // matters at deploy time, but blocking system update on it
135
+ // means the operator can't refresh unrelated modules until
136
+ // they fix the ABI for a module they haven't deployed yet.
137
+ severity: DEPLOYED_STATES.has(m.state) ? 'blocked' : 'todo',
124
138
  code: 'capability_abi_provider_mismatch',
125
139
  message: `${m.id} provides ${p.name}@${p.version} but framework runtime expects ${runtimeVersion}`,
126
140
  details,
@@ -190,7 +204,9 @@ export async function auditCapabilityAbi(deps: CapabilityAbiAuditDeps): Promise<
190
204
 
191
205
  findings.push({
192
206
  category: 'capability_abi',
193
- severity: 'blocked',
207
+ // Same demotion as the provider check — non-deployed
208
+ // modules surface as todos rather than blockers.
209
+ severity: DEPLOYED_STATES.has(m.state) ? 'blocked' : 'todo',
194
210
  code: 'capability_abi_consumer_mismatch',
195
211
  message: `${m.id} requires ${need.name}@${need.version} but ${provider.moduleId} provides ${provider.version}`,
196
212
  details,
@@ -24,7 +24,10 @@ function makeModule(
24
24
  celilo_contract: '1.0',
25
25
  variables: { owns: variables, imports: [] },
26
26
  } as unknown as ModuleManifest;
27
- return { id, manifest, configs };
27
+ // Default state INSTALLED so existing tests continue to expect
28
+ // `blocked` severity. Tests that exercise the IMPORTED path
29
+ // override the state explicitly.
30
+ return { id, state: 'INSTALLED', manifest, configs };
28
31
  }
29
32
 
30
33
  describe('auditModuleConfigs', () => {
@@ -128,4 +131,47 @@ describe('auditModuleConfigs', () => {
128
131
  expect(result).toHaveLength(1);
129
132
  expect(result[0].subject).toBe('caddy');
130
133
  });
134
+
135
+ // The exact regression the operator hit on celilo-mgmt: required
136
+ // configs missing on IMPORTED-but-not-yet-deployed modules used to
137
+ // surface as `blocked`, gating system update entirely. The deploy
138
+ // interview collects them at deploy time, so they're todos here.
139
+ test('IMPORTED module with missing required config → todo, not blocked', async () => {
140
+ const moduleWithImportedState = {
141
+ ...makeModule('namecheap', [makeVariable({ name: 'domains', required: true })], {}),
142
+ state: 'IMPORTED',
143
+ };
144
+ const result = await auditModuleConfigs({
145
+ modules: [moduleWithImportedState],
146
+ });
147
+ expect(result).toHaveLength(1);
148
+ expect(result[0].severity).toBe('todo');
149
+ // Remediation also flips: don't tell the operator to manually
150
+ // `module config set` (the deploy interview is the right path).
151
+ expect(result[0].remediation).toBe('celilo module deploy namecheap');
152
+ });
153
+
154
+ test('VALIDATED / GENERATING / other pre-deploy states also demote to todo', async () => {
155
+ const validated = {
156
+ ...makeModule('foo', [makeVariable({ name: 'x', required: true })], {}),
157
+ state: 'VALIDATED',
158
+ };
159
+ const generating = {
160
+ ...makeModule('bar', [makeVariable({ name: 'x', required: true })], {}),
161
+ state: 'GENERATING',
162
+ };
163
+ const result = await auditModuleConfigs({ modules: [validated, generating] });
164
+ expect(result).toHaveLength(2);
165
+ expect(result.every((f) => f.severity === 'todo')).toBe(true);
166
+ });
167
+
168
+ test('VERIFIED state stays blocked (deployed module)', async () => {
169
+ const verified = {
170
+ ...makeModule('caddy', [makeVariable({ name: 'acme_email', required: true })], {}),
171
+ state: 'VERIFIED',
172
+ };
173
+ const result = await auditModuleConfigs({ modules: [verified] });
174
+ expect(result).toHaveLength(1);
175
+ expect(result[0].severity).toBe('blocked');
176
+ });
131
177
  });
@@ -8,20 +8,30 @@
8
8
  * the user.
9
9
  *
10
10
  * Severity rules:
11
- * - `required: true` AND no current value AND no `default:` → BLOCKED
12
- * (the user has to set it; `system update` would otherwise fail at
13
- * deploy time anyway).
14
- * - All other unset cases no finding. An optional variable with a
15
- * default is by definition fine when unset (the default applies);
16
- * a required variable with a default is also fine (the default
17
- * resolves the value). Either case as drift would just be noise.
11
+ * - DEPLOYED module (state=INSTALLED|VERIFIED) with `required: true`
12
+ * AND no current value AND no `default:` BLOCKED. The module is
13
+ * live and a config gap is a real divergence: the next deploy
14
+ * would fail, capability consumers may be reading the unset value,
15
+ * etc.
16
+ * - NON-DEPLOYED module (IMPORTED, etc.) TODO. The deploy
17
+ * interview will collect required values when the operator
18
+ * eventually runs `module deploy`. Telling them to manually
19
+ * `module config set` is bad UX — the interview is the canonical
20
+ * path. Surfacing as TODO keeps the visibility without escalating
21
+ * the verdict.
22
+ * - Variable has a `default:` (any state) → no finding. The default
23
+ * resolves the value.
18
24
  */
19
25
 
20
26
  import type { ModuleManifest, VariableDeclare } from '../../manifest/schema';
21
27
  import type { DriftFinding } from './types';
22
28
 
29
+ const DEPLOYED_STATES = new Set(['INSTALLED', 'VERIFIED']);
30
+
23
31
  export interface InstalledModuleConfig {
24
32
  id: string;
33
+ /** Lifecycle state from the modules table — drives severity. */
34
+ state: string;
25
35
  manifest: ModuleManifest;
26
36
  /** Map of config key → current value (string for primitives, parsed object for complex). */
27
37
  configs: Record<string, unknown>;
@@ -37,6 +47,7 @@ function isUnset(value: unknown): boolean {
37
47
 
38
48
  function ownVariableFindings(
39
49
  moduleId: string,
50
+ state: string,
40
51
  variable: VariableDeclare,
41
52
  currentValue: unknown,
42
53
  ): DriftFinding[] {
@@ -47,23 +58,30 @@ function ownVariableFindings(
47
58
  if (!isUnset(currentValue)) return [];
48
59
 
49
60
  const hasDefault = variable.default !== undefined && variable.default !== null;
61
+ if (!variable.required || hasDefault) return [];
50
62
 
51
- if (variable.required && !hasDefault) {
52
- return [
53
- {
54
- category: 'module_configs',
55
- severity: 'blocked',
56
- code: 'module_config_required_unset',
57
- message: `${moduleId}: required config "${variable.name}" is not set`,
58
- details: variable.description,
59
- remediation: `celilo module config set ${moduleId} ${variable.name} <value>`,
60
- actionable: true,
61
- subject: moduleId,
62
- },
63
- ];
64
- }
63
+ // BLOCKED only when the module is currently deployed. For
64
+ // IMPORTED-and-similar pre-deploy states, the deploy interview
65
+ // collects this value automatically — surfacing as a blocker
66
+ // wrongly directs the operator at `module config set` (a manual
67
+ // workaround) when the right command is `module deploy <id>`.
68
+ const severity = DEPLOYED_STATES.has(state) ? 'blocked' : 'todo';
69
+ const remediation = DEPLOYED_STATES.has(state)
70
+ ? `celilo module config set ${moduleId} ${variable.name} <value>`
71
+ : `celilo module deploy ${moduleId}`;
65
72
 
66
- return [];
73
+ return [
74
+ {
75
+ category: 'module_configs',
76
+ severity,
77
+ code: 'module_config_required_unset',
78
+ message: `${moduleId}: required config "${variable.name}" is not set`,
79
+ details: variable.description,
80
+ remediation,
81
+ actionable: true,
82
+ subject: moduleId,
83
+ },
84
+ ];
67
85
  }
68
86
 
69
87
  export async function auditModuleConfigs(deps: ModuleConfigsAuditDeps): Promise<DriftFinding[]> {
@@ -72,7 +90,7 @@ export async function auditModuleConfigs(deps: ModuleConfigsAuditDeps): Promise<
72
90
  for (const m of deps.modules) {
73
91
  const owned = m.manifest.variables?.owns ?? [];
74
92
  for (const variable of owned) {
75
- findings.push(...ownVariableFindings(m.id, variable, m.configs[variable.name]));
93
+ findings.push(...ownVariableFindings(m.id, m.state, variable, m.configs[variable.name]));
76
94
  }
77
95
  }
78
96