configorama 0.11.0 → 1.0.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.
Files changed (78) hide show
  1. package/README.md +429 -123
  2. package/cli.js +282 -49
  3. package/index.d.ts +43 -1
  4. package/package.json +5 -1
  5. package/src/capabilities.js +59 -0
  6. package/src/capabilities.test.js +44 -0
  7. package/src/display.js +70 -7
  8. package/src/display.test.js +82 -0
  9. package/src/errors.js +73 -0
  10. package/src/index.js +91 -1
  11. package/src/main.js +159 -19
  12. package/src/parsers/esm.js +1 -16
  13. package/src/parsers/typescript.js +1 -48
  14. package/src/resolvers/valueFromCron.js +4 -25
  15. package/src/resolvers/valueFromEval.js +11 -1
  16. package/src/resolvers/valueFromFile.js +8 -1
  17. package/src/resolvers/valueFromGit.js +43 -17
  18. package/src/resolvers/valueFromOptions.js +5 -4
  19. package/src/utils/filters/filterArgs.js +57 -0
  20. package/src/utils/filters/oneOf.js +77 -0
  21. package/src/utils/introspection/audit.js +78 -0
  22. package/src/utils/introspection/graph.js +43 -0
  23. package/src/utils/introspection/model.js +150 -0
  24. package/src/utils/introspection/model.test.js +93 -0
  25. package/src/utils/parsing/commentAnnotations.js +107 -0
  26. package/src/utils/parsing/commentAnnotations.test.js +123 -0
  27. package/src/utils/parsing/enrichMetadata.js +64 -1
  28. package/src/utils/parsing/enrichMetadata.test.js +84 -0
  29. package/src/utils/parsing/extractComment.js +145 -0
  30. package/src/utils/parsing/extractComment.test.js +182 -0
  31. package/src/utils/parsing/preProcess.js +2 -1
  32. package/src/utils/paths/findLineForKey.js +2 -2
  33. package/src/utils/paths/ignorePaths.js +22 -9
  34. package/src/utils/redaction/redact.js +78 -0
  35. package/src/utils/redaction/redact.test.js +38 -0
  36. package/src/utils/redaction/setupRedaction.js +47 -0
  37. package/src/utils/redaction/setupRedaction.test.js +68 -0
  38. package/src/utils/requirements/configRequirements.js +351 -0
  39. package/src/utils/requirements/configRequirements.test.js +380 -0
  40. package/src/utils/requirements/serializeRequirements.js +120 -0
  41. package/src/utils/requirements/serializeRequirements.test.js +211 -0
  42. package/src/utils/security/evalSafety.js +86 -0
  43. package/src/utils/security/evalSafety.test.js +61 -0
  44. package/src/utils/security/safetyPolicy.js +110 -0
  45. package/src/utils/security/safetyPolicy.test.js +29 -0
  46. package/src/utils/strings/didYouMean.js +70 -0
  47. package/src/utils/strings/didYouMean.test.js +52 -0
  48. package/src/utils/strings/formatFunctionArgs.js +6 -1
  49. package/src/utils/strings/splitByComma.js +5 -0
  50. package/src/utils/ui/configWizard.js +208 -34
  51. package/src/utils/ui/createEditorLink.js +17 -1
  52. package/src/utils/ui/promptDescriptors.js +196 -0
  53. package/src/utils/ui/promptDescriptors.test.js +162 -0
  54. package/src/utils/variables/cleanVariable.js +22 -0
  55. package/src/utils/variables/getVariableType.js +1 -0
  56. package/types/src/index.d.ts +0 -24
  57. package/types/src/index.d.ts.map +1 -1
  58. package/types/src/main.d.ts +16 -8
  59. package/types/src/main.d.ts.map +1 -1
  60. package/types/src/resolvers/valueFromFile.d.ts +0 -2
  61. package/types/src/resolvers/valueFromFile.d.ts.map +1 -1
  62. package/types/src/resolvers/valueFromGit.d.ts.map +1 -1
  63. package/types/src/resolvers/valueFromSelf.d.ts +1 -0
  64. package/types/src/resolvers/valueFromSelf.d.ts.map +1 -0
  65. package/types/src/utils/parsing/parse.d.ts.map +1 -1
  66. package/types/src/utils/parsing/preProcess.d.ts.map +1 -1
  67. package/types/src/utils/paths/findLineForKey.d.ts +0 -9
  68. package/types/src/utils/paths/findLineForKey.d.ts.map +1 -1
  69. package/types/src/utils/strings/replaceAll.d.ts.map +1 -1
  70. package/types/src/utils/variables/variableUtils.d.ts +1 -1
  71. package/types/src/display.d.ts +0 -62
  72. package/types/src/display.d.ts.map +0 -1
  73. package/types/src/metadata.d.ts +0 -28
  74. package/types/src/metadata.d.ts.map +0 -1
  75. package/types/src/utils/BoundedMap.d.ts +0 -10
  76. package/types/src/utils/BoundedMap.d.ts.map +0 -1
  77. package/types/src/utils/paths/ignorePaths.d.ts +0 -5
  78. package/types/src/utils/paths/ignorePaths.d.ts.map +0 -1
package/src/display.js CHANGED
@@ -5,13 +5,64 @@ const chalk = require('./utils/ui/chalk')
5
5
  const { logHeader } = require('./utils/ui/logs')
6
6
  const { makeStackedBoxes } = require('@davidwells/box-logger')
7
7
  const { findLineForKey } = require('./utils/paths/findLineForKey')
8
- const { createEditorLink } = require('./utils/ui/createEditorLink')
9
- const { isSensitiveVariable } = require('./utils/ui/configWizard')
8
+ const { createEditorLink, toClickablePath } = require('./utils/ui/createEditorLink')
9
+ const { isSensitiveVariable } = require('./utils/redaction/redact')
10
10
 
11
11
  const SPACING = ' '
12
12
  const TITLE_TEXT = `Variable:${SPACING}`
13
13
  const VALUE_HEX = '#899499'
14
14
 
15
+ function uniqueCompact(values) {
16
+ return [...new Set((values || []).filter(value => value !== undefined && value !== null && value !== ''))]
17
+ }
18
+
19
+ function firstValue(source, field) {
20
+ if (!source) return undefined
21
+ if (source[field] !== undefined && source[field] !== null && source[field] !== '') return source[field]
22
+ const occurrences = Array.isArray(source.occurrences) ? source.occurrences : []
23
+ const occurrence = occurrences.find(occ => occ[field] !== undefined && occ[field] !== null && occ[field] !== '')
24
+ return occurrence ? occurrence[field] : undefined
25
+ }
26
+
27
+ function getAnnotationMetadata(source) {
28
+ const occurrences = Array.isArray(source?.occurrences) ? source.occurrences : []
29
+ return {
30
+ group: firstValue(source, 'group'),
31
+ obtainHint: firstValue(source, 'obtainHint'),
32
+ examples: uniqueCompact([
33
+ ...(Array.isArray(source?.examples) ? source.examples : []),
34
+ ...occurrences.flatMap(occ => Array.isArray(occ.examples) ? occ.examples : [])
35
+ ]),
36
+ defaultHint: firstValue(source, 'defaultHint'),
37
+ deprecationMessage: firstValue(source, 'deprecationMessage'),
38
+ }
39
+ }
40
+
41
+ function appendAnnotationMetadata(varMsg, source, keyChalk, valueChalk, indent = '') {
42
+ const metadata = getAnnotationMetadata(source)
43
+ const lines = []
44
+
45
+ if (metadata.group) lines.push(['Group:', metadata.group])
46
+ if (metadata.obtainHint) lines.push(['From:', metadata.obtainHint])
47
+ if (metadata.examples.length > 0) {
48
+ lines.push([metadata.examples.length === 1 ? 'Example:' : 'Examples:', metadata.examples.join(', ')])
49
+ }
50
+ if (metadata.defaultHint) lines.push(['Default hint:', metadata.defaultHint])
51
+ if (metadata.deprecationMessage) lines.push(['Deprecated:', metadata.deprecationMessage])
52
+
53
+ for (const [label, value] of lines) {
54
+ varMsg += `${indent}${keyChalk(label.padEnd(TITLE_TEXT.length, ' '))} ${valueChalk(value)}\n`
55
+ }
56
+
57
+ return varMsg
58
+ }
59
+
60
+ function isSensitiveOccurrence(varName, occurrences = []) {
61
+ const explicitOccurrence = occurrences.find(occ => typeof occ.sensitive === 'boolean')
62
+ if (explicitOccurrence) return explicitOccurrence.sensitive
63
+ return isSensitiveVariable(varName)
64
+ }
65
+
15
66
  /**
16
67
  * Display "No Variables Found" message
17
68
  * @param {string} configFilePath
@@ -58,9 +109,13 @@ function displayVariableDetails({ varKeys, variableData, uniqueVariables, varPre
58
109
 
59
110
  const getBaseVarName = (key) => key.replace(varPrefixPattern, '').replace(varSuffixPattern, '').split(',')[0].trim()
60
111
 
61
- const fileName = configFilePath ? ` in ${configFilePath}` : ''
112
+ logHeader(`Found ${varKeys.length} Variables`)
62
113
 
63
- logHeader(`Found ${varKeys.length} Variables${fileName}`)
114
+ // Print the path on its own line (outside the wrapping box) so it stays clickable
115
+ if (configFilePath) {
116
+ console.log()
117
+ console.log(` in ${toClickablePath(configFilePath)}`)
118
+ }
64
119
 
65
120
  // deepLog('variableData', variableData)
66
121
 
@@ -96,6 +151,8 @@ function displayVariableDetails({ varKeys, variableData, uniqueVariables, varPre
96
151
  // Get uniqueVariable data for description and other metadata
97
152
  const varName = getBaseVarName(key)
98
153
  const uniqueVar = uniqueVariables[varName]
154
+ const occurrenceMetadata = uniqueVar || { occurrences: variableInstances }
155
+ const isSensitive = isSensitiveOccurrence(varName, occurrenceMetadata.occurrences || variableInstances)
99
156
 
100
157
  // Build display message from enriched metadata
101
158
  let varMsg = ''
@@ -128,7 +185,7 @@ function displayVariableDetails({ varKeys, variableData, uniqueVariables, varPre
128
185
 
129
186
  // Show default value from metadata
130
187
  if (typeof firstInstance.defaultValue !== 'undefined') {
131
- const defaultValueRender = firstInstance.defaultValue === '' ? '""' : firstInstance.defaultValue
188
+ const defaultValueRender = isSensitive ? '********' : (firstInstance.defaultValue === '' ? '""' : firstInstance.defaultValue)
132
189
  const defaultValueText = `${indent}${keyChalk('Default value:'.padEnd(TITLE_TEXT.length, ' '))}`
133
190
  varMsg += `${defaultValueText} ${valueChalk(defaultValueRender)}\n`
134
191
  }
@@ -144,6 +201,8 @@ function displayVariableDetails({ varKeys, variableData, uniqueVariables, varPre
144
201
  }
145
202
  }
146
203
 
204
+ varMsg = appendAnnotationMetadata(varMsg, occurrenceMetadata, keyChalk, valueChalk, indent)
205
+
147
206
  // Show path(s) from metadata
148
207
  const configPathLine = findLineForKey(variableInstances[0].path, lines, fileType)
149
208
  let locationRender = configPathLine
@@ -250,7 +309,7 @@ function displayUniqueVariables({ uniqueVarKeys, uniqueVariables, lines, fileTyp
250
309
 
251
310
  // Show default value only if it's a true fallback, not a pre-resolved value
252
311
  // Redact sensitive values like API keys, secrets, tokens
253
- const isSensitive = isSensitiveVariable(varName)
312
+ const isSensitive = isSensitiveOccurrence(varName, occurrences)
254
313
  const hasActualDefault = firstOcc.hasFallback && typeof firstOcc.defaultValue !== 'undefined'
255
314
  if (hasActualDefault) {
256
315
  const defaultValueRender = isSensitive ? '********' : (firstOcc.defaultValue === '' ? '""' : firstOcc.defaultValue)
@@ -275,6 +334,8 @@ function displayUniqueVariables({ uniqueVarKeys, uniqueVariables, lines, fileTyp
275
334
  }
276
335
  }
277
336
 
337
+ varMsg = appendAnnotationMetadata(varMsg, uniqueVar, keyChalk, valueChalk)
338
+
278
339
  // Show config path(s) from occurrences
279
340
  let locationRender
280
341
  let locationLabel
@@ -403,7 +464,7 @@ function displayConfigurableVariables({ uniqueVarKeys, uniqueVariables, lines, f
403
464
  }
404
465
 
405
466
  // Show current/default value (redact sensitive values)
406
- const isSensitive = isSensitiveVariable(v.varName)
467
+ const isSensitive = isSensitiveOccurrence(v.varName, occurrences)
407
468
  if (v.resolvedValue !== undefined) {
408
469
  const resolvedRender = isSensitive ? '********' : (v.resolvedValue === '' ? '""' : v.resolvedValue)
409
470
  varMsg += `${keyChalk('Current value:'.padEnd(TITLE_TEXT.length, ' '))} ${valueChalk(resolvedRender)}\n`
@@ -412,6 +473,8 @@ function displayConfigurableVariables({ uniqueVarKeys, uniqueVariables, lines, f
412
473
  varMsg += `${keyChalk('Default value:'.padEnd(TITLE_TEXT.length, ' '))} ${valueChalk(defaultRender)}\n`
413
474
  }
414
475
 
476
+ varMsg = appendAnnotationMetadata(varMsg, v, keyChalk, valueChalk)
477
+
415
478
  // Show config path(s)
416
479
  let locationRender
417
480
  let locationLabel
@@ -0,0 +1,82 @@
1
+ const { test } = require('uvu')
2
+ const assert = require('uvu/assert')
3
+ const { displayConfigurableVariables } = require('./display')
4
+
5
+ function captureStdout(fn) {
6
+ const originalLog = console.log
7
+ const output = []
8
+ console.log = (...args) => output.push(args.join(' '))
9
+ try {
10
+ fn()
11
+ } finally {
12
+ console.log = originalLog
13
+ }
14
+ return output.join('\n')
15
+ }
16
+
17
+ test('displayConfigurableVariables renders annotation metadata and sensitive overrides', () => {
18
+ const output = captureStdout(() => {
19
+ displayConfigurableVariables({
20
+ uniqueVarKeys: [
21
+ 'env:CONFIGORAMA_DISPLAY_SECRET',
22
+ 'env:CONFIGORAMA_DISPLAY_PUBLIC_KEY',
23
+ ],
24
+ uniqueVariables: {
25
+ 'env:CONFIGORAMA_DISPLAY_SECRET': {
26
+ varName: 'env:CONFIGORAMA_DISPLAY_SECRET',
27
+ variableType: 'env',
28
+ variableSourceType: 'user',
29
+ descriptions: ['Stripe live secret key'],
30
+ resolvedValue: 'secret-value',
31
+ occurrences: [
32
+ {
33
+ path: 'secrets.stripeSecret',
34
+ varMatch: '${env:CONFIGORAMA_DISPLAY_SECRET}',
35
+ isRequired: true,
36
+ sensitive: true,
37
+ group: 'Payments',
38
+ obtainHint: 'Stripe dashboard > Developers > API keys',
39
+ examples: ['sk_live_...'],
40
+ defaultHint: 'Set in CI',
41
+ deprecationMessage: 'Use STRIPE_RESTRICTED_KEY instead',
42
+ }
43
+ ],
44
+ },
45
+ 'env:CONFIGORAMA_DISPLAY_PUBLIC_KEY': {
46
+ varName: 'env:CONFIGORAMA_DISPLAY_PUBLIC_KEY',
47
+ variableType: 'env',
48
+ variableSourceType: 'user',
49
+ descriptions: ['Public publishable key'],
50
+ resolvedValue: 'public-value',
51
+ occurrences: [
52
+ {
53
+ path: 'secrets.publishableKey',
54
+ varMatch: '${env:CONFIGORAMA_DISPLAY_PUBLIC_KEY}',
55
+ isRequired: true,
56
+ sensitive: false,
57
+ }
58
+ ],
59
+ },
60
+ },
61
+ lines: [],
62
+ fileType: 'yaml',
63
+ configFilePath: '',
64
+ })
65
+ })
66
+
67
+ assert.match(output, /Group:/)
68
+ assert.match(output, /Payments/)
69
+ assert.match(output, /From:/)
70
+ assert.match(output, /Stripe dashboard > Developers > API keys/)
71
+ assert.match(output, /Example:/)
72
+ assert.match(output, /sk_live_\.\.\./)
73
+ assert.match(output, /Default hint:/)
74
+ assert.match(output, /Set in CI/)
75
+ assert.match(output, /Deprecated:/)
76
+ assert.match(output, /Use STRIPE_RESTRICTED_KEY instead/)
77
+ assert.match(output, /\*{8}/)
78
+ assert.not.match(output, /secret-value/)
79
+ assert.match(output, /public-value/)
80
+ })
81
+
82
+ test.run()
package/src/errors.js ADDED
@@ -0,0 +1,73 @@
1
+ // Registry of stable error codes emitted on `error.code` and in `--error-format json`.
2
+ // Single source of truth for the codes referenced by classifyErrorMessage and thrown
3
+ // directly as ConfigoramaError; surfaced by `configorama capabilities`.
4
+ const ERROR_CODES = [
5
+ { code: 'missing_env', description: 'A referenced environment variable was not set and had no fallback.' },
6
+ { code: 'missing_file', description: 'A referenced file could not be found on disk.' },
7
+ { code: 'unresolved_variable', description: 'A variable could not be resolved to a value.' },
8
+ { code: 'unknown_filter', description: 'A pipe filter name is not registered.' },
9
+ { code: 'invalid_variable_syntax', description: 'A variable reference is malformed.' },
10
+ { code: 'circular_dependency', description: 'Variables reference each other in a cycle.' },
11
+ { code: 'invalid_view', description: 'An unknown --view was passed to the inspect command.' },
12
+ { code: 'blocked_by_safe_mode', description: 'An executable or mutating reference was blocked by --safe.' },
13
+ { code: 'blocked_eval_escape', description: 'An eval/if expression attempted a prototype-chain (constructor) escape.' },
14
+ { code: 'file_root_forbidden', description: 'A file/text reference resolved outside an allowed --safe-root.' },
15
+ { code: 'unknown_command', description: 'The first argument was not a recognized command.' },
16
+ { code: 'no_input_file', description: 'No config file was provided on the command line.' },
17
+ { code: 'file_not_found', description: 'The provided config file path does not exist.' },
18
+ { code: 'path_not_found', description: 'A jq-style extraction path matched nothing in the resolved config.' },
19
+ { code: 'configorama_error', description: 'Generic, unclassified configorama error.' },
20
+ ]
21
+
22
+ class ConfigoramaError extends Error {
23
+ constructor(code, message, details = {}) {
24
+ super(message)
25
+ this.name = 'ConfigoramaError'
26
+ this.code = code || 'configorama_error'
27
+ this.details = details
28
+ }
29
+
30
+ toJSON() {
31
+ return {
32
+ error: {
33
+ code: this.code,
34
+ message: this.message,
35
+ details: this.details || {},
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ function isConfigoramaError(error) {
42
+ return !!(error && error.name === 'ConfigoramaError' && error.code)
43
+ }
44
+
45
+ function normalizeError(error, fallbackCode = 'configorama_error') {
46
+ if (isConfigoramaError(error)) return error
47
+ const code = error && error.code ? error.code : classifyErrorMessage(error && error.message, fallbackCode)
48
+ return new ConfigoramaError(
49
+ code,
50
+ error && error.message ? error.message : String(error),
51
+ error && error.details ? error.details : {}
52
+ )
53
+ }
54
+
55
+ function classifyErrorMessage(message, fallbackCode = 'configorama_error') {
56
+ const text = String(message || '')
57
+ if (/Filter ".+" not found/.test(text)) return 'unknown_filter'
58
+ if (/Unable to resolve config variable/.test(text) && /env:/.test(text)) return 'missing_env'
59
+ if (/Unable to resolve config variable/.test(text)) return 'unresolved_variable'
60
+ if (/Blocked eval expression/.test(text)) return 'blocked_eval_escape'
61
+ if (/File not found|cannot resolve due to missing file/i.test(text)) return 'missing_file'
62
+ if (/Invalid variable reference syntax/.test(text)) return 'invalid_variable_syntax'
63
+ if (/Circular variable dependency/.test(text)) return 'circular_dependency'
64
+ return fallbackCode
65
+ }
66
+
67
+ module.exports = {
68
+ ERROR_CODES,
69
+ ConfigoramaError,
70
+ classifyErrorMessage,
71
+ isConfigoramaError,
72
+ normalizeError,
73
+ }
package/src/index.js CHANGED
@@ -2,6 +2,14 @@ const Configorama = require('./main')
2
2
  const parsers = require('./parsers')
3
3
  const enrichMetadata = require('./utils/parsing/enrichMetadata')
4
4
  const { buildVariableSyntax } = require('./utils/variables/variableUtils')
5
+ const { serializeRequirements } = require('./utils/requirements/serializeRequirements')
6
+ const { buildConfigRequirements } = require('./utils/requirements/configRequirements')
7
+ const { buildIntrospection } = require('./utils/introspection/model')
8
+ const { buildAuditReport } = require('./utils/introspection/audit')
9
+ const { formatGraph } = require('./utils/introspection/graph')
10
+ const { ConfigoramaError } = require('./errors')
11
+
12
+ const INSPECT_VIEWS = ['requirements', 'audit', 'graph']
5
13
 
6
14
  /**
7
15
  * @typedef {Object} ConfigoramaSettings
@@ -127,7 +135,89 @@ module.exports.analyze = async (configPathOrObject, settings = {}) => {
127
135
  returnPreResolvedVariableDetails: true,
128
136
  })
129
137
  const options = settings.options || {}
130
- return instance.init(options)
138
+ const analysis = await instance.init(options)
139
+ if (settings.instructions) {
140
+ return serializeRequirements(analysis, { configPathOrObject })
141
+ }
142
+ return analysis
143
+ }
144
+
145
+ module.exports.introspect = async (configPathOrObject, settings = {}) => {
146
+ const analysis = await module.exports.analyze(configPathOrObject, {
147
+ ...settings,
148
+ blockCustomResolvers: false,
149
+ blockCustomFunctions: false,
150
+ blockDotEnv: false,
151
+ })
152
+ const requirements = buildConfigRequirements(analysis)
153
+ return buildIntrospection(analysis, { requirements })
154
+ }
155
+
156
+ module.exports.audit = async (configPathOrObject, settings = {}) => {
157
+ const analysis = await module.exports.analyze(configPathOrObject, {
158
+ ...settings,
159
+ blockCustomResolvers: false,
160
+ blockCustomFunctions: false,
161
+ blockDotEnv: false,
162
+ })
163
+ const requirements = buildConfigRequirements(analysis)
164
+ const introspection = buildIntrospection(analysis, { requirements })
165
+ const customResolvers = Array.isArray(settings.variableSources)
166
+ ? settings.variableSources.map(source => source.type).filter(Boolean)
167
+ : []
168
+ const originalConfig = analysis.originalConfig || {}
169
+ return buildAuditReport(introspection, {
170
+ safeMode: settings.safeMode === true || settings.safe === true,
171
+ dotenv: originalConfig.useDotenv === true || originalConfig.useDotEnv === true || settings.useDotEnvFiles === true,
172
+ customResolvers,
173
+ })
174
+ }
175
+
176
+ module.exports.graph = async (configPathOrObject, settings = {}) => {
177
+ const graph = await module.exports.introspect(configPathOrObject, settings)
178
+ if (settings.formatGraph === false) return graph
179
+ return formatGraph(graph, settings.format || 'json')
180
+ }
181
+
182
+ /**
183
+ * Unified introspection entry point. Without a view it returns the full model
184
+ * (requirements + graph + audit). With `settings.view` it returns a single
185
+ * projection, identical to the focused `requirements` / `graph` / `audit` verbs.
186
+ * @param {string|object} configPathOrObject - Path to config file or raw config object
187
+ * @param {object} [settings] - Same settings as the main API, plus `view` and `format`
188
+ * @return {Promise<object|string>} the requested model (string for mermaid/dot graph views)
189
+ */
190
+ module.exports.inspect = async (configPathOrObject, settings = {}) => {
191
+ const view = settings.view
192
+ if (view !== undefined && view !== null && view !== '' && !INSPECT_VIEWS.includes(view)) {
193
+ throw new ConfigoramaError('invalid_view', `Unknown inspect view "${view}". Valid views: ${INSPECT_VIEWS.join(', ')}.`)
194
+ }
195
+ if (view === 'requirements') {
196
+ return module.exports.analyze(configPathOrObject, { ...settings, instructions: true })
197
+ }
198
+ if (view === 'audit') {
199
+ return module.exports.audit(configPathOrObject, settings)
200
+ }
201
+ if (view === 'graph') {
202
+ return module.exports.graph(configPathOrObject, settings)
203
+ }
204
+
205
+ // Full model: each projection runs its own verb's code path so the unified
206
+ // output stays identical to `requirements`, `graph`, and `audit` run alone.
207
+ // (Those paths use different analyze settings, so they are not collapsible
208
+ // into a single shared analysis without changing their output.)
209
+ const [requirements, graph, audit] = await Promise.all([
210
+ module.exports.analyze(configPathOrObject, { ...settings, instructions: true }),
211
+ module.exports.graph(configPathOrObject, { ...settings, formatGraph: false }),
212
+ module.exports.audit(configPathOrObject, settings),
213
+ ])
214
+ return {
215
+ schemaVersion: 1,
216
+ config: typeof configPathOrObject === 'string' ? configPathOrObject : (settings.configFilePath || null),
217
+ requirements,
218
+ graph,
219
+ audit,
220
+ }
131
221
  }
132
222
 
133
223
  /**