Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions migrations/3.1-to-4.0/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Migration recipe — methodology 3.1 → 4.0

The on-disk migration recipe an adopter follows when upgrading to methodology version 4.0, whenever that major ships. Format per [`notations/CONTRACT.md`](../../notations/CONTRACT.md) §10.4. Landed ahead of the actual `4.0.0` release per §10.6 — the deprecation's replacement (DGCA) has been settled since `2.0.0`, so the recipe does not wait for the major-release cut.

## What this recipe covers — FGA retirement

`4.0.0` removes the `fga` notation key. FGA (`*.fga.transitrix.yaml`, `notation: fga`) was deprecated in `2.0.0` (2026-07-12) in favour of DGCA with the Changes layer toggled off; see [`notations/views/diagrams/03-fga.md`](../../notations/views/diagrams/03-fga.md) and [CONTRACT.md](../../notations/CONTRACT.md) §10.6.

| Location | 3.x form | 4.0 form |
|---|---|---|
| File name | `*.fga.transitrix.yaml` | `*.dgca.transitrix.yaml` |
| `notation:` field | `notation: fga` | `notation: dgca` |
| Changes layer | absent (FGA had no Changes layer) | `view_config.layers.changes: off` |
| `factors[]` / `goals[]` / `actions[]` | unchanged | unchanged |

FGA had no `changes[]` layer at all — `view_config.layers.changes: off` is exactly the DGCA config that reproduces that 3-layer Driver → Goal → Activity shape (DGA mode; see `02-dgca.md` §"Layer toggles"). No other field is renamed.

## What to migrate

### Step 1 — Rename the file and swap the notation key

```bash
mv strategy.fga.transitrix.yaml strategy.dgca.transitrix.yaml
```

```yaml
# Before
notation: fga

# After
notation: dgca
```

### Step 2 — Add the Changes-layer toggle

```yaml
view_config:
layers:
changes: off
```

### Step 3 — Bump `methodology_version`

```yaml
# transitrix.yaml
methodology_version: "4.0.0"
```

### Step 4 — Re-run `repo-check`

```bash
transitrix-ingest repo-check [org-root]
```

## Codemod

`codemod.mjs` automates Steps 1–2.
It is idempotent — re-running on a repo with no remaining `*.fga.transitrix.yaml` files is a no-op.

```bash
# Preview — shows what would change without writing any files
node migrations/3.1-to-4.0/codemod.mjs <adopter-root> --dry-run

# Apply
node migrations/3.1-to-4.0/codemod.mjs <adopter-root>

# Post-migration check
node migrations/3.1-to-4.0/validate.mjs <adopter-root>
```

`validate.mjs` exits `0` if no `*.fga.transitrix.yaml` file and no `notation: fga` remain; exits `1` with the offending file list otherwise.

## Folder shape

```
migrations/3.1-to-4.0/
├── README.md
├── codemod.mjs # idempotent transform; runs Steps 1–2
├── validate.mjs # post-migration check; exits 0 on clean repo
└── fixtures/
├── before/ # minimal adopter repo before migration (fga form)
└── after/ # the same after running codemod.mjs (dgca form)
```
96 changes: 96 additions & 0 deletions migrations/3.1-to-4.0/codemod.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env node
// Migration codemod — methodology 3.1 → 4.0.
//
// Covers the FGA → DGCA notation-key retirement (CONTRACT.md §10.6; spec:
// notations/views/diagrams/03-fga.md). FGA was deprecated in 2.0.0 and is
// removed in 4.0.0.
//
// *.fga.transitrix.yaml → *.dgca.transitrix.yaml (file rename)
// notation: fga → notation: dgca (field rewrite)
// (no view_config) → view_config.layers.changes: off (block insert)
//
// The field schema is otherwise unchanged — factors[]/goals[]/actions[] carry
// over as-is; FGA had no changes[] layer, which is exactly what
// view_config.layers.changes: off produces in DGCA (DGA mode).
//
// Conventions (canonical for every migration recipe):
// - Pure-Node, no native deps; Node ≥ 20.
// - Idempotent: a repo already on 4.0 form (no *.fga.transitrix.yaml) is a no-op.
// - CLI: [--dry-run] [target-dir]. Default target = current working dir.
// - Diff-style summary of changes. Exit 0 on clean run.

import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, renameSync } from 'node:fs';
import { join, relative, resolve, basename, dirname } from 'node:path';

const args = process.argv.slice(2);
const dryRun = args.includes('--dry-run');
const target = resolve(args.find(a => !a.startsWith('--')) ?? process.cwd());

if (!existsSync(target)) {
console.error(`error: target directory does not exist: ${target}`);
process.exit(2);
}

function walkFga(dir, out = []) {
let entries;
try { entries = readdirSync(dir); } catch { return out; }
for (const ent of entries) {
const full = join(dir, ent);
let st;
try { st = statSync(full); } catch { continue; }
if (st.isDirectory()) walkFga(full, out);
else if (st.isFile() && ent.endsWith('.fga.transitrix.yaml')) out.push(full);
}
return out;
}

// Rewrites notation: fga → dgca and inserts view_config.layers.changes: off
// directly after the header block (after generated_at:, or after
// methodology_version: if generated_at is absent).
function rewriteFgaDocument(content) {
let n = 0;
let result = content.replace(/^(notation\s*:\s*)fga\b/m, (_, pre) => { n++; return `${pre}dgca`; });

if (/^view_config\s*:/m.test(result)) return { content: result, modified: n };

const block = '\nview_config:\n layers:\n changes: off\n';
const anchor = result.match(/^generated_at\s*:.*\n/m) ?? result.match(/^methodology_version\s*:.*\n/m);
if (anchor) {
const cut = anchor.index + anchor[0].length;
const rest = result.slice(cut).replace(/^\n+/, '\n');
result = result.slice(0, cut) + block + rest;
} else {
result = `${block.slice(1)}\n${result}`;
}
n++;
return { content: result, modified: n };
}

const files = walkFga(target);
let totalModified = 0;
const touched = [];

for (const f of files) {
let content;
try { content = readFileSync(f, 'utf8'); } catch { continue; }

const r = rewriteFgaDocument(content);
if (r.modified === 0) continue;

const dest = join(dirname(f), basename(f).replace(/\.fga\.transitrix\.yaml$/, '.dgca.transitrix.yaml'));
touched.push(`${relative(target, f)} → ${relative(target, dest)}`);
totalModified += r.modified;

if (!dryRun) {
writeFileSync(f, r.content);
renameSync(f, dest);
}
}

console.log('Transform — notation: fga → dgca; insert view_config.layers.changes: off; file rename');
touched.forEach(x => console.log(` ~ ${x}`));
console.log('');
console.log('Summary:');
console.log(` files scanned ${files.length}`);
console.log(` files changed ${touched.length}${dryRun ? ' (dry-run; no files written)' : ''}`);
process.exit(0);
29 changes: 29 additions & 0 deletions migrations/3.1-to-4.0/fixtures/after/strategy.dgca.transitrix.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
notation: dgca
spec_version: "0.3"
methodology_version: "3.1.0"

id: FGA-STARTUP-1
name: "Product launch — strategy chain"
period: "2026"
generated_at: "2026-07-14"

view_config:
layers:
changes: off

factors:
- id: DRIVER-MARKET-1
name: "Growing demand for automated reporting"
type: external
category: technological

goals:
- id: GOAL-1
name: "Launch our analytics product to market"
factors: [DRIVER-MARKET-1]

actions:
- id: ACTION-1
name: "Analytics MVP"
type: Project
goals: [GOAL-1]
25 changes: 25 additions & 0 deletions migrations/3.1-to-4.0/fixtures/before/strategy.fga.transitrix.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
notation: fga
spec_version: "0.3"
methodology_version: "3.1.0"

id: FGA-STARTUP-1
name: "Product launch — strategy chain"
period: "2026"
generated_at: "2026-07-14"

factors:
- id: DRIVER-MARKET-1
name: "Growing demand for automated reporting"
type: external
category: technological

goals:
- id: GOAL-1
name: "Launch our analytics product to market"
factors: [DRIVER-MARKET-1]

actions:
- id: ACTION-1
name: "Analytics MVP"
type: Project
goals: [GOAL-1]
47 changes: 47 additions & 0 deletions migrations/3.1-to-4.0/validate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env node
// Post-migration validation — methodology 3.1 → 4.0 (FGA retirement).
//
// Asserts no FGA residue remains:
// - no *.fga.transitrix.yaml file
// - no file with notation: fga
//
// Exit 0 = clean (fully on 4.0 form); Exit 1 = FGA residue found (run codemod again).

import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
import { join, relative, resolve } from 'node:path';

const target = resolve(process.argv.slice(2).find(a => !a.startsWith('--')) ?? process.cwd());
if (!existsSync(target)) { console.error(`error: no such dir: ${target}`); process.exit(2); }

function walk(dir, out = []) {
let ents; try { ents = readdirSync(dir); } catch { return out; }
for (const e of ents) {
const f = join(dir, e);
let st; try { st = statSync(f); } catch { continue; }
if (st.isDirectory()) walk(f, out);
else if (st.isFile() && e.endsWith('.yaml')) out.push(f);
}
return out;
}

const problems = [];

for (const f of walk(target)) {
const rel = relative(target, f);

if (f.endsWith('.fga.transitrix.yaml'))
problems.push(`${rel}: *.fga.transitrix.yaml — run codemod to rename to *.dgca.transitrix.yaml`);

const c = readFileSync(f, 'utf8');
if (/^notation\s*:\s*fga\b/m.test(c))
problems.push(`${rel}: notation: fga — run codemod to migrate to notation: dgca`);
}

if (problems.length) {
console.error(`FAIL — ${problems.length} FGA residue(s):`);
problems.forEach(p => console.error(` ✗ ${p}`));
process.exit(1);
}

console.log('PASS — no FGA residue; repo is fully on 4.0 form.');
process.exit(0);
11 changes: 11 additions & 0 deletions notations/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,17 @@ Worked examples: [`migrations/0.5-to-0.6/`](../migrations/0.5-to-0.6/) and [`mig
- **Per-notation versioning.** `spec_version` on individual files is informational; only `methodology_version` in `transitrix.yaml` drives compatibility decisions.
- **Migration for adopter repositories of non-methodology versions** (DSM, Studio, CLI). Those have their own SemVer policies.

### 10.6 Deprecation policy

A spec file marked `status: "deprecated"` in its own front matter (§1 — the spec-authoring header, distinct from an adopter's notation-file header) names its removal release in the same change that deprecates it. Decided 2026-08-03.

- **A deprecation names its removal release.** The front matter carries `removed_in: "X.0.0"` alongside `status: "deprecated"`, and the spec body states it in prose. A deprecation with no stated end is not a deprecation — it is an unmaintained file. Checked by the `DEP1` doc-lint rule in [`check-notations.mjs`](../scripts/check-notations.mjs).
- **The window is at least one MAJOR.** Deprecated during a `2.x` release → removable in `3.0.0` at the earliest. Ordinary SemVer (§10.2); no local invention of a shorter or longer window.
- **Removal is always a `BREAKING` CHANGELOG entry** — folded into the `MAJOR` bump that performs it, never a silent tidy-up inside a `MINOR` or `PATCH` release.
- **The migration recipe outlives the file it replaces.** When a deprecated spec is deleted, its migration instructions move into `migrations/<from>-to-<to>/` (§10.4 shape) rather than disappearing with it. The recipe MAY land ahead of the actual removal, once the deprecation's replacement is settled — waiting until the major-release cut is not required and risks authoring it under time pressure.

**Historical note.** The `3.0.0` release (`CHANGELOG.md`) removed `HAZARD`, `RISK_CONTROL`, and the Design-Controls Trace Matrix view one minor after they shipped (`2.1.0` → `3.0.0`), with no deprecation window. That CHANGELOG entry is a record of what happened and is not rewritten to imply the window above was honoured — this section states the rule going forward, not retroactively.

---

## 11. Confidence and freshness
Expand Down
5 changes: 3 additions & 2 deletions notations/views/diagrams/03-fga.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ version: "0.3"
author: "Valerii Korobeinikov"
last_updated: "2026-06-23"
status: "deprecated"
removed_in: "4.0.0"
file_extension: "*.fga.transitrix.yaml"
dsm_status: "not implemented — superseded by dgca with layers.changes: off"
---

# FGA Notation — Deprecated

> **This notation is superseded.** FGA (`*.fga.transitrix.yaml`, `notation: fga`) is replaced by the DGCA notation with the Changes layer toggled off.
> **This notation is superseded and scheduled for removal in `4.0.0`.** FGA (`*.fga.transitrix.yaml`, `notation: fga`) is replaced by the DGCA notation with the Changes layer toggled off. It was deprecated in `2.0.0` (2026-07-12); the one-major window (CONTRACT.md §10.6) is satisfied as of `3.0.0`, so removal is scheduled for the next major release. Removal is not performed inside a `MINOR` or `PATCH` release.
>
> **Migrate:** rename your file to `*.dgca.transitrix.yaml`, change `notation: fga` → `notation: dgca`, and add:
> **Migrate:** see the recipe under [`migrations/3.1-to-4.0/`](../../../migrations/3.1-to-4.0/) — rename your file to `*.dgca.transitrix.yaml`, change `notation: fga` → `notation: dgca`, and add:
>
> ```yaml
> view_config:
Expand Down
29 changes: 28 additions & 1 deletion scripts/check-notations.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const VERSION_PIN_ALLOWLIST = new Set([
'migrations/0.7-to-1.0/fixtures/before/canon/views/compliance-impact/retail.compliance-impact.transitrix.yaml', // pre-migration fixture
'migrations/0.7-to-1.0/fixtures/after/canon/views/compliance-impact/retail.compliance-impact.transitrix.yaml', // post-migration fixture
'migrations/1.0-to-2.0/README.md', // documents the target version, not the current pin
'migrations/3.1-to-4.0/README.md', // documents the target version, not the current pin
'migrations/1.0-to-2.0/fixtures/after/canon/views/goals/strategy-2026.goals.transitrix.yaml', // post-migration fixture
'migrations/1.0-to-2.0/fixtures/after/canon/views/action/platform-launch.action.transitrix.yaml', // post-migration fixture
'migrations/2.1-to-3.0/fixtures/before/canon/views/design-controls-trace-matrix/example.design-controls-trace-matrix.transitrix.yaml', // pre-migration fixture
Expand Down Expand Up @@ -287,11 +288,33 @@ async function readClassDir(dir) {
const text = await readFile(join(dir, e.name), 'utf8');
if (!/^notation:\s*/m.test(text)) continue;
const statusM = text.match(/^status:\s*"?(\w+)"?/m);
out.push({ name: e.name, deprecated: statusM ? statusM[1] === 'deprecated' : false });
const removedInM = text.match(/^removed_in:\s*"?([^"\s]*)"?/m);
out.push({
name: e.name,
deprecated: statusM ? statusM[1] === 'deprecated' : false,
removedIn: removedInM ? removedInM[1] : null,
});
}
return out;
}

// Pure — no I/O. specs: [{ name, deprecated, removedIn }] as read from a spec
// directory's front matter. CONTRACT.md §10.6: a spec marked
// status: "deprecated" must also carry removed_in: — a deprecation with no
// stated end is not a deprecation.
export function deriveDeprecationFailures(specs, dirLabel) {
const failures = [];
for (const s of specs) {
if (s.deprecated && !s.removedIn) {
failures.push({
check: 'DEP1',
message: `${dirLabel}/${s.name}: status: "deprecated" with no removed_in: — a deprecation names its removal release (CONTRACT.md §10.6).`,
});
}
}
return failures;
}

async function checkNotationCounts(failures) {
const filesByClass = {
diagrams: await readClassDir(join(VIEWS_DIR, 'diagrams')),
Expand All @@ -300,6 +323,10 @@ async function checkNotationCounts(failures) {
};
const counts = deriveClassCounts(filesByClass);

for (const [cls, files] of Object.entries(filesByClass)) {
failures.push(...deriveDeprecationFailures(files, `notations/views/${cls}`));
}

const elemCount = (await readdir(ELEMENTS_DIR, { withFileTypes: true }))
.filter(e => e.isFile() && e.name.endsWith('.md')).length;

Expand Down
Loading
Loading