Skip to content

fix(resolver): stop false circular file references from leaked origins - #85

Merged
DavidWells merged 1 commit into
masterfrom
fix/false-circular-reference
Oct 9, 2026
Merged

DavidWells merged 1 commit into
masterfrom
fix/false-circular-reference

Conversation

@DavidWells

Copy link
Copy Markdown
Owner

What

Fixes two false Circular file reference detected errors that 1.5.0 throws and 1.4.9 did not.

Root cause

A value's nextOrigin (the file its text now belongs to) leaked onto values that did not wholly come from that file. The next pass then resolved a file() ref in the value from that file's lineage and reported a cycle.

  1. Mixed value + self ref.
    b: sl-${file(./mod.cjs):naming.name}-${env:TOK}
    x: ${self:b}
    Resolving the embedded file() set mod.cjs as b's next origin. x resolved ${self:b} to ${deep:0}, and resolving that deep value carried mod.cjs onto x, which assignProperties stored for path x. Re-resolving the embedded file() reported stack.yml -> mod.cjs -> mod.cjs.
    Fix: populateValue keeps the prior origin unless the value is one whole reference.
  2. Fallback chain.
    slug: ${env:A, env:B, file('./config.json'):slug, 'lit'}
    Preprocessing nests the second link (${env:A, ${env:B}, ...}). shortCircuitFallbacks probes the list's plain items up front, so it resolved the file() item too (index 2, after the nested match at index 1). That value was thrown away, but its origin stayed on the valueObject. The next pass reported config.json -> config.json. It only shows up with two or more failing links before a file() link that is not last. The parent-dir walk-up is not needed to trigger it.
    Fix: after probing, the origin goes back to what it was before the probe. Only a probe whose value replaces its list keeps its origin.

A fallback list that is a whole reference, and whose nested item wins, still passes that item's origin on. So a file object with relative file() refs inside a fallback still resolves next to that file.

Tests

In tests/reliability/fileOrigins.test.js, each test runs through both the async and sync APIs:

  • a self ref to a mixed file() + env: value
  • fallback chains: a parent-dir config.json, a local file, and a file() followed by another file()
  • real cycles behind a mixed value and behind a fallback chain are still detected

The first two fail on master. The full suite passes: npm test (8/8, 27/27, 863/863, 1810/1810), plus typecheck and the configx tests.

Real-world check (no AWS): SaaSLayer services/permissions-service/ui/stack.yml (sync and async) and services/user-service/stack.yml, standalone without slug/prefix env. Both fail with a circular error on 1.5.0 and resolve on this branch.

A value's next origin (the file its text now belongs to) leaked in two ways,
so a later file() ref in the value looked like it came from that same file:

- A value mixing file() with other text (sl-${file(./mod.cjs):name}-${env:X})
  took the file's origin. A ${self:} ref to it then carried that origin and
  re-resolving the embedded file() reported mod.cjs -> mod.cjs. populateValue
  now keeps the prior origin unless the value is one whole reference.
- Fallback short-circuiting probes plain items up front. With two failing
  links before a file() link that is not last, the file() probe ran, its value
  went unused, but its origin stuck, so the next pass reported
  config.json -> config.json. Only a probe whose value replaces its list keeps
  its origin now.

Real cycles behind mixed values and fallback chains are still detected.

Validation: configorama npm test (8/8, 27/27, 863/863, 1810/1810), typecheck,
configx tests. New tests fail on 1.5.0 in async and sync APIs.
@DavidWells
DavidWells merged commit c9da5ee into master Oct 9, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant