Files
hackerspace.sh/lib/validations/governance.ts
T
Moheeb ZaraandClaude Opus 4.8 f775f954ad refactor: dead-code cleanup + drop the lib/types shim (slice 9)
- Delete orphaned components (theme-provider, task-claim-button), the unused
  roleDisplayName helper, the fully-dead cleanupRateLimits, the 6 never-imported
  validation schemas (signIn/signUp/createSpace/joinSpace — a replaced auth flow;
  proposalStatuses; updateAreaLeadRoleSchema).
- Migrate all @/lib/types importers to the canonical @/types/domain and delete
  the compatibility shim.

tsc 0, 635 tests green, lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-04 06:19:11 -07:00

113 lines
4.9 KiB
TypeScript

import { z } from 'zod'
import { flexibleDateTime } from './primitives'
// ─── Governance: proposals ───────────────────────────────────────────────────
export const proposalTypes = ['bylaw_change','board_action','membership_vote','advisory_poll','recall','budget'] as const
export const thresholdRules = ['simple_majority','two_thirds','three_fourths','unanimous'] as const
export const votePositions = ['yes','no','abstain','recused'] as const
export const createProposalSchema = z.object({
title: z.string().min(1, 'Title is required').max(200, 'Title is too long'),
body: z.string().max(20000).default(''),
proposal_type: z.enum(proposalTypes).default('advisory_poll'),
threshold: z.enum(thresholdRules).optional(),
policy_ref_id: z.string().uuid().optional().nullable(),
parent_incident_id: z.string().uuid().optional().nullable(),
voting_opens_at: flexibleDateTime().optional(),
voting_closes_at: flexibleDateTime().optional(),
open_immediately: z.boolean().optional(),
})
export const openProposalSchema = z.object({
proposalId: z.string().uuid('Invalid proposal ID'),
voting_closes_at: flexibleDateTime().optional(),
})
export const castVoteSchema = z.object({
proposalId: z.string().uuid('Invalid proposal ID'),
position: z.enum(votePositions),
recusal_reason: z.string().max(1000).optional().nullable(),
comment: z.string().max(2000).optional().nullable(),
}).refine(
v => v.position !== 'recused' || (v.recusal_reason && v.recusal_reason.trim().length > 0),
{ message: 'Recusal requires a reason', path: ['recusal_reason'] },
)
export const decideProposalSchema = z.object({
proposalId: z.string().uuid('Invalid proposal ID'),
})
export const withdrawProposalSchema = z.object({
proposalId: z.string().uuid('Invalid proposal ID'),
})
// ─── Governance: incidents ───────────────────────────────────────────────────
export const incidentSeverities = ['low','medium','high','critical'] as const
export const incidentStatuses = ['received','under_review','decided','appealed','closed'] as const
export const incidentUpdateVisibilities = ['reporter_only','all_parties','board_only'] as const
export const fileIncidentSchema = z.object({
title: z.string().min(1, 'Title is required').max(200),
body: z.string().min(1, 'Description is required').max(20000),
category: z.string().max(50).default('general'),
severity: z.enum(incidentSeverities).default('medium'),
subjects: z.array(z.string().uuid()).max(20).optional(),
is_anonymous: z.boolean().optional(),
})
export const updateIncidentStatusSchema = z.object({
incidentId: z.string().uuid('Invalid incident ID'),
status: z.enum(incidentStatuses),
disposition: z.string().max(20000).optional().nullable(),
})
export const addIncidentUpdateSchema = z.object({
incidentId: z.string().uuid('Invalid incident ID'),
body: z.string().min(1, 'Update body is required').max(20000),
visibility: z.enum(incidentUpdateVisibilities).default('all_parties'),
})
export const appealIncidentSchema = z.object({
incidentId: z.string().uuid('Invalid incident ID'),
title: z.string().min(1).max(200),
body: z.string().max(20000).default(''),
})
// Anonymous reporters look their case up with the opaque token they were
// given at filing time (48 hex chars today; bound loosely so the format can
// evolve without breaking old tokens).
export const trackIncidentSchema = z.object({
token: z.string().trim().min(16).max(128),
})
// ─── Governance: policies ────────────────────────────────────────────────────
export const policyStatuses = ['draft','active','deprecated','superseded'] as const
export const createPolicySchema = z.object({
slug: z.string().min(1, 'Slug is required').max(80).regex(/^[a-z0-9-]+$/, 'Slug must be lowercase letters, numbers, and hyphens only'),
title: z.string().min(1, 'Title is required').max(200),
section_ref: z.string().max(80).optional().nullable(),
parent_policy_id: z.string().uuid().optional().nullable(),
body_formal: z.string().max(100000).default(''),
body_plain: z.string().max(100000).optional().nullable(),
effective_at: flexibleDateTime().optional(),
})
export const supersedePolicySchema = z.object({
policyId: z.string().uuid('Invalid policy ID'),
body_formal: z.string().max(100000).default(''),
body_plain: z.string().max(100000).optional().nullable(),
section_ref: z.string().max(80).optional().nullable(),
title: z.string().min(1).max(200).optional(),
adopted_by_proposal_id: z.string().uuid().optional().nullable(),
effective_at: flexibleDateTime().optional(),
})
export const updatePolicyStatusSchema = z.object({
policyId: z.string().uuid('Invalid policy ID'),
status: z.enum(policyStatuses),
})