diff --git a/pgpm/naming-spec/README.md b/pgpm/naming-spec/README.md
new file mode 100644
index 000000000..287aa196f
--- /dev/null
+++ b/pgpm/naming-spec/README.md
@@ -0,0 +1,43 @@
+# @pgpmjs/naming-spec
+
+
+
+
+
+
+
+
+
+
+
+
+
+The PGPM naming spec — canonical, **derived** change paths.
+
+A change path is never authored and never identity: it is a pure projection of an
+object's identity through this spec. Objects (content-addressed ASTs + dependency
+edges) are the source of truth; paths are re-derivable at any time, so regrouping,
+renaming schemes, or repartitioning packages can never break identity-keyed
+consumers (diff, dependency resolution).
+
+```ts
+import { pathFor } from '@pgpmjs/naming-spec';
+import { classifyStatements, identityOf } from '@pgsql/transform';
+
+const facts = classifyStatements('CREATE TABLE app.users (id int);')[0];
+pathFor(identityOf(facts)!); // 'schemas/app/tables/users/table'
+```
+
+Canonical templates (naming spec v1, the conventions used across constructive-db):
+
+| kind | path |
+|------|------|
+| schema | `schemas/{schema}/schema` |
+| table | `schemas/{schema}/tables/{table}/table` |
+| trigger / policy / index / constraint / seed | `schemas/{schema}/tables/{table}/{triggers\|policies\|indexes\|constraints\|fixtures}/{name}` |
+| function / view / type / sequence | `schemas/{schema}/{procedures\|views\|types\|sequences}/{name}` |
+| extension | `extensions/{name}` |
+| role | `roles/{name}` |
+
+Pure leaf with zero dependencies. Identity derivation (`identityOf`) lives upstream
+in `@pgsql/transform`; this package only renders identities to paths.
diff --git a/pgpm/naming-spec/__tests__/naming-spec.test.ts b/pgpm/naming-spec/__tests__/naming-spec.test.ts
new file mode 100644
index 000000000..5a7dad823
--- /dev/null
+++ b/pgpm/naming-spec/__tests__/naming-spec.test.ts
@@ -0,0 +1,62 @@
+import { alterationPathFor, ObjectIdentity, pathFor, PGPM_NAMING_SPEC_VERSION } from '../src';
+
+const id = (partial: Partial & Pick): ObjectIdentity => ({
+ schema: 'app',
+ ...partial
+});
+
+describe('PGPM naming spec v1', () => {
+ it('declares its spec version', () => {
+ expect(PGPM_NAMING_SPEC_VERSION).toBe(1);
+ });
+
+ it('renders canonical directory-style paths per object kind (db_deps parity)', () => {
+ expect(pathFor(id({ kind: 'schema', schema: null, name: 'app' }))).toBe('schemas/app/schema');
+ expect(pathFor(id({ kind: 'table', name: 'users' }))).toBe('schemas/app/tables/users/table');
+ expect(pathFor(id({ kind: 'view', name: 'v_users' }))).toBe('schemas/app/views/v_users/view');
+ expect(pathFor(id({ kind: 'function', name: 'fn' }))).toBe('schemas/app/procedures/fn/procedure');
+ expect(pathFor(id({ kind: 'type', name: 'status' }))).toBe('schemas/app/types/status');
+ expect(pathFor(id({ kind: 'sequence', name: 'seq' }))).toBe('schemas/app/sequences/seq');
+ expect(pathFor(id({ kind: 'extension', schema: null, name: 'pgcrypto' }))).toBe('extensions/pgcrypto');
+ expect(pathFor(id({ kind: 'role', schema: null, name: 'admin' }))).toBe('roles/admin');
+ });
+
+ it('renders flat-style paths for hand-authored layouts', () => {
+ const flat = { style: 'flat' as const };
+ expect(pathFor(id({ kind: 'function', name: 'fn' }), flat)).toBe('schemas/app/procedures/fn');
+ expect(pathFor(id({ kind: 'view', name: 'v_users' }), flat)).toBe('schemas/app/views/v_users');
+ expect(pathFor(id({ kind: 'policy', name: 'p', table: 'users' }), flat))
+ .toBe('schemas/app/tables/users/policies/p');
+ });
+
+ it('scopes table-owned objects under the table', () => {
+ expect(pathFor(id({ kind: 'trigger', name: 'trg', table: 'users' })))
+ .toBe('schemas/app/tables/users/triggers/trg');
+ expect(pathFor(id({ kind: 'policy', name: 'p', table: 'users' })))
+ .toBe('schemas/app/tables/users/policies/p/policy');
+ expect(pathFor(id({ kind: 'index', name: 'users_email_idx', table: 'users' })))
+ .toBe('schemas/app/tables/users/indexes/users_email_idx');
+ expect(pathFor(id({ kind: 'constraint', name: 'users_pkey', table: 'users' })))
+ .toBe('schemas/app/tables/users/constraints/users_pkey/constraint');
+ expect(pathFor(id({ kind: 'seed_dml', name: 'users', table: 'users' })))
+ .toBe('schemas/app/tables/users/fixtures/users');
+ });
+
+ it('numbers re-alterations like db_deps.next_alteration', () => {
+ const parent = 'schemas/app/tables/users/table';
+ expect(alterationPathFor(parent, 1)).toBe('schemas/app/tables/users/table/alterations/alt0000000001');
+ expect(alterationPathFor(alterationPathFor(parent, 1), 2))
+ .toBe('schemas/app/tables/users/table/alterations/alt0000000002');
+ });
+
+ it('defaults missing schema to public and unknown kinds to objects/', () => {
+ expect(pathFor(id({ kind: 'table', schema: null, name: 'users' })))
+ .toBe('schemas/public/tables/users/table');
+ expect(pathFor(id({ kind: 'other', name: 'thing' }))).toBe('schemas/app/objects/thing');
+ });
+
+ it('is a pure projection: same identity, same path, no state', () => {
+ const identity = id({ kind: 'table', name: 'users' });
+ expect(pathFor(identity)).toBe(pathFor({ ...identity }));
+ });
+});
diff --git a/pgpm/naming-spec/jest.config.js b/pgpm/naming-spec/jest.config.js
new file mode 100644
index 000000000..057a9420e
--- /dev/null
+++ b/pgpm/naming-spec/jest.config.js
@@ -0,0 +1,18 @@
+/** @type {import('ts-jest').JestConfigWithTsJest} */
+module.exports = {
+ preset: 'ts-jest',
+ testEnvironment: 'node',
+ transform: {
+ '^.+\\.tsx?$': [
+ 'ts-jest',
+ {
+ babelConfig: false,
+ tsconfig: 'tsconfig.json',
+ },
+ ],
+ },
+ transformIgnorePatterns: [`/node_modules/*`],
+ testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
+ modulePathIgnorePatterns: ['dist/*']
+};
diff --git a/pgpm/naming-spec/package.json b/pgpm/naming-spec/package.json
new file mode 100644
index 000000000..106cf2ac1
--- /dev/null
+++ b/pgpm/naming-spec/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "@pgpmjs/naming-spec",
+ "version": "0.0.1",
+ "author": "Constructive ",
+ "description": "PGPM naming spec — canonical, derived change paths rendered from object identity; paths are projections, never authored and never identity",
+ "main": "index.js",
+ "module": "esm/index.js",
+ "types": "index.d.ts",
+ "homepage": "https://github.com/constructive-io/constructive",
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public",
+ "directory": "dist"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/constructive-io/constructive"
+ },
+ "bugs": {
+ "url": "https://github.com/constructive-io/constructive/issues"
+ },
+ "scripts": {
+ "clean": "makage clean",
+ "prepack": "npm run build",
+ "build": "makage build",
+ "build:dev": "makage build --dev",
+ "lint": "eslint . --fix",
+ "test": "jest --passWithNoTests",
+ "test:watch": "jest --watch"
+ },
+ "keywords": [
+ "pgpm",
+ "pgpmjs",
+ "naming",
+ "spec",
+ "postgresql",
+ "migrations"
+ ],
+ "devDependencies": {
+ "makage": "^0.3.0"
+ }
+}
diff --git a/pgpm/naming-spec/src/index.ts b/pgpm/naming-spec/src/index.ts
new file mode 100644
index 000000000..528c0d0d6
--- /dev/null
+++ b/pgpm/naming-spec/src/index.ts
@@ -0,0 +1,170 @@
+/**
+ * PGPM naming spec v1 — canonical, derived change paths.
+ *
+ * A change path is never authored and never identity: it is a pure projection
+ * of an object's identity through this spec. Objects (content-addressed ASTs
+ * + dependency edges) are the source of truth; paths are re-derivable at any
+ * time, so regrouping, renaming schemes, or repartitioning packages can never
+ * break identity-keyed consumers (diff, dependency resolution).
+ *
+ * The identity itself is produced upstream (`identityOf` in
+ * `@pgsql/transform`, a pure function of classifier facts). This package only
+ * renders identities to paths; the `ObjectIdentity` shape below is
+ * structurally identical to the upstream type, so either can be passed.
+ *
+ * The reference implementation of these templates is constructive-db's
+ * `db_deps` SQL package (`db_deps.table_deps`, `db_deps.column_deps`,
+ * `db_deps.next_alteration`, ...); this package is the TypeScript rendering
+ * of the same spec, so paths derived in SQL and in TS agree byte-for-byte.
+ *
+ * Canonical templates (naming spec v1, `directory` style — matching
+ * `db_deps.*` and the generated `application/constructive` plan):
+ *
+ * schema schemas/{schema}/schema
+ * table schemas/{schema}/tables/{table}/table
+ * column schemas/{schema}/tables/{table}/columns/{name}/column
+ * constraint schemas/{schema}/tables/{table}/constraints/{name}/constraint
+ * policy schemas/{schema}/tables/{table}/policies/{name}/policy
+ * rls schemas/{schema}/tables/{table}/policies/enable_row_level_security
+ * trigger schemas/{schema}/tables/{table}/triggers/{name}
+ * index schemas/{schema}/tables/{table}/indexes/{name}
+ * seed_dml schemas/{schema}/tables/{table}/fixtures/{name}
+ * function schemas/{schema}/procedures/{name}/procedure
+ * view schemas/{schema}/views/{name}/view
+ * type schemas/{schema}/types/{name}
+ * sequence schemas/{schema}/sequences/{name}
+ * extension extensions/{name}
+ * role roles/{name}
+ *
+ * `flat` style drops the trailing kind token for functions/views/columns/
+ * constraints/policies (`schemas/{s}/procedures/{n}`) — the convention used
+ * by hand-authored constructive-db packages (`db_deps.fn_deps`).
+ *
+ * Conflicts/re-alterations: when the same object is altered again, the spec
+ * appends a monotonically numbered alteration segment (`db_deps.next_alteration`):
+ * `/alterations/alt0000000042` — see {@link alterationPathFor}.
+ */
+
+/** Spec version, so bundles/modules can declare which scheme derived their paths. */
+export const PGPM_NAMING_SPEC_VERSION = 1;
+
+/** The kinds of objects the naming spec assigns paths to. */
+export type ObjectIdentityKind =
+ | 'schema'
+ | 'extension'
+ | 'role'
+ | 'table'
+ | 'view'
+ | 'sequence'
+ | 'type'
+ | 'function'
+ | 'index'
+ | 'trigger'
+ | 'policy'
+ | 'constraint'
+ | 'seed_dml'
+ | 'other';
+
+/**
+ * The identity of a database object — what a change path is derived from.
+ * Structurally identical to `ObjectIdentity` in `@pgsql/transform`.
+ */
+export interface ObjectIdentity {
+ kind: ObjectIdentityKind;
+ /** Owning schema (`null` for non-schema objects: roles, extensions). */
+ schema: string | null;
+ /** Object name, unqualified (for table-scoped kinds: without the table). */
+ name: string;
+ /** Owning table, for objects only unique per table (trigger/policy/index/constraint/seed). */
+ table?: string;
+}
+
+/**
+ * Rendering style.
+ *
+ * - `directory` — every object is a directory closed by a kind token
+ * (`.../procedures/{n}/procedure`), as produced by `db_deps.*` and the
+ * generated `application/constructive` plan. The default.
+ * - `flat` — schema-scoped objects are leaf files (`.../procedures/{n}`),
+ * as used by hand-authored constructive-db packages.
+ */
+export type PathStyle = 'directory' | 'flat';
+
+export interface PathForOptions {
+ style?: PathStyle;
+}
+
+/** Kinds whose objects are scoped to (and only unique within) a table. */
+const TABLE_SCOPED = new Set([
+ 'trigger',
+ 'policy',
+ 'index',
+ 'constraint',
+ 'seed_dml'
+]);
+
+/** Directory names for schema-scoped object kinds. */
+const SCHEMA_DIRS: Partial> = {
+ view: 'views',
+ sequence: 'sequences',
+ type: 'types',
+ function: 'procedures'
+};
+
+/** Directory names for table-scoped object kinds. */
+const TABLE_DIRS: Partial> = {
+ trigger: 'triggers',
+ policy: 'policies',
+ index: 'indexes',
+ constraint: 'constraints',
+ seed_dml: 'fixtures'
+};
+
+/** Trailing kind tokens in `directory` style (mirrors `db_deps.*_deps`). */
+const KIND_TOKENS: Partial> = {
+ function: 'procedure',
+ view: 'view',
+ policy: 'policy',
+ constraint: 'constraint'
+};
+
+/**
+ * Render an identity to its canonical pgpm change path (naming spec v1).
+ * Total and deterministic: every identity gets exactly one path per style.
+ */
+export function pathFor(identity: ObjectIdentity, options: PathForOptions = {}): string {
+ const style = options.style ?? 'directory';
+ const { kind, name } = identity;
+ const schema = identity.schema ?? 'public';
+ const token = style === 'directory' ? KIND_TOKENS[kind] : undefined;
+
+ if (kind === 'schema') return `schemas/${name}/schema`;
+ if (kind === 'extension') return `extensions/${name}`;
+ if (kind === 'role') return `roles/${name}`;
+ if (kind === 'table') return `schemas/${schema}/tables/${name}/table`;
+
+ if (TABLE_SCOPED.has(kind)) {
+ const dir = TABLE_DIRS[kind]!;
+ const base = `schemas/${schema}/tables/${identity.table ?? name}/${dir}/${name}`;
+ return token ? `${base}/${token}` : base;
+ }
+
+ const dir = SCHEMA_DIRS[kind];
+ if (dir) {
+ const base = `schemas/${schema}/${dir}/${name}`;
+ return token ? `${base}/${token}` : base;
+ }
+ return `schemas/${schema}/objects/${name}`;
+}
+
+/**
+ * Path for the nth re-alteration of an object (the conflict convention):
+ * `/alterations/alt0000000042`, mirroring `db_deps.next_alteration`.
+ * Any existing alteration suffix on `parent` is stripped first, so the same
+ * parent can be renumbered. Sequencing (the counter) is the caller's state;
+ * this renders deterministically from `(parent, n)`.
+ */
+export function alterationPathFor(parent: string, n: number, prefix = 'alt'): string {
+ const stripped = parent.replace(new RegExp(`/alterations/${prefix}[0-9]+$`), '');
+ return `${stripped}/alterations/${prefix}${String(n).padStart(10, '0')}`;
+}
diff --git a/pgpm/naming-spec/tsconfig.esm.json b/pgpm/naming-spec/tsconfig.esm.json
new file mode 100644
index 000000000..800d7506d
--- /dev/null
+++ b/pgpm/naming-spec/tsconfig.esm.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist/esm",
+ "module": "es2022",
+ "rootDir": "src/",
+ "declaration": false
+ }
+}
diff --git a/pgpm/naming-spec/tsconfig.json b/pgpm/naming-spec/tsconfig.json
new file mode 100644
index 000000000..1a9d5696c
--- /dev/null
+++ b/pgpm/naming-spec/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src/"
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["dist", "node_modules", "**/*.spec.*", "**/*.test.*"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 78c318f74..5c8583858 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -3051,6 +3051,13 @@ importers:
version: 0.3.0
publishDirectory: dist
+ pgpm/naming-spec:
+ devDependencies:
+ makage:
+ specifier: ^0.3.0
+ version: 0.3.0
+ publishDirectory: dist
+
pgpm/portability:
dependencies:
'@pgpmjs/core':