affaan-m/ECC · error · Error

Cannot merge ECC configuration at ${destinationPath}: expect

Error message

Cannot merge ECC configuration at ${destinationPath}: expected a JSON object.

What it means

Thrown by assertMergeDestination when the destination file parses as JSON but is not a JSON object — i.e., it is null, an array, or a non-object primitive. merge-json merges ECC's payload into an object root; merging into an array or scalar is undefined and would destroy the existing structure, so the installer refuses. This is a type guard on the shape of the existing config.

Source

Thrown at scripts/lib/multi-harness-setup.js:197

        `Refusing unverified ownership from install-state at ${plan.installStatePath}: `
        + `content digest does not match ${destinationPath}.`
      );
    }
    destinations.add(canonicalDestination);
  }
  return { destinations, stateFingerprint: validatedFingerprint };
}

function assertMergeDestination(destinationPath) {
  if (!fs.existsSync(destinationPath)) return null;
  let current;
  try {
    current = JSON.parse(fs.readFileSync(destinationPath, 'utf8'));
  } catch (error) {
    throw new Error(`Cannot merge ECC configuration into invalid JSON at ${destinationPath}: ${error.message}`);
  }
  if (!current || typeof current !== 'object' || Array.isArray(current)) {
    throw new Error(`Cannot merge ECC configuration at ${destinationPath}: expected a JSON object.`);
  }
  return current;
}

function isPlainObject(value) {
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}

function findJsonConflicts(current, patch, prefix = '') {
  if (!isPlainObject(patch)) return [];
  return Object.entries(patch).flatMap(([key, patchValue]) => {
    if (!Object.prototype.hasOwnProperty.call(current, key)) return [];
    const currentValue = current[key];
    const field = prefix ? `${prefix}.${key}` : key;
    if (isPlainObject(currentValue) && isPlainObject(patchValue)) {
      return findJsonConflicts(currentValue, patchValue, field);
    }
    return JSON.stringify(currentValue) === JSON.stringify(patchValue) ? [] : [field];

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Inspect destinationPath: if it should be an object, rewrite it as {} or a proper object literal and re-run.
  2. If the file legitimately holds an array/scalar, it cannot be a merge-json target — exclude it from the managed profile or change the operation kind.
  3. If the file is null/empty, replace it with {} so ECC can merge into it.
  4. Verify the ECC catalog entry for this destination is correct (kind should be merge-json only for object configs).

Example fix

// before: destination contains an array
// file: ["a", "b"]
classifyManagedOperation(op, owned); // throws [290]

// after: make it an object root
// file: { "items": ["a", "b"] }
Defensive patterns

Strategy: type-guard

Validate before calling

const fs = require('fs');
function assertMergeDestinationIsObject(destinationPath) {
  if (!fs.existsSync(destinationPath)) return;
  const v = JSON.parse(fs.readFileSync(destinationPath, 'utf8'));
  if (!v || typeof v !== 'object' || Array.isArray(v)) {
    throw new Error(`Cannot merge into ${destinationPath}: root is ${Array.isArray(v) ? 'array' : typeof v}, expected a JSON object.`);
  }
}
assertMergeDestinationIsObject(operation.destinationPath);

Type guard

function isJsonObjectFile(filePath) {
  if (!require('fs').existsSync(filePath)) return true;
  const v = JSON.parse(require('fs').readFileSync(filePath, 'utf8'));
  return Boolean(v) && typeof v === 'object' && !Array.isArray(v);
}

Try / catch

try {
  classifyManagedOperation(operation, ownedDestinations);
} catch (err) {
  if (/expected a JSON object/.test(err.message)) {
    throw new Error(`${operation.destinationPath} is not a JSON object; convert it or remove it before merge.`);
  }
  throw err;
}

Prevention

When it happens

Trigger: Fires when the parsed `current` is falsy, typeof !== 'object', or Array.isArray(current) for a merge-json destination. Occurs when a settings file that ECC expects to be an object is actually a JSON array (e.g., a TSConfig 'extends' list stored standalone), a bare string/number, or the literal null.

Common situations: A file like .eslintignore or a JSON array config living at a path ECC treats as an object merge target; null written by another tool to 'clear' a config; a generated manifest that is an array at the root; mislabeling an operation kind (merge-json) for a destination that is fundamentally not an object.

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/11fd29f9df670828. Report an issue: GitHub.