feat(hierarchy): M4-1b-ii hierarchy command family, grant evaluation, visibility (#1465)
ci/woodpecker/push/publish Pipeline was canceled

This commit was merged in pull request #1465.
This commit is contained in:
2026-08-29 16:54:39 +00:00
parent 5125fe21b0
commit 215faeda0a
20 changed files with 8407 additions and 132 deletions
@@ -0,0 +1,5 @@
ALTER TABLE "hierarchy_audit_events" DROP CONSTRAINT "hierarchy_audit_events_verb_check";--> statement-breakpoint
ALTER TABLE "companies" ADD COLUMN "visibility" text DEFAULT 'private' NOT NULL;--> statement-breakpoint
ALTER TABLE "companies" ADD CONSTRAINT "companies_visibility_check" CHECK (visibility IN ('private', 'directory'));--> statement-breakpoint
ALTER TABLE "hierarchy_audit_events" ADD CONSTRAINT "hierarchy_audit_events_verb_check" CHECK (verb IN ('create', 'rename', 'transfer', 'visibility_change', 'delete', 'grant_create', 'grant_change', 'grant_revoke'));--> statement-breakpoint
ALTER TABLE "hierarchy_grants" ADD CONSTRAINT "hierarchy_grants_role_check" CHECK (role IN ('viewer', 'member', 'owner'));
File diff suppressed because it is too large Load Diff
+7
View File
@@ -141,6 +141,13 @@
"when": 1787880918208,
"tag": "0019_volatile_killraven",
"breakpoints": true
},
{
"idx": 20,
"version": "7",
"when": 1787963521142,
"tag": "0020_special_betty_brant",
"breakpoints": true
}
]
}
@@ -44,7 +44,7 @@ type AnyDb = {
/** Column allowlist — the exact declared sets of §2/§3. Nothing else. */
const COLUMN_ALLOWLIST: Record<string, string[]> = {
companies: ['id', 'name', 'slug', 'created_at', 'updated_at'],
companies: ['id', 'name', 'slug', 'visibility', 'created_at', 'updated_at'],
estates: ['id', 'name', 'slug', 'company_id'],
platform_projects: ['id', 'name', 'slug', 'estate_id'],
workspaces: ['id', 'name', 'slug', 'platform_project_id'],
@@ -377,7 +377,61 @@ function witnessSuite(getHandle: () => AnyDb): void {
// Control: same subject and target with a different role is a new grant.
await db()
.insert(hierarchyGrants)
.values({ userId: userA, companyId, role: `${T}-other-role`, grantedBy: userA });
.values({ userId: userA, companyId, role: 'member', grantedBy: userA });
});
// ── §2.6 role vocabulary CHECK ─────────────────────────────────────────────
it('refuses a grant role outside the ratified vocabulary, accepts each ratified role', async () => {
await expectViolation(
db()
.insert(hierarchyGrants)
.values({ userId: userB, companyId, role: 'superuser', grantedBy: userA }),
/check constraint/i,
);
// Serialized namespaced forms are storage-invalid too: rows hold bare roles.
await expectViolation(
db()
.insert(hierarchyGrants)
.values({ userId: userB, companyId, role: 'hierarchy:owner', grantedBy: userA }),
/check constraint/i,
);
for (const role of ['viewer', 'member', 'owner'] as const) {
await db()
.insert(hierarchyGrants)
.values({ userId: userB, estateId, role, grantedBy: userA });
}
await db().execute(
sql`DELETE FROM hierarchy_grants WHERE user_id = ${userB} AND estate_id = ${estateId}`,
);
});
// ── §2.8 visibility column ─────────────────────────────────────────────────
it('defaults companies.visibility to private and refuses values outside the class', async () => {
const visId = randomUUID();
await db().execute(
sql`INSERT INTO companies (id, name, slug) VALUES (${visId}, 'Vis', ${T + '-vis'})`,
);
const res = rows(await db().execute(sql`SELECT visibility FROM companies WHERE id = ${visId}`));
expect(res[0]!['visibility']).toBe('private');
await db().execute(
sql`INSERT INTO companies (id, name, slug, visibility) VALUES (${randomUUID()}, 'Vis D', ${T + '-vis-d'}, 'directory')`,
);
await expectViolation(
db().execute(
sql`INSERT INTO companies (id, name, slug, visibility) VALUES (${randomUUID()}, 'Vis X', ${T + '-vis-x'}, 'public')`,
),
/check constraint/i,
);
await expectViolation(
db().execute(sql`UPDATE companies SET visibility = 'hidden' WHERE id = ${visId}`),
/check constraint/i,
);
await expectViolation(
db().execute(sql`UPDATE companies SET visibility = NULL WHERE id = ${visId}`),
/null value|not-null/i,
);
});
// ── §6.1 NOT NULLs ─────────────────────────────────────────────────────────
@@ -391,7 +445,7 @@ function witnessSuite(getHandle: () => AnyDb): void {
);
await expectViolation(
db().execute(
sql`INSERT INTO hierarchy_grants (user_id, company_id, role, granted_by) VALUES (${userA}, ${companyId}, 'x', NULL)`,
sql`INSERT INTO hierarchy_grants (user_id, company_id, role, granted_by) VALUES (${userA}, ${companyId}, 'viewer', NULL)`,
),
/null value|not-null/i,
);
@@ -135,9 +135,9 @@
* production code is anomalous and review-visible; that blind spot is
* accepted as a residual, not closed.
*
* The writer allowlist names hierarchy command/repository modules ONLY. It is
* empty today: the hierarchy command family (M4-1b-ii) has not landed, so no
* production module may write the class tables. The infrastructure register
* The writer allowlist names hierarchy command/repository modules ONLY. Its
* single entry is the M4-1b-ii hierarchy command repository — the sole
* production module permitted to write the class tables. The infrastructure register
* holds legitimate non-hierarchy raw execution; registered modules are exempt
* from prong (iii) only — prongs (i) and (ii) apply to them with no
* exemption, and no registered module may appear on the writer allowlist.
@@ -181,13 +181,15 @@ const CLASS_TABLES = [
/**
* Writer allowlist (§6.3b): hierarchy command/repository modules only.
* EMPTY until the hierarchy command family lands (M4-1b-ii; M4-1b-i ships
* only the audit/outbox machinery, which writes no class table). Adding a module
* here is a contract-conformance decision reviewed under §5.1 — the module
* must be part of the Gateway hierarchy command path, and it must not export
* a function that executes caller-supplied SQL.
* Adding a module here is a contract-conformance decision reviewed under
* §5.1 — the module must be part of the Gateway hierarchy command path, and
* it must not export a function that executes caller-supplied SQL.
*/
const WRITER_ALLOWLIST: string[] = [];
const WRITER_ALLOWLIST: string[] = [
// The hierarchy command repository (M4-1b-ii): the sole class-table
// writer; every mutation is audited on its own transaction (§5.2).
'apps/gateway/src/hierarchy/hierarchy.repository.ts',
];
/**
* Infrastructure register: closed enumeration of legitimate non-hierarchy raw
+27 -9
View File
@@ -1063,13 +1063,24 @@ export const federationEnrollmentTokens = pgTable('federation_enrollment_tokens'
// command family only (§5.1), enforced by the writer-coverage assertion
// (§6.3b) — do not add writers outside that allowlist.
export const companies = pgTable('companies', {
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
slug: text('slug').notNull().unique(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
/** Company visibility classes (contract 1 §2.8, Ruling 4b): 'private' is the
* only creatable class (§5.5 — creation carries no visibility argument);
* 'directory' discloses existence/name/slug to all users and is entered only
* through the admin-gated visibility-change command. */
export const COMPANY_VISIBILITY = ['private', 'directory'] as const;
export const companies = pgTable(
'companies',
{
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
slug: text('slug').notNull().unique(),
visibility: text('visibility').notNull().default('private'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
() => [check('companies_visibility_check', sql`visibility IN ('private', 'directory')`)],
);
export const estates = pgTable(
'estates',
@@ -1110,6 +1121,10 @@ export const workspaces = pgTable(
(t) => [unique('workspaces_platform_project_slug_uniq').on(t.platformProjectId, t.slug)],
);
/** Grant role vocabulary (contract 2 §2): totally ordered, viewer ⊂ member ⊂
* owner. Order in this tuple IS the ordering — index = strength. */
export const HIERARCHY_GRANT_ROLES = ['viewer', 'member', 'owner'] as const;
export const hierarchyGrants = pgTable(
'hierarchy_grants',
{
@@ -1126,7 +1141,8 @@ export const hierarchyGrants = pgTable(
platformProjectId: uuid('platform_project_id').references(() => platformProjects.id, {
onDelete: 'cascade',
}),
// Role vocabulary and its CHECK constraint are contract 2 §2 (M4-2).
// Role vocabulary per contract 2 §2: exactly viewer ⊂ member ⊂ owner,
// totally ordered; CHECK below closes the column to that vocabulary.
role: text('role').notNull(),
grantedBy: text('granted_by')
.notNull()
@@ -1134,6 +1150,7 @@ export const hierarchyGrants = pgTable(
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
check('hierarchy_grants_role_check', sql`role IN ('viewer', 'member', 'owner')`),
check('hierarchy_grants_subject_check', sql`num_nonnulls(user_id, team_id) = 1`),
check(
'hierarchy_grants_target_check',
@@ -1170,6 +1187,7 @@ export const HIERARCHY_AUDIT_VERBS = [
'create',
'rename',
'transfer',
'visibility_change',
'delete',
'grant_create',
'grant_change',
@@ -1220,7 +1238,7 @@ export const hierarchyAuditEvents = pgTable(
index('hierarchy_audit_events_correlation_idx').on(t.correlationId),
check(
'hierarchy_audit_events_verb_check',
sql`verb IN ('create', 'rename', 'transfer', 'delete', 'grant_create', 'grant_change', 'grant_revoke')`,
sql`verb IN ('create', 'rename', 'transfer', 'visibility_change', 'delete', 'grant_create', 'grant_change', 'grant_revoke')`,
),
check(
'hierarchy_audit_events_target_kind_check',