# How update-system.mjs Enforces the Data Contract Boundary in Career-Ops

> Learn how update-system.mjs enforces the data contract boundary in career-ops by protecting user data through strict separation, static paths, runtime validation, and automatic violation detection.

- Repository: [Santiago Fernández de Valderrama/career-ops](https://github.com/santifer/career-ops)
- Tags: internals
- Published: 2026-08-22

---

**The `update-system.mjs` script enforces the data contract boundary by maintaining strict separation between system-layer files (which it can update) and user-layer files (which it protects), using static path declarations, runtime validation, and automatic violation detection to abort any update that would overwrite user data.**

The Career-Ops repository implements a strict two-layer architecture to separate project infrastructure from personal content. The `update-system.mjs` auto-updater serves as the gatekeeper for this boundary, ensuring that automatic upgrades modify only the system layer while leaving every user-authored CV, profile, and report untouched. This mechanism is formally defined in [`DATA_CONTRACT.md`](https://github.com/santifer/career-ops/blob/main/DATA_CONTRACT.md) and implemented through a series of defensive checks in the updater source code.

## Static Declarations of System and User Layers

The boundary enforcement relies on two exhaustive static arrays declared in `update-system.mjs`.

The `SYSTEM_PATHS` array (lines 63‑73) contains an exhaustive list of paths belonging to the **system layer**—scripts, mode definitions, dashboards, and templates that ship with the project. These files are explicitly allowed to be overwritten during automatic updates.

Conversely, the `USER_PATHS` export (lines 104‑112) defines the **user layer** data contract. This hard-coded list protects files like [`cv.md`](https://github.com/santifer/career-ops/blob/main/cv.md), [`config/profile.yml`](https://github.com/santifer/career-ops/blob/main/config/profile.yml), [`modes/_profile.md`](https://github.com/santifer/career-ops/blob/main/modes/_profile.md), and the `reports/` directory from any automatic modification. These paths represent personal content that must survive system upgrades untouched.

## Dynamic Path Resolution and Local Overrides

To accommodate fork-level customization without modifying source code, the script supports extending the user layer through an optional configuration file.

The `localUserPaths()` helper (lines 75‑84) parses [`config/local-paths.txt`](https://github.com/santifer/career-ops/blob/main/config/local-paths.txt) and validates each entry, rejecting absolute paths, parent directory references (`..`), and backslashes. This validation prevents path traversal attacks while allowing legitimate extensions to the protected user list.

The `effectiveUserPaths()` function (lines 133‑143) merges the built-in `USER_PATHS` export with any locally declared paths from `localUserPaths()`, producing the definitive runtime list of files protected by the data contract.

## Violation Detection and Update Abort Logic

Before applying any changes, the updater validates operations against the effective user list. The `userLayerViolations()` function (lines 64‑73) iterates through every file modified in the upstream checkout. If a changed file exists within the effective user list and is not explicitly allowed by the update's `updatePaths` parameter, the function flags it as a contract violation.

During the `apply()` workflow, the script invokes `userLayerViolations()` and immediately aborts if violations are detected, outputting a JSON status describing the conflict. This ensures that automatic updates never proceed when user-layer files would be affected.

## System Integrity and Backup Safeguards

Beyond protecting user files, the script implements safeguards for system file integrity and preservation of local modifications to system files.

The `missingFromTargetManifest()` function (lines 124‑136) verifies that every path listed in `SYSTEM_PATHS` actually materializes on disk after the checkout. This prevents silent partial updates that could leave the system in an inconsistent state.

For cases where users have legitimately edited system-layer files, the `locallyModifiedSystemFiles()` function (lines 88‑102) identifies system files that differ from upstream. Rather than silently overwriting these edits, the script backs up modified files with a `.bak` extension before replacement, satisfying the "no data loss" guarantee even for system-layer content.

## Programmatic and CLI Usage

Developers can invoke the boundary checks programmatically or via command line interface.

```js
// Example: checking an upcoming update for user‑layer violations
import { gitStatusEntries, effectiveUserPaths, userLayerViolations } from './update-system.mjs';

const changed = gitStatusEntries();                   // ← all files the update would modify
const updatePaths = SYSTEM_PATHS;                     // ← allowed paths for this run
const userPaths = effectiveUserPaths();               // ← full user‑layer list
const violations = userLayerViolations(changed, updatePaths, userPaths);

if (violations.length) {
  console.error('Update aborted – user‑layer files would be overwritten:');
  console.error(violations);
  process.exit(1);
}

```

```bash

# CLI usage – the script itself performs the same checks internally

node update-system.mjs check   # prints JSON with status/offline/etc.

node update-system.mjs apply   # aborts if any user‑layer violation is detected

```

## Summary

- The data contract separates `SYSTEM_PATHS` (updatable infrastructure) from `USER_PATHS` (protected user content) via static declarations in `update-system.mjs`.
- `localUserPaths()` and `effectiveUserPaths()` allow fork-specific extensions while validating against path traversal vulnerabilities.
- `userLayerViolations()` detects potential overwrites before they occur, triggering immediate aborts in the `apply()` workflow if user files are at risk.
- `missingFromTargetManifest()` ensures complete system updates without silent file omissions.
- `locallyModifiedSystemFiles()` backs up user edits to system files with `.bak` extensions before overwriting, preventing data loss.

## Frequently Asked Questions

### What happens if I add invalid paths to config/local-paths.txt?

The `localUserPaths()` function validates each entry and rejects absolute paths, parent directory references (`..`), and backslashes. Invalid entries are filtered out before being merged into the effective user list, protecting the repository against path traversal attacks while allowing legitimate extensions to the data contract.

### Does the updater ever overwrite user-layer files?

No. The `userLayerViolations()` function explicitly checks every changed file against the effective user list built from `USER_PATHS` and local overrides. If any user-layer file would be modified, the `apply()` operation aborts immediately and reports the violation via JSON status, ensuring user data remains untouched without explicit consent.

### How does the script handle my edits to system files?

The `locallyModifiedSystemFiles()` function (lines 88‑102) identifies system-layer files that differ from the upstream version. Before overwriting these files during an update, the script automatically creates `.bak` copies, preserving your local modifications while allowing the system upgrade to proceed.

### Where is the data contract formally documented?

The two-layer contract is formally defined in [`DATA_CONTRACT.md`](https://github.com/santifer/career-ops/blob/main/DATA_CONTRACT.md) at the repository root, while the enforcement implementation resides in `update-system.mjs`. Specific boundary logic is concentrated in functions like `effectiveUserPaths()` (lines 133‑143) and `userLayerViolations()` (lines 64‑73) according to the Career-Ops source code.