@clear-capabilities/agentic-security-scanner 0.153.2 → 0.155.0
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/CHANGELOG.md +47 -0
- package/bin/agentic-security.js +131 -19
- package/dist/1444.index.js +64 -19
- package/dist/1905.index.js +46 -3
- package/dist/1919.index.js +445 -0
- package/dist/2862.index.js +325 -0
- package/dist/3499.index.js +5 -1
- package/dist/3531.index.js +261 -0
- package/dist/3596.index.js +103 -0
- package/dist/4113.index.js +15 -3
- package/dist/4171.index.js +319 -0
- package/dist/5637.index.js +10 -4
- package/dist/6359.index.js +433 -0
- package/dist/7838.index.js +15 -3
- package/dist/8551.index.js +1192 -0
- package/dist/8587.index.js +410 -0
- package/dist/8714.index.js +98 -0
- package/dist/8798.index.js +218 -0
- package/dist/9220.index.js +35 -3
- package/dist/9560.index.js +26 -3
- package/dist/agentic-security.mjs +51 -51
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +20 -3
- package/src/dataflow/CLAUDE.md +9 -0
- package/src/dataflow/catalog-haskell.js +37 -0
- package/src/dataflow/catalog.js +4 -0
- package/src/dataflow/engine.js +33 -3
- package/src/dataflow/privacy-catalog-haskell.js +50 -0
- package/src/dataflow/privacy-catalog.js +5 -1
- package/src/discovery/CLAUDE.md +6 -0
- package/src/discovery/index.js +12 -3
- package/src/discovery/lenses.js +4 -1
- package/src/discovery/partition.js +28 -0
- package/src/egress/redact.js +8 -0
- package/src/engine.js +125 -14
- package/src/ir/index.js +39 -2
- package/src/language/CLAUDE.md +74 -0
- package/src/language/accuracy.js +90 -0
- package/src/language/aibom.js +307 -0
- package/src/language/assurance.js +193 -0
- package/src/language/bom-validate.js +227 -0
- package/src/language/bom.js +236 -0
- package/src/language/bridges.js +392 -0
- package/src/language/compliance-map.js +61 -0
- package/src/language/context.js +199 -0
- package/src/language/contracts.js +339 -0
- package/src/language/discovery.js +359 -0
- package/src/language/engine-pass.js +120 -0
- package/src/language/fix-lifecycle.js +120 -0
- package/src/language/haskell-adapter.js +51 -0
- package/src/language/haskell-disclosure.js +170 -0
- package/src/language/haskell-findings.js +44 -0
- package/src/language/haskell-fix.js +409 -0
- package/src/language/haskell-grammar.js +78 -0
- package/src/language/haskell-guards.js +382 -0
- package/src/language/haskell-ir.js +1377 -0
- package/src/language/haskell-llm.js +128 -0
- package/src/language/haskell-manifests.js +1290 -0
- package/src/language/haskell-models.js +278 -0
- package/src/language/haskell-parser.js +1073 -0
- package/src/language/haskell-resolved-graph.js +414 -0
- package/src/language/haskell-sca.js +387 -0
- package/src/language/haskell-security-rules.js +332 -0
- package/src/language/haskell-supply.js +140 -0
- package/src/language/haskell-sweep.js +85 -0
- package/src/language/haskell-syntax.js +895 -0
- package/src/language/haskell-web.js +749 -0
- package/src/language/nix-adapter.js +42 -0
- package/src/language/nix-agents.js +155 -0
- package/src/language/nix-build-trust.js +790 -0
- package/src/language/nix-closure.js +331 -0
- package/src/language/nix-eval-isolation.js +274 -0
- package/src/language/nix-fix.js +268 -0
- package/src/language/nix-grammar.js +97 -0
- package/src/language/nix-inventory.js +379 -0
- package/src/language/nix-ir.js +642 -0
- package/src/language/nix-parser.js +908 -0
- package/src/language/nix-privacy.js +114 -0
- package/src/language/nix-sca.js +358 -0
- package/src/language/nix-script-taint.js +885 -0
- package/src/language/nix-secrets.js +396 -0
- package/src/language/nixos-hardening.js +564 -0
- package/src/language/nixos-module-resolver.js +595 -0
- package/src/language/nixos-option-catalog.js +96 -0
- package/src/language/pragma.js +150 -0
- package/src/language/resolved-pass.js +174 -0
- package/src/language/secrets.js +323 -0
- package/src/language/state-artifacts.js +78 -0
- package/src/language/support-registry.js +289 -0
- package/src/language/trusted-inputs.js +38 -0
- package/src/language/witness.js +185 -0
- package/src/lineage/CLAUDE.md +8 -0
- package/src/lineage/coverage.js +2 -0
- package/src/lineage/cross-repo-link.js +22 -0
- package/src/lineage/graph-builder.js +5 -1
- package/src/lineage/haskell-view.js +319 -0
- package/src/lineage/index.js +8 -1
- package/src/lineage/nix-view.js +198 -0
- package/src/lineage/sink-registry.js +10 -1
- package/src/lineage/source-registry.js +3 -0
- package/src/lineage/transform-catalog.js +12 -0
- package/src/llm-validator/index.js +16 -2
- package/src/lsp/server.js +69 -9
- package/src/mcp/CLAUDE.md +7 -0
- package/src/mcp/tools.js +26 -3
- package/src/pipeline/coverage-ledger.js +0 -0
- package/src/pipeline/scan-health.js +19 -1
- package/src/posture/accuracy-scorecard.js +51 -0
- package/src/posture/aibom.js +61 -3
- package/src/posture/artifact-registry.js +12 -0
- package/src/posture/auditor-walkthrough.js +41 -0
- package/src/posture/clustering.js +2 -2
- package/src/posture/evidence-bundle.js +7 -1
- package/src/posture/execution-proof.js +15 -0
- package/src/posture/falsification.js +7 -0
- package/src/posture/family-resolve.js +7 -0
- package/src/posture/fix-history.js +40 -0
- package/src/posture/fix-verify.js +15 -3
- package/src/posture/provenance/git-evidence.js +24 -0
- package/src/posture/provenance/origin-resolver.js +14 -2
- package/src/posture/provenance/predicate-replay.js +11 -2
- package/src/posture/provenance/sca-origin.js +20 -3
- package/src/posture/sbom-diff.js +7 -6
- package/src/posture/sbom.js +85 -32
- package/src/posture/sca-policy.js +10 -1
- package/src/posture/sca-verdict.js +8 -1
- package/src/posture/secret-history.js +62 -18
- package/src/posture/stable-id.js +21 -2
- package/src/posture/state-dir.js +21 -0
- package/src/posture/threat-model-auto.js +4 -4
- package/src/posture/triage.js +18 -1
- package/src/posture/watch-mode.js +34 -3
- package/src/pr-delta.js +9 -4
- package/src/report/index.js +35 -9
- package/src/runScan.js +56 -5
- package/src/sast/pipeline.js +34 -1
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
// Bounded effective NixOS / Home Manager configuration reasoning (NIX-002).
|
|
2
|
+
//
|
|
3
|
+
// Documented static subset. Given entry module file(s) it follows LITERAL local
|
|
4
|
+
// imports (nix-ir's bounded import graph, never `nix`), collects every assignment of
|
|
5
|
+
// each option, and decides the effective value by the module system's own rules:
|
|
6
|
+
//
|
|
7
|
+
// * priorities: mkForce 50, plain 100, mkDefault 1000, mkOptionDefault 1500,
|
|
8
|
+
// mkOverride N (literal N only), lower number wins; mkMerge/mkBefore/mkAfter do
|
|
9
|
+
// not change priority;
|
|
10
|
+
// * conditions: `mkIf`, `optionalAttrs`, `if/else`, judged by a small evaluator
|
|
11
|
+
// (booleans, strings, ints, `!`, `&&`, `||`, `->`, `==`, `!=`, references to
|
|
12
|
+
// `config.<option>` and let-aliases of them, `pkgs.stdenv.isLinux`-style platform
|
|
13
|
+
// flags from `target.system`, and function arguments supplied in `target.args`).
|
|
14
|
+
// Anything else is UNKNOWN, and an unknown condition keeps its definition
|
|
15
|
+
// "conditional", it is never assumed true or false;
|
|
16
|
+
// * list options merge; scalar options with equal-priority different values are a
|
|
17
|
+
// `conflict`; an option whose type is not cataloged with such a disagreement is
|
|
18
|
+
// `unresolved-precedence`. A winner is never guessed.
|
|
19
|
+
//
|
|
20
|
+
// A missing setting gets a default only when the catalog PROVES it for the release in
|
|
21
|
+
// force (or for every cataloged release). The release comes from `target.release` or a
|
|
22
|
+
// flake `nixpkgs` input, NEVER from `system.stateVersion`, which is a state
|
|
23
|
+
// compatibility marker and is reported separately.
|
|
24
|
+
//
|
|
25
|
+
// Home Manager options live in their own namespace, scoped per user
|
|
26
|
+
// (`home-manager.users.<u>` or a module imported from it), and never merge with NixOS
|
|
27
|
+
// options of the same spelling.
|
|
28
|
+
//
|
|
29
|
+
// Evaluation is capped (`DEFAULT_RESOLVE_BUDGETS`); a cap reports `truncated` and the
|
|
30
|
+
// affected option is `unknown`. Cyclic references converge to `unknown` the same way.
|
|
31
|
+
|
|
32
|
+
import * as path from 'node:path';
|
|
33
|
+
import { parseNix, spanText } from './nix-parser.js';
|
|
34
|
+
import { analyzeNix, buildNixImportGraph } from './nix-ir.js';
|
|
35
|
+
import { defaultCatalog, normalizeRelease } from './nixos-option-catalog.js';
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_RESOLVE_BUDGETS = Object.freeze({
|
|
38
|
+
maxEvaluations: 20_000,
|
|
39
|
+
maxEvalDepth: 24,
|
|
40
|
+
maxDefinitionsPerOption: 500,
|
|
41
|
+
maxFiles: 200,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const PRIORITY = { mkForce: 50, mkVMOverride: 10, mkDefault: 1000, mkOptionDefault: 1500, mkBefore: 100, mkAfter: 100 };
|
|
45
|
+
const SKIP_TOP = new Set(['options', 'meta', 'imports', '_module', 'inputs', 'outputs', 'description', 'disabledModules']);
|
|
46
|
+
const BLOCKER_KINDS = new Set(['dynamic-import', 'missing-import', 'import-cycle', 'syntax-error', 'budget-exceeded', 'import-budget', 'import-depth', 'ir-budget', 'missing-grammar', 'parse-failed', 'opaque-config', 'flake-modules-not-followed']);
|
|
47
|
+
const UNK = Object.freeze({ known: false });
|
|
48
|
+
const known = (value) => ({ known: true, value });
|
|
49
|
+
|
|
50
|
+
class Cap extends Error {
|
|
51
|
+
constructor(budget, limit) { super(`budget ${budget}`); this.budget = budget; this.limit = limit; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const unparen = (n) => { let x = n; while (x && x.type === 'paren') x = x.expr; return x; };
|
|
55
|
+
const sameValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
56
|
+
const within = (outer, inner) => outer && inner && outer.startOffset <= inner.startOffset && inner.endOffset <= outer.endOffset;
|
|
57
|
+
|
|
58
|
+
function platformFlags(system) {
|
|
59
|
+
if (typeof system !== 'string' || !system.includes('-')) return null;
|
|
60
|
+
const [cpu, os] = system.split('-');
|
|
61
|
+
return { isLinux: os === 'linux', isDarwin: os === 'darwin', isx86_64: cpu === 'x86_64', isAarch64: cpu === 'aarch64' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class Resolver {
|
|
65
|
+
constructor(opts) {
|
|
66
|
+
this.opts = opts;
|
|
67
|
+
this.b = { ...DEFAULT_RESOLVE_BUDGETS, ...(opts.budgets || {}) };
|
|
68
|
+
this.catalog = opts.catalog || defaultCatalog();
|
|
69
|
+
this.target = opts.target || {};
|
|
70
|
+
this.irs = new Map();
|
|
71
|
+
this.sources = new Map();
|
|
72
|
+
this.fileNs = new Map();
|
|
73
|
+
this.unresolved = [];
|
|
74
|
+
this.truncated = [];
|
|
75
|
+
this.defs = new Map();
|
|
76
|
+
this.opaque = [];
|
|
77
|
+
this.dynamicPrefixes = [];
|
|
78
|
+
this.overlays = [];
|
|
79
|
+
this.unknownOptions = new Map();
|
|
80
|
+
this.renamed = new Map();
|
|
81
|
+
this.memo = new Map();
|
|
82
|
+
this.stack = new Set();
|
|
83
|
+
this.parseCache = new Map();
|
|
84
|
+
this.steps = 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
gap(g) { this.unresolved.push(g); }
|
|
88
|
+
|
|
89
|
+
cap(name, limit, detail) {
|
|
90
|
+
if (!this.truncated.some((t) => t.budget === name)) this.truncated.push({ budget: name, limit, detail });
|
|
91
|
+
return new Cap(name, limit);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── loading ──
|
|
95
|
+
|
|
96
|
+
read(file) {
|
|
97
|
+
const { readFile, files } = this.opts;
|
|
98
|
+
if (files) return Object.prototype.hasOwnProperty.call(files, file) ? files[file] : null;
|
|
99
|
+
return readFile ? readFile(file) : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
load() {
|
|
103
|
+
const entries = [].concat(this.opts.entry || []).map((e) => path.posix.normalize(String(e).split(path.sep).join('/')));
|
|
104
|
+
const edges = [];
|
|
105
|
+
const order = [];
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
const graph = buildNixImportGraph(entry, { root: this.opts.root, readFile: (p) => this.read(p), budgets: { maxFiles: this.b.maxFiles } });
|
|
108
|
+
for (const u of graph.unresolved) this.gap({ ...u });
|
|
109
|
+
if (graph.truncated) this.truncated.push({ budget: 'maxFiles', limit: this.b.maxFiles, detail: 'import graph truncated' });
|
|
110
|
+
for (const e of graph.edges) edges.push(e);
|
|
111
|
+
for (const f of graph.files) if (!order.includes(f.file)) order.push(f.file);
|
|
112
|
+
}
|
|
113
|
+
for (const file of order) {
|
|
114
|
+
const text = this.read(file);
|
|
115
|
+
if (text === null || text === undefined) continue;
|
|
116
|
+
const { ir } = analyzeNix(text, { file });
|
|
117
|
+
this.irs.set(file, ir);
|
|
118
|
+
this.sources.set(file, text);
|
|
119
|
+
if (ir.fileKind === 'flake') this.gap({ kind: 'flake-modules-not-followed', detail: `${file} is a flake; its module lists are not followed, pass the module files as entries`, file, span: null });
|
|
120
|
+
}
|
|
121
|
+
// namespace per file: entries are NixOS unless declared otherwise; imports from a home-manager user binding are that user's
|
|
122
|
+
const entryNs = this.target.namespace === 'home-manager' ? { namespace: 'home-manager', scope: this.target.user || 'standalone' } : { namespace: 'nixos', scope: null };
|
|
123
|
+
for (const e of entries) if (this.irs.has(e)) this.fileNs.set(e, entryNs);
|
|
124
|
+
const queue = entries.filter((e) => this.irs.has(e));
|
|
125
|
+
while (queue.length) {
|
|
126
|
+
const from = queue.shift();
|
|
127
|
+
const base = this.fileNs.get(from);
|
|
128
|
+
for (const e of edges.filter((x) => x.from === from)) {
|
|
129
|
+
if (this.fileNs.has(e.to) || !this.irs.has(e.to)) continue;
|
|
130
|
+
const scope = base.namespace === 'nixos' ? this.homeScopeOf(this.irs.get(from), e.span) : null;
|
|
131
|
+
this.fileNs.set(e.to, scope ? { namespace: 'home-manager', scope } : base);
|
|
132
|
+
queue.push(e.to);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
this.graphEdges = edges.map((e) => ({ from: e.from, to: e.to, kind: e.kind }));
|
|
136
|
+
this.order = order.filter((f) => this.fileNs.has(f));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
homeScopeOf(ir, span) {
|
|
140
|
+
let best = null;
|
|
141
|
+
for (const b of ir.bindings) {
|
|
142
|
+
if (b.scope !== 'file' || b.origin === 'let' || b.path[0] !== 'home-manager' || b.path[1] !== 'users' || typeof b.path[2] !== 'string') continue;
|
|
143
|
+
if (!within(b.span, span)) continue;
|
|
144
|
+
if (!best || (b.span.endOffset - b.span.startOffset) < (best.span.endOffset - best.span.startOffset)) best = b;
|
|
145
|
+
}
|
|
146
|
+
return best ? best.path[2] : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── definitions ──
|
|
150
|
+
|
|
151
|
+
collect() {
|
|
152
|
+
let id = 0;
|
|
153
|
+
for (const file of this.order) {
|
|
154
|
+
const ir = this.irs.get(file);
|
|
155
|
+
const base = this.fileNs.get(file);
|
|
156
|
+
for (const o of ir.overlays) this.overlays.push({ file, kind: o.kind, span: o.span, params: o.params || [], bindings: o.bindings });
|
|
157
|
+
for (const b of ir.bindings) {
|
|
158
|
+
if (b.scope !== 'file' || b.origin === 'let' || b.overridden || !b.path.length) continue;
|
|
159
|
+
if (b.dynamic) {
|
|
160
|
+
const cut = b.path.indexOf(null);
|
|
161
|
+
this.dynamicPrefixes.push({ prefix: b.path.slice(0, cut), file, span: b.span, namespace: base.namespace });
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
let p = b.path;
|
|
165
|
+
if (p[0] === 'config' && p.length > 1) p = p.slice(1);
|
|
166
|
+
else if (p[0] === 'config') {
|
|
167
|
+
if (b.value.type !== 'attrset') { this.opaque.push({ prefix: [], namespace: base.namespace, scope: base.scope, file, span: b.span }); this.gap({ kind: 'opaque-config', detail: 'the module `config` is not an attribute set literal and is not followed', file, span: b.span }); }
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (SKIP_TOP.has(p[0])) continue;
|
|
171
|
+
let ns = base.namespace;
|
|
172
|
+
let scope = base.scope;
|
|
173
|
+
if (ns === 'nixos' && p[0] === 'home-manager' && p[1] === 'users' && typeof p[2] === 'string') {
|
|
174
|
+
if (p.length === 3) {
|
|
175
|
+
if (b.value.type === 'function') this.gap({ kind: 'home-manager-function-module', detail: `home-manager.users.${p[2]} is a function module whose body is not followed`, file, span: b.span });
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
ns = 'home-manager'; scope = p[2]; p = p.slice(3);
|
|
179
|
+
if (SKIP_TOP.has(p[0])) continue;
|
|
180
|
+
if (p[0] === 'config' && p.length > 1) p = p.slice(1);
|
|
181
|
+
}
|
|
182
|
+
let pathText = p.join('.');
|
|
183
|
+
let viaRename = null;
|
|
184
|
+
const cat = ns === 'nixos' ? this.catalog.nixos : this.catalog.homeManager;
|
|
185
|
+
if (cat.renames[pathText]) { viaRename = { from: pathText, to: cat.renames[pathText] }; pathText = cat.renames[pathText]; p = pathText.split('.'); }
|
|
186
|
+
const def = {
|
|
187
|
+
id: id++, namespace: ns, scope, path: p, pathText, file, span: b.span, valueSpan: b.valueSpan, summary: b.value,
|
|
188
|
+
priorityName: b.priority || 'plain', priorityArg: b.priorityArg || null, conditions: b.conditions || [], merge: b.merge || null, viaRename,
|
|
189
|
+
};
|
|
190
|
+
const key = this.key(ns, scope, pathText);
|
|
191
|
+
if (!this.defs.has(key)) this.defs.set(key, []);
|
|
192
|
+
this.defs.get(key).push(def);
|
|
193
|
+
if (b.value.type !== 'attrset' && !['bool', 'string', 'number', 'null', 'list', 'path', 'uri', 'inherit'].includes(b.value.type)) {
|
|
194
|
+
this.opaque.push({ prefix: p, namespace: ns, scope, file, span: b.span, selfDef: true });
|
|
195
|
+
}
|
|
196
|
+
const info = this.catalogInfo(ns, pathText);
|
|
197
|
+
if (viaRename) { const k = `${ns}:${viaRename.from}`; if (!this.renamed.has(k)) this.renamed.set(k, { from: viaRename.from, to: viaRename.to, namespace: ns, sources: [] }); this.renamed.get(k).sources.push({ file, span: b.span }); }
|
|
198
|
+
if (info.status === 'unknown') { const k = `${ns}:${pathText}`; if (!this.unknownOptions.has(k)) this.unknownOptions.set(k, { path: pathText, namespace: ns, authoritativePrefix: info.prefix, sources: [] }); this.unknownOptions.get(k).sources.push({ file, span: b.span }); }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
key(ns, scope, pathText) { return `${ns}|${scope || ''}|${pathText}`; }
|
|
204
|
+
|
|
205
|
+
catalogInfo(ns, pathText) {
|
|
206
|
+
const cat = ns === 'nixos' ? this.catalog.nixos : this.catalog.homeManager;
|
|
207
|
+
const entry = cat.options[pathText];
|
|
208
|
+
if (entry) return { status: 'known', entry };
|
|
209
|
+
if (cat.renames[pathText]) return { status: 'renamed', entry: cat.options[cat.renames[pathText]] || null };
|
|
210
|
+
if (cat.freeform.some((f) => pathText.startsWith(`${f}.`))) return { status: 'freeform', entry: null };
|
|
211
|
+
const prefix = cat.authoritative.find((a) => pathText === a || pathText.startsWith(`${a}.`));
|
|
212
|
+
if (prefix) return { status: 'unknown', entry: null, prefix };
|
|
213
|
+
return { status: 'uncataloged', entry: null };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── release / defaults ──
|
|
217
|
+
|
|
218
|
+
determineRelease() {
|
|
219
|
+
if (this.target.release) {
|
|
220
|
+
const r = normalizeRelease(this.target.release);
|
|
221
|
+
return r ? { release: r, source: 'target.release' } : { release: null, source: 'unparseable-target-release' };
|
|
222
|
+
}
|
|
223
|
+
if (this.opts.flake && this.read(this.opts.flake) != null) {
|
|
224
|
+
const { ir } = analyzeNix(this.read(this.opts.flake), { file: this.opts.flake });
|
|
225
|
+
for (const inp of (ir.flake && ir.flake.inputs) || []) {
|
|
226
|
+
if (!/nixpkgs/i.test(inp.name) || !inp.url) continue;
|
|
227
|
+
const r = normalizeRelease(inp.url.replace(/^.*nixpkgs\//i, '/'));
|
|
228
|
+
if (r) return { release: r, source: `flake-input:${inp.name}` };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return { release: null, source: 'unknown' };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
defaultFor(info) {
|
|
235
|
+
const d = info.entry && info.entry.default;
|
|
236
|
+
if (!d) return { known: false, reason: 'no-cataloged-default' };
|
|
237
|
+
const rel = this.release.release;
|
|
238
|
+
if (rel) {
|
|
239
|
+
if (!this.catalog.releases.includes(rel)) return { known: false, reason: `release-${rel}-not-in-catalog` };
|
|
240
|
+
if (!(rel in d)) return { known: false, reason: `no-default-recorded-for-${rel}` };
|
|
241
|
+
return { known: true, value: d[rel], proof: `catalog default for NixOS ${rel}` };
|
|
242
|
+
}
|
|
243
|
+
const vals = this.catalog.releases.map((r) => d[r]);
|
|
244
|
+
if (vals.every((v) => v !== undefined && sameValue(v, vals[0]))) return { known: true, value: vals[0], proof: 'identical default in every cataloged release (release not determined)' };
|
|
245
|
+
return { known: false, reason: 'default-differs-by-release-and-release-unknown' };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── expression evaluation ──
|
|
249
|
+
|
|
250
|
+
tick() {
|
|
251
|
+
if (++this.steps > this.b.maxEvaluations) throw this.cap('maxEvaluations', this.b.maxEvaluations, 'static evaluation step budget exhausted');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
parseAt(file, span) {
|
|
255
|
+
if (!span) return null;
|
|
256
|
+
const k = `${file}:${span.startOffset}:${span.endOffset}`;
|
|
257
|
+
if (this.parseCache.has(k)) return this.parseCache.get(k);
|
|
258
|
+
const src = this.sources.get(file);
|
|
259
|
+
const text = src ? spanText(src, span) : null;
|
|
260
|
+
const r = text ? parseNix(text, { file }) : null;
|
|
261
|
+
const ast = r && r.ast && r.status === 'ok' ? r.ast : null;
|
|
262
|
+
this.parseCache.set(k, ast);
|
|
263
|
+
return ast;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
alias(file, name, guard) {
|
|
267
|
+
const ir = this.irs.get(file);
|
|
268
|
+
const hits = ir ? ir.bindings.filter((b) => b.origin === 'let' && b.path.length === 1 && b.path[0] === name) : [];
|
|
269
|
+
if (hits.length !== 1 || guard.has(`${file}:${name}`)) return null;
|
|
270
|
+
guard.add(`${file}:${name}`);
|
|
271
|
+
return this.parseAt(file, hits[0].valueSpan);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** `config.a.b`, or `cfg.b` where `cfg = config.a`, as ['a','b']; otherwise null. */
|
|
275
|
+
configPathOf(node, env, guard = new Set()) {
|
|
276
|
+
const n = unparen(node);
|
|
277
|
+
if (!n) return null;
|
|
278
|
+
let base; let segs = [];
|
|
279
|
+
if (n.type === 'select') {
|
|
280
|
+
if (n.default || n.attrpath.some((s) => s.kind !== 'static')) return null;
|
|
281
|
+
base = unparen(n.base); segs = n.attrpath.map((s) => s.name);
|
|
282
|
+
} else base = n;
|
|
283
|
+
if (!base || base.type !== 'ident') return null;
|
|
284
|
+
if (base.name === 'config') return segs.length ? segs : null;
|
|
285
|
+
const al = this.alias(env.file, base.name, guard);
|
|
286
|
+
const head = al ? this.configPathOf(al, env, guard) : null;
|
|
287
|
+
return head ? head.concat(segs) : null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
evalExpr(node, env) {
|
|
291
|
+
this.tick();
|
|
292
|
+
const n = unparen(node);
|
|
293
|
+
if (!n) return UNK;
|
|
294
|
+
switch (n.type) {
|
|
295
|
+
case 'ident':
|
|
296
|
+
if (n.name === 'true') return known(true);
|
|
297
|
+
if (n.name === 'false') return known(false);
|
|
298
|
+
if (n.name === 'null') return known(null);
|
|
299
|
+
if (this.target.args && Object.prototype.hasOwnProperty.call(this.target.args, n.name)) return known(this.target.args[n.name]);
|
|
300
|
+
return UNK;
|
|
301
|
+
case 'int': case 'float': return known(n.value);
|
|
302
|
+
case 'string': return n.interpolated ? UNK : known(n.literal);
|
|
303
|
+
case 'unop': {
|
|
304
|
+
if (n.op !== '!') return UNK;
|
|
305
|
+
const v = this.evalExpr(n.operand, env);
|
|
306
|
+
return v.known && typeof v.value === 'boolean' ? known(!v.value) : UNK;
|
|
307
|
+
}
|
|
308
|
+
case 'binop': return this.evalBinop(n, env);
|
|
309
|
+
case 'select': {
|
|
310
|
+
const flags = platformFlags(this.target.system);
|
|
311
|
+
const b = unparen(n.base);
|
|
312
|
+
if (flags && b && b.type === 'ident' && b.name === 'pkgs' && n.attrpath.length === 2 && n.attrpath[0].name === 'stdenv' && n.attrpath[1].name in flags) return known(flags[n.attrpath[1].name]);
|
|
313
|
+
const p = this.configPathOf(n, env);
|
|
314
|
+
return p ? this.optionValue(env, p) : UNK;
|
|
315
|
+
}
|
|
316
|
+
default: return UNK;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
evalBinop(n, env) {
|
|
321
|
+
const bool = (r) => (r.known && typeof r.value === 'boolean' ? r.value : null);
|
|
322
|
+
if (['&&', '||', '->'].includes(n.op)) {
|
|
323
|
+
const l = bool(this.evalExpr(n.left, env));
|
|
324
|
+
const r = bool(this.evalExpr(n.right, env));
|
|
325
|
+
if (n.op === '&&') return l === false || r === false ? known(false) : l === true && r === true ? known(true) : UNK;
|
|
326
|
+
if (n.op === '||') return l === true || r === true ? known(true) : l === false && r === false ? known(false) : UNK;
|
|
327
|
+
return l === false || r === true ? known(true) : l === true && r === false ? known(false) : UNK;
|
|
328
|
+
}
|
|
329
|
+
if (n.op === '==' || n.op === '!=') {
|
|
330
|
+
const l = this.evalExpr(n.left, env);
|
|
331
|
+
const r = this.evalExpr(n.right, env);
|
|
332
|
+
if (!l.known || !r.known) return UNK;
|
|
333
|
+
const eq = sameValue(l.value, r.value);
|
|
334
|
+
return known(n.op === '==' ? eq : !eq);
|
|
335
|
+
}
|
|
336
|
+
return UNK;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
optionValue(env, segs) {
|
|
340
|
+
const res = this.resolve(env.namespace, env.scope, segs.join('.'));
|
|
341
|
+
return (res.status === 'set' || res.status === 'default') && res.valueKnown ? known(res.value) : UNK;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
condOutcome(def, c) {
|
|
345
|
+
const ast = this.parseAt(def.file, c.span);
|
|
346
|
+
if (!ast) return 'unknown';
|
|
347
|
+
const r = this.evalExpr(ast, { file: def.file, namespace: def.namespace, scope: def.scope });
|
|
348
|
+
if (!r.known || typeof r.value !== 'boolean') return 'unknown';
|
|
349
|
+
const truth = c.branch === 'else' ? !r.value : r.value;
|
|
350
|
+
return truth ? 'true' : 'false';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
defValue(def) {
|
|
354
|
+
const s = def.summary;
|
|
355
|
+
switch (s.type) {
|
|
356
|
+
case 'bool': case 'number': case 'null': return known(s.value);
|
|
357
|
+
case 'string': return s.interpolated ? UNK : known(s.literal);
|
|
358
|
+
case 'list': {
|
|
359
|
+
if (s.items.length < s.length) return UNK;
|
|
360
|
+
const out = [];
|
|
361
|
+
for (const it of s.items) {
|
|
362
|
+
const v = it.type === 'string' && !it.interpolated ? known(it.literal) : it.type === 'number' || it.type === 'bool' ? known(it.value) : UNK;
|
|
363
|
+
if (!v.known) return UNK;
|
|
364
|
+
out.push(v.value);
|
|
365
|
+
}
|
|
366
|
+
return known(out);
|
|
367
|
+
}
|
|
368
|
+
case 'expression': case 'reference': {
|
|
369
|
+
const ast = this.parseAt(def.file, def.valueSpan);
|
|
370
|
+
return ast ? this.evalExpr(ast, { file: def.file, namespace: def.namespace, scope: def.scope }) : UNK;
|
|
371
|
+
}
|
|
372
|
+
default: return UNK;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
priorityOf(def) {
|
|
377
|
+
if (def.priorityName === 'plain') return 100;
|
|
378
|
+
if (def.priorityName === 'mkOverride') return def.priorityArg && def.priorityArg.type === 'number' ? def.priorityArg.value : null;
|
|
379
|
+
return def.priorityName in PRIORITY ? PRIORITY[def.priorityName] : null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── option resolution ──
|
|
383
|
+
|
|
384
|
+
resolve(ns, scope, pathText) {
|
|
385
|
+
const cat = ns === 'nixos' ? this.catalog.nixos : this.catalog.homeManager;
|
|
386
|
+
const canonical = cat.renames[pathText] || pathText;
|
|
387
|
+
const key = this.key(ns, scope, canonical);
|
|
388
|
+
if (this.memo.has(key)) return this.memo.get(key);
|
|
389
|
+
if (this.stack.has(key)) {
|
|
390
|
+
this.gap({ kind: 'cyclic-config-reference', detail: `${canonical} depends on itself through config references`, file: null, span: null });
|
|
391
|
+
return this.shell(ns, scope, canonical, 'unknown', { reason: 'cyclic-reference' });
|
|
392
|
+
}
|
|
393
|
+
if (this.stack.size >= this.b.maxEvalDepth) throw this.cap('maxEvalDepth', this.b.maxEvalDepth, `reference chain deeper than ${this.b.maxEvalDepth}`);
|
|
394
|
+
this.stack.add(key);
|
|
395
|
+
let res;
|
|
396
|
+
try { res = this.compute(ns, scope, canonical, key); } finally { this.stack.delete(key); }
|
|
397
|
+
this.memo.set(key, res);
|
|
398
|
+
return res;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Top-level entry: caps surface as an `unknown` result plus `truncated`, never as a throw. */
|
|
402
|
+
resolveTop(ns, scope, pathText) {
|
|
403
|
+
this.steps = 0;
|
|
404
|
+
try { return this.resolve(ns, scope, pathText); } catch (e) {
|
|
405
|
+
if (!(e instanceof Cap)) throw e;
|
|
406
|
+
this.stack.clear();
|
|
407
|
+
return this.finish(this.shell(ns, scope, pathText, 'unknown', { reason: 'evaluation-budget', budget: { name: e.budget, limit: e.limit } }));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
shell(ns, scope, pathText, status, extra = {}) {
|
|
412
|
+
const info = this.catalogInfo(ns, pathText);
|
|
413
|
+
return {
|
|
414
|
+
namespace: ns, scope, path: pathText, status, value: undefined, valueKnown: false, definite: false,
|
|
415
|
+
sources: [], precedence: { rule: null, winnerPriority: null }, caveats: [],
|
|
416
|
+
catalog: { revision: this.catalog.revision, release: this.release.release, releaseSource: this.release.source, optionStatus: info.status, type: info.entry ? info.entry.type : null },
|
|
417
|
+
...extra,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
compute(ns, scope, pathText, key) {
|
|
422
|
+
const info = this.catalogInfo(ns, pathText);
|
|
423
|
+
const defs = this.defs.get(key) || [];
|
|
424
|
+
if (defs.length > this.b.maxDefinitionsPerOption) throw this.cap('maxDefinitionsPerOption', this.b.maxDefinitionsPerOption, `${pathText} has more than ${this.b.maxDefinitionsPerOption} definitions`);
|
|
425
|
+
const rows = defs.map((d) => {
|
|
426
|
+
const conditions = d.conditions.map((c) => ({ kind: c.kind, text: c.text, branch: c.branch, outcome: this.condOutcome(d, c) }));
|
|
427
|
+
const active = conditions.some((c) => c.outcome === 'false') ? 'inactive' : conditions.some((c) => c.outcome === 'unknown') ? 'conditional' : 'certain';
|
|
428
|
+
return { d, conditions, active, priority: this.priorityOf(d), value: this.defValue(d) };
|
|
429
|
+
});
|
|
430
|
+
const live = rows.filter((r) => r.active !== 'inactive');
|
|
431
|
+
const certain = live.filter((r) => r.active === 'certain');
|
|
432
|
+
const isList = info.entry && info.entry.type === 'listOf';
|
|
433
|
+
const typed = !!(info.entry && info.entry.type);
|
|
434
|
+
const out = this.shell(ns, scope, pathText, 'unknown');
|
|
435
|
+
const def = this.defaultFor(info);
|
|
436
|
+
const mark = (r, role) => out.sources.push({
|
|
437
|
+
file: r.d.file, span: r.d.span, namespace: r.d.namespace, scope: r.d.scope, priority: r.priority, priorityLabel: r.d.priorityName,
|
|
438
|
+
conditions: r.conditions, role, viaRename: r.d.viaRename, orderSensitive: r.d.priorityName === 'mkBefore' || r.d.priorityName === 'mkAfter' || undefined,
|
|
439
|
+
});
|
|
440
|
+
const finishWith = (patch, roles) => {
|
|
441
|
+
Object.assign(out, patch);
|
|
442
|
+
for (const r of rows) mark(r, roles.get(r) || (r.active === 'inactive' ? 'inactive-condition-false' : 'shadowed'));
|
|
443
|
+
return out;
|
|
444
|
+
};
|
|
445
|
+
const roles = new Map();
|
|
446
|
+
for (const r of rows) if (r.active === 'conditional') roles.set(r, 'conditional');
|
|
447
|
+
|
|
448
|
+
if (!live.length) {
|
|
449
|
+
out.precedence = { rule: 'no-active-definition', winnerPriority: null };
|
|
450
|
+
if (def.known) return finishWith({ status: 'default', value: def.value, valueKnown: true, defaultProof: def.proof }, roles);
|
|
451
|
+
return finishWith({ status: 'unknown', reason: `no-definition-and-no-proven-default (${def.reason})` }, roles);
|
|
452
|
+
}
|
|
453
|
+
const certainBest = certain.length && certain.every((r) => r.priority !== null) ? Math.min(...certain.map((r) => r.priority)) : null;
|
|
454
|
+
const nullCertain = certain.some((r) => r.priority === null);
|
|
455
|
+
const winners = live.filter((r) => r.priority === null || certainBest === null || r.priority <= certainBest);
|
|
456
|
+
const condWinners = winners.filter((r) => r.active === 'conditional');
|
|
457
|
+
const certWinners = winners.filter((r) => r.active === 'certain');
|
|
458
|
+
const prio = { rule: 'lowest priority number wins (mkForce 50 < plain 100 < mkDefault 1000)', winnerPriority: certainBest };
|
|
459
|
+
out.precedence = prio;
|
|
460
|
+
|
|
461
|
+
const vals = (rs) => rs.map((r) => (r.value.known ? JSON.stringify(r.value.value) : `?${r.d.id}`));
|
|
462
|
+
const allSame = (rs) => new Set(vals(rs)).size === 1;
|
|
463
|
+
|
|
464
|
+
if (nullCertain && !allSame(live)) {
|
|
465
|
+
out.precedence = { ...prio, rule: 'priority is not a literal (mkOverride argument)' };
|
|
466
|
+
return finishWith({ status: 'unresolved-precedence', reason: 'a definition has a non-literal priority' }, roles);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (isList && !condWinners.length && !nullCertain) {
|
|
470
|
+
const group = certWinners.filter((r) => r.priority === certainBest);
|
|
471
|
+
group.forEach((r) => roles.set(r, 'winner'));
|
|
472
|
+
const values = group.map((r) => r.value);
|
|
473
|
+
const ok = values.every((v) => v.known && Array.isArray(v.value));
|
|
474
|
+
return finishWith({ status: 'set', value: ok ? values.flatMap((v) => v.value) : undefined, valueKnown: ok, merged: group.length > 1, precedence: { ...prio, rule: 'list option: definitions at the winning priority are concatenated' } }, roles);
|
|
475
|
+
}
|
|
476
|
+
if (isList) {
|
|
477
|
+
certWinners.forEach((r) => roles.set(r, 'winner'));
|
|
478
|
+
const base = certWinners.filter((r) => r.priority === certainBest).map((r) => r.value);
|
|
479
|
+
const everyLive = winners.map((r) => r.value);
|
|
480
|
+
return finishWith({
|
|
481
|
+
status: 'conditional', reason: 'a list definition depends on a condition that could not be decided',
|
|
482
|
+
definiteItems: base.every((v) => v.known) ? base.flatMap((v) => v.value) : undefined,
|
|
483
|
+
possibleItems: everyLive.every((v) => v.known && Array.isArray(v.value)) ? everyLive.flatMap((v) => v.value) : undefined,
|
|
484
|
+
}, roles);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (!condWinners.length) {
|
|
488
|
+
const group = certWinners.filter((r) => r.priority === certainBest || certainBest === null);
|
|
489
|
+
group.forEach((r) => roles.set(r, 'winner'));
|
|
490
|
+
if (group.length === 1 || allSame(group)) {
|
|
491
|
+
const w = group[0];
|
|
492
|
+
return finishWith({ status: 'set', value: w.value.known ? w.value.value : undefined, valueKnown: w.value.known }, roles);
|
|
493
|
+
}
|
|
494
|
+
const differ = group.every((r) => r.value.known);
|
|
495
|
+
if (differ && typed) return finishWith({ status: 'conflict', reason: `${group.length} definitions at priority ${certainBest} disagree; the module system rejects this`, candidates: group.map((r) => r.value.value) }, roles);
|
|
496
|
+
return finishWith({ status: 'unresolved-precedence', reason: differ ? 'option type is not cataloged, so whether the definitions merge is unknown' : 'definition values are not statically evaluable' }, roles);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// some winner depends on an undecided condition
|
|
500
|
+
const cands = winners.map((r) => r);
|
|
501
|
+
const candVals = vals(cands);
|
|
502
|
+
const needDefault = !certWinners.length;
|
|
503
|
+
if (needDefault) candVals.push(def.known ? JSON.stringify(def.value) : '?default');
|
|
504
|
+
if (new Set(candVals).size === 1 && !candVals[0].startsWith('?')) {
|
|
505
|
+
const w = cands[0];
|
|
506
|
+
certWinners.forEach((r) => roles.set(r, 'winner'));
|
|
507
|
+
return finishWith({ status: 'set', value: w.value.value, valueKnown: true, note: 'every undecided condition leads to the same value' }, roles);
|
|
508
|
+
}
|
|
509
|
+
certWinners.forEach((r) => roles.set(r, 'winner'));
|
|
510
|
+
const possible = cands.filter((r) => r.value.known).map((r) => r.value.value);
|
|
511
|
+
if (needDefault && def.known) possible.push(def.value);
|
|
512
|
+
return finishWith({
|
|
513
|
+
status: 'conditional', reason: 'the effective value depends on a condition that could not be decided statically',
|
|
514
|
+
possibleValues: possible, allPossibleValuesKnown: cands.every((r) => r.value.known) && (!needDefault || def.known),
|
|
515
|
+
}, roles);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** Adds caveats and the `definite` flag. */
|
|
519
|
+
finish(res) {
|
|
520
|
+
const segs = res.path.split('.');
|
|
521
|
+
const caveats = [];
|
|
522
|
+
for (const dp of this.dynamicPrefixes) {
|
|
523
|
+
if (dp.namespace === res.namespace && dp.prefix.every((s, i) => segs[i] === s)) caveats.push({ kind: 'dynamic-attribute', detail: `an attribute name under ${dp.prefix.join('.') || '(root)'} is computed at evaluation time`, file: dp.file, span: dp.span });
|
|
524
|
+
}
|
|
525
|
+
for (const o of this.opaque) {
|
|
526
|
+
if (o.namespace !== res.namespace || (o.scope || null) !== (res.scope || null)) continue;
|
|
527
|
+
const proper = o.selfDef ? o.prefix.length < segs.length : true;
|
|
528
|
+
if (proper && o.prefix.every((s, i) => segs[i] === s)) caveats.push({ kind: 'opaque-definition', detail: `${o.prefix.join('.') || 'config'} is assigned a non-literal value that may define this option`, file: o.file, span: o.span });
|
|
529
|
+
}
|
|
530
|
+
if (res.status === 'default' || res.status === 'unknown') {
|
|
531
|
+
const ns = res.namespace === 'nixos' ? this.catalog.nixos : this.catalog.homeManager;
|
|
532
|
+
const prefix = ns.authoritative.find((a) => res.path === a || res.path.startsWith(`${a}.`));
|
|
533
|
+
if (prefix) for (const u of this.unknownOptions.values()) if (u.namespace === res.namespace && u.authoritativePrefix === prefix) caveats.push({ kind: 'unknown-option-in-namespace', detail: `${u.path} is not a known option; if it is a misspelling or a removed option the default here may not be in force`, file: u.sources[0].file, span: u.sources[0].span });
|
|
534
|
+
}
|
|
535
|
+
if (segs[segs.length - 1] === 'package' && this.overlays.length) caveats.push({ kind: 'overlay-may-change-package', detail: 'an overlay is applied; the package this option selects may differ from nixpkgs', file: this.overlays[0].file, span: this.overlays[0].span });
|
|
536
|
+
const blockers = this.unresolved.filter((g) => BLOCKER_KINDS.has(g.kind));
|
|
537
|
+
if (blockers.length) caveats.push({ kind: 'incomplete-module-graph', detail: `module graph is partial (${[...new Set(blockers.map((g) => g.kind))].join(', ')}); an unread module could override this`, file: null, span: null });
|
|
538
|
+
res.caveats = caveats;
|
|
539
|
+
res.definite = (res.status === 'set' || res.status === 'default') && res.valueKnown && caveats.length === 0;
|
|
540
|
+
return res;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* @param {{entry:string|string[], root?:string, files?:Record<string,string>, readFile?:(p:string)=>string|null,
|
|
546
|
+
* target?:{name?:string, release?:string, system?:string, args?:object, namespace?:'nixos'|'home-manager', user?:string},
|
|
547
|
+
* flake?:string, catalog?:object, budgets?:object}} opts
|
|
548
|
+
*/
|
|
549
|
+
export function resolveNixosConfig(opts) {
|
|
550
|
+
const r = new Resolver(opts);
|
|
551
|
+
r.release = r.determineRelease();
|
|
552
|
+
r.load();
|
|
553
|
+
r.collect();
|
|
554
|
+
const keys = [...r.defs.keys()].sort();
|
|
555
|
+
const options = [];
|
|
556
|
+
for (const k of keys) {
|
|
557
|
+
const [ns, scope, ...rest] = k.split('|');
|
|
558
|
+
const pathText = rest.join('|');
|
|
559
|
+
options.push(r.finish(r.resolveTop(ns, scope || null, pathText)));
|
|
560
|
+
}
|
|
561
|
+
const sv = r.defs.get(r.key('nixos', null, 'system.stateVersion'));
|
|
562
|
+
const stateVersion = sv && sv[0] && sv[0].summary.type === 'string'
|
|
563
|
+
? { value: sv[0].summary.literal, file: sv[0].file, span: sv[0].span, role: 'state-compatibility-marker', usedForRelease: false }
|
|
564
|
+
: null;
|
|
565
|
+
const blockers = r.unresolved.filter((g) => BLOCKER_KINDS.has(g.kind));
|
|
566
|
+
const report = {
|
|
567
|
+
kind: 'nixos-effective-config', version: 1,
|
|
568
|
+
target: { name: (opts.target && opts.target.name) || null, entries: [].concat(opts.entry || []), system: (opts.target && opts.target.system) || null, release: r.release.release, releaseSource: r.release.source },
|
|
569
|
+
catalog: { revision: r.catalog.revision, schema: r.catalog.schema, releases: r.catalog.releases, matchedRelease: r.release.release && r.catalog.releases.includes(r.release.release) ? r.release.release : null },
|
|
570
|
+
stateVersion,
|
|
571
|
+
namespaces: [...new Set([...r.fileNs.values()].map((v) => v.namespace).concat([...r.defs.keys()].map((k) => k.split('|')[0])))].sort(),
|
|
572
|
+
files: r.order.map((f) => ({ file: f, ...r.fileNs.get(f), status: r.irs.get(f).status })),
|
|
573
|
+
graph: { edges: r.graphEdges },
|
|
574
|
+
options,
|
|
575
|
+
unknownOptions: [...r.unknownOptions.values()],
|
|
576
|
+
renamedOptions: [...r.renamed.values()],
|
|
577
|
+
overlays: r.overlays,
|
|
578
|
+
unresolved: r.unresolved,
|
|
579
|
+
truncated: r.truncated,
|
|
580
|
+
completeness: blockers.length || r.truncated.length ? 'partial' : 'complete',
|
|
581
|
+
};
|
|
582
|
+
Object.defineProperty(report, 'lookup', {
|
|
583
|
+
enumerable: false,
|
|
584
|
+
value: (path, { namespace = 'nixos', scope = null } = {}) => {
|
|
585
|
+
const cat = namespace === 'nixos' ? r.catalog.nixos : r.catalog.homeManager;
|
|
586
|
+
const canonical = cat.renames[path] || path;
|
|
587
|
+
const hit = options.find((o) => o.namespace === namespace && (o.scope || null) === scope && o.path === canonical);
|
|
588
|
+
if (hit) return hit;
|
|
589
|
+
const res = r.finish(r.resolveTop(namespace, scope, canonical));
|
|
590
|
+
report.truncated = r.truncated;
|
|
591
|
+
return res;
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
return report;
|
|
595
|
+
}
|