This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
ACCOUNT_SIDES,
|
||||
AFFILIATION_KINDS,
|
||||
CUSTOMER_SEGMENTS,
|
||||
DEMAND_STAGES,
|
||||
INTERCONNECT_TYPES,
|
||||
PRODUCT_LINES,
|
||||
SUPPLIER_TYPES,
|
||||
SUPPLY_STAGES,
|
||||
} from './ontology';
|
||||
|
||||
export const IMPORT_ENTITIES = ['account', 'contact', 'demand_deal', 'supply_deal'] as const;
|
||||
export type ImportEntity = (typeof IMPORT_ENTITIES)[number];
|
||||
|
||||
export type ImportFieldKind =
|
||||
| 'text'
|
||||
| 'email'
|
||||
| 'url'
|
||||
| 'uuid'
|
||||
| 'integer'
|
||||
| 'decimal'
|
||||
| 'boolean'
|
||||
| 'date'
|
||||
| 'currency'
|
||||
| 'enum';
|
||||
|
||||
export interface ImportFieldDefinition {
|
||||
key: string;
|
||||
label: string;
|
||||
kind: ImportFieldKind;
|
||||
required?: boolean;
|
||||
options?: readonly string[];
|
||||
min?: number;
|
||||
max?: number;
|
||||
maxLength?: number;
|
||||
description?: string;
|
||||
/** False for non-null columns where a blank update must leave the stored value intact. */
|
||||
clearable?: boolean;
|
||||
}
|
||||
|
||||
export interface ImportEntityDefinition {
|
||||
label: string;
|
||||
description: string;
|
||||
fields: readonly ImportFieldDefinition[];
|
||||
}
|
||||
|
||||
export const IMPORT_ENTITY_DEFINITIONS: Readonly<Record<ImportEntity, ImportEntityDefinition>> = {
|
||||
account: {
|
||||
label: 'Accounts',
|
||||
description: 'Customer, supplier, and dual-sided organisations.',
|
||||
fields: [
|
||||
{ key: 'name', label: 'Account name', kind: 'text', required: true, maxLength: 200 },
|
||||
{ key: 'domain', label: 'Domain', kind: 'text', maxLength: 255 },
|
||||
{ key: 'website', label: 'Website', kind: 'url' },
|
||||
{ key: 'description', label: 'Description', kind: 'text', maxLength: 10_000 },
|
||||
{ key: 'side', label: 'Commercial side', kind: 'enum', required: true, options: ACCOUNT_SIDES },
|
||||
{ key: 'supplierType', label: 'Supplier type', kind: 'enum', options: SUPPLIER_TYPES },
|
||||
{ key: 'customerSegment', label: 'Customer segment', kind: 'enum', options: CUSTOMER_SEGMENTS },
|
||||
{ key: 'country', label: 'Country', kind: 'text', maxLength: 160 },
|
||||
{ key: 'region', label: 'Region', kind: 'text', maxLength: 160 },
|
||||
{ key: 'jurisdiction', label: 'Legal jurisdiction', kind: 'text', maxLength: 160 },
|
||||
{ key: 'ultimateParentName', label: 'Ultimate parent', kind: 'text', maxLength: 200 },
|
||||
{ key: 'ultimateParentCountry', label: 'Ultimate parent country', kind: 'text', maxLength: 160 },
|
||||
],
|
||||
},
|
||||
contact: {
|
||||
label: 'Contacts',
|
||||
description: 'People and their evidenced relationship to an account.',
|
||||
fields: [
|
||||
{ key: 'accountId', label: 'Account ID', kind: 'uuid', description: 'A PIG account UUID, not an account name.' },
|
||||
{ key: 'fullName', label: 'Full name', kind: 'text', required: true, maxLength: 200 },
|
||||
{ key: 'firstName', label: 'First name', kind: 'text', maxLength: 100 },
|
||||
{ key: 'lastName', label: 'Last name', kind: 'text', maxLength: 100 },
|
||||
{ key: 'title', label: 'Title', kind: 'text', maxLength: 200 },
|
||||
{ key: 'email', label: 'Email', kind: 'email', description: 'Import only sourced or provided addresses.' },
|
||||
{ key: 'phone', label: 'Phone', kind: 'text', maxLength: 100 },
|
||||
{ key: 'linkedinUrl', label: 'LinkedIn URL', kind: 'url' },
|
||||
{ key: 'twitterHandle', label: 'X / Twitter handle', kind: 'text', maxLength: 100 },
|
||||
{ key: 'githubHandle', label: 'GitHub handle', kind: 'text', maxLength: 100 },
|
||||
{ key: 'websiteUrl', label: 'Website URL', kind: 'url' },
|
||||
{ key: 'affiliation', label: 'Affiliation', kind: 'enum', options: AFFILIATION_KINDS, clearable: false },
|
||||
{ key: 'isDecisionMaker', label: 'Decision maker', kind: 'boolean', clearable: false },
|
||||
{ key: 'confidenceNote', label: 'Provenance note', kind: 'text', maxLength: 2_000 },
|
||||
],
|
||||
},
|
||||
demand_deal: {
|
||||
label: 'Demand deals',
|
||||
description: 'Customer opportunities. Account relationships must already exist in PIG.',
|
||||
fields: [
|
||||
{ key: 'accountId', label: 'Account ID', kind: 'uuid', required: true },
|
||||
{ key: 'name', label: 'Deal name', kind: 'text', required: true, maxLength: 200 },
|
||||
{ key: 'description', label: 'Description', kind: 'text', maxLength: 10_000 },
|
||||
{ key: 'productLine', label: 'Product line', kind: 'enum', required: true, options: PRODUCT_LINES },
|
||||
{ key: 'stage', label: 'Stage', kind: 'enum', required: true, options: DEMAND_STAGES },
|
||||
{ key: 'acvCents', label: 'ACV in cents', kind: 'integer', min: 0 },
|
||||
{ key: 'tcvCents', label: 'TCV in cents', kind: 'integer', min: 0 },
|
||||
{ key: 'currency', label: 'Currency', kind: 'currency', clearable: false },
|
||||
{ key: 'termMonths', label: 'Term months', kind: 'integer', min: 1 },
|
||||
{ key: 'probability', label: 'Probability (0–1)', kind: 'decimal', min: 0, max: 1 },
|
||||
{ key: 'expectedCloseDate', label: 'Expected close date', kind: 'date' },
|
||||
{ key: 'msaExecuted', label: 'MSA executed', kind: 'boolean', clearable: false },
|
||||
{ key: 'dpaExecuted', label: 'DPA executed', kind: 'boolean', clearable: false },
|
||||
{ key: 'closedReason', label: 'Closed reason', kind: 'text', maxLength: 2_000 },
|
||||
],
|
||||
},
|
||||
supply_deal: {
|
||||
label: 'Supply deals',
|
||||
description: 'Capacity sourcing opportunities attached to supplier accounts.',
|
||||
fields: [
|
||||
{ key: 'accountId', label: 'Account ID', kind: 'uuid', required: true },
|
||||
{ key: 'name', label: 'Deal name', kind: 'text', required: true, maxLength: 200 },
|
||||
{ key: 'stage', label: 'Stage', kind: 'enum', required: true, options: SUPPLY_STAGES },
|
||||
{ key: 'gpuType', label: 'GPU type', kind: 'text', maxLength: 100 },
|
||||
{ key: 'gpuCount', label: 'GPU count', kind: 'integer', min: 1 },
|
||||
{ key: 'interconnectType', label: 'Interconnect', kind: 'enum', options: INTERCONNECT_TYPES },
|
||||
{ key: 'targetCostPerGpuHourCents', label: 'Target cost in cents', kind: 'integer', min: 0 },
|
||||
{ key: 'termMonths', label: 'Term months', kind: 'integer', min: 1 },
|
||||
{ key: 'availableFrom', label: 'Available from', kind: 'date' },
|
||||
{ key: 'technicalVerdict', label: 'Technical verdict', kind: 'text', maxLength: 1_000 },
|
||||
{ key: 'technicalNotes', label: 'Technical notes', kind: 'text', maxLength: 10_000 },
|
||||
{ key: 'financialVerdict', label: 'Financial verdict', kind: 'text', maxLength: 1_000 },
|
||||
{ key: 'financialNotes', label: 'Financial notes', kind: 'text', maxLength: 10_000 },
|
||||
{ key: 'rejectionReason', label: 'Rejection reason', kind: 'text', maxLength: 2_000 },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './ontology';
|
||||
export * from './margin';
|
||||
export * from './permissions';
|
||||
export * from './theme';
|
||||
export * from './imports';
|
||||
|
||||
@@ -256,13 +256,50 @@ export type InterconnectType = (typeof INTERCONNECT_TYPES)[number];
|
||||
*
|
||||
* This is how compute aggregators express reliability when they cannot offer a
|
||||
* conventional uptime SLA — because they resell third-party infrastructure they
|
||||
* do not control. Secure-cloud capacity sits in vetted datacenters; community
|
||||
* capacity is cheaper and less dependable. See `SlaKind` below for how this
|
||||
* interacts with contractual commitments.
|
||||
* do not control. Government capacity is sovereign-grade and isolated,
|
||||
* secure-cloud capacity sits in vetted datacenters, and community capacity is
|
||||
* cheaper and less dependable. See `SlaKind` below for how this interacts with
|
||||
* contractual commitments.
|
||||
*/
|
||||
export const SECURITY_TIERS = ['secure_cloud', 'community_cloud'] as const;
|
||||
export const SECURITY_TIERS = ['government', 'secure_cloud', 'community_cloud'] as const;
|
||||
export type SecurityTier = (typeof SECURITY_TIERS)[number];
|
||||
|
||||
/** Allocation states distinguish forecast, sold usage, and returned capacity. */
|
||||
export const CONSUMING_ALLOCATION_STATUSES = ['committed', 'active', 'completed'] as const;
|
||||
export const RESERVING_ALLOCATION_STATUSES = [
|
||||
'planned',
|
||||
...CONSUMING_ALLOCATION_STATUSES,
|
||||
] as const;
|
||||
export const ALLOCATION_STATUSES = [
|
||||
...RESERVING_ALLOCATION_STATUSES,
|
||||
'released',
|
||||
] as const;
|
||||
export type AllocationStatus = (typeof ALLOCATION_STATUSES)[number];
|
||||
|
||||
/** Commercial priority is separate from lifecycle state. */
|
||||
export const GUARANTEE_TYPES = [
|
||||
'guaranteed',
|
||||
'committed',
|
||||
'on_demand',
|
||||
'preemptible',
|
||||
'internal',
|
||||
] as const;
|
||||
export type GuaranteeType = (typeof GUARANTEE_TYPES)[number];
|
||||
|
||||
/** Higher classifications may satisfy lower requirements, never the reverse. */
|
||||
export const SECURITY_TIER_RANK: Record<SecurityTier, number> = {
|
||||
community_cloud: 0,
|
||||
secure_cloud: 1,
|
||||
government: 2,
|
||||
};
|
||||
|
||||
export function securityTierSatisfies(
|
||||
available: SecurityTier,
|
||||
required: SecurityTier,
|
||||
): boolean {
|
||||
return SECURITY_TIER_RANK[available] >= SECURITY_TIER_RANK[required];
|
||||
}
|
||||
|
||||
/** Scarcity signal on a listing. Drives "sell this now" alerts. */
|
||||
export const STOCK_STATUSES = [
|
||||
'Available',
|
||||
@@ -347,6 +384,7 @@ export type FactBand = (typeof FACT_BANDS)[number];
|
||||
|
||||
export const FACT_STATUSES = [
|
||||
'applied', // Written to the record
|
||||
'approved', // Accepted by a person, but not written without a field-aware applicator
|
||||
'proposed', // Awaiting human review
|
||||
'dismissed', // Rejected by a human
|
||||
'superseded', // Replaced by a newer fact
|
||||
@@ -389,6 +427,10 @@ export const ACTIVITY_TYPES = [
|
||||
] as const;
|
||||
export type ActivityType = (typeof ACTIVITY_TYPES)[number];
|
||||
|
||||
/** Outbound events a linked collaboration channel may subscribe to. */
|
||||
export const NOTIFICATION_KINDS = ['stage_change', 'idle_capacity'] as const;
|
||||
export type NotificationKind = (typeof NOTIFICATION_KINDS)[number];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Agent task queue
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { Team, TeamRole } from './ontology';
|
||||
import { TEAMS } from './ontology';
|
||||
|
||||
export const API_KEY_SCOPES = ['read', 'write'] as const;
|
||||
export type ApiKeyScope = (typeof API_KEY_SCOPES)[number];
|
||||
|
||||
export const CAPABILITIES = [
|
||||
'deal:write',
|
||||
'commitment:write',
|
||||
'contract:sign',
|
||||
'data:import',
|
||||
'settings:admin',
|
||||
] as const;
|
||||
export type Capability = (typeof CAPABILITIES)[number];
|
||||
|
||||
export const TEAM_CAPABILITIES = [
|
||||
'deal:write',
|
||||
'commitment:write',
|
||||
'contract:sign',
|
||||
'data:import',
|
||||
] as const satisfies readonly Capability[];
|
||||
export type TeamCapability = (typeof TEAM_CAPABILITIES)[number];
|
||||
export type GlobalCapability = Exclude<Capability, TeamCapability>;
|
||||
|
||||
export interface PermissionSubject {
|
||||
isPlatformAdmin: boolean;
|
||||
teams: readonly { team: Team; role: TeamRole }[];
|
||||
}
|
||||
|
||||
export interface PermissionGrant {
|
||||
capability: Capability;
|
||||
/** Null is a platform-wide grant; ordinary role grants always name a team. */
|
||||
team: Team | null;
|
||||
}
|
||||
|
||||
interface TeamCapabilityRule {
|
||||
teams: readonly Team[];
|
||||
minimumRole: TeamRole;
|
||||
}
|
||||
|
||||
/**
|
||||
* The role policy is shared by API and browser code so controls cannot drift
|
||||
* from server enforcement as new write paths are added.
|
||||
*/
|
||||
export const TEAM_CAPABILITY_RULES: Readonly<Record<TeamCapability, TeamCapabilityRule>> = {
|
||||
'deal:write': { teams: ['supply', 'demand'], minimumRole: 'member' },
|
||||
'commitment:write': { teams: ['supply'], minimumRole: 'lead' },
|
||||
'contract:sign': { teams: ['supply', 'demand'], minimumRole: 'admin' },
|
||||
'data:import': { teams: TEAMS, minimumRole: 'admin' },
|
||||
};
|
||||
|
||||
const ROLE_RANK: Readonly<Record<TeamRole, number>> = {
|
||||
member: 0,
|
||||
lead: 1,
|
||||
admin: 2,
|
||||
};
|
||||
|
||||
export function resolvePermissionGrants(subject: PermissionSubject): PermissionGrant[] {
|
||||
if (subject.isPlatformAdmin) {
|
||||
return CAPABILITIES.map((capability) => ({ capability, team: null }));
|
||||
}
|
||||
|
||||
const grants: PermissionGrant[] = [];
|
||||
for (const capability of TEAM_CAPABILITIES) {
|
||||
const rule = TEAM_CAPABILITY_RULES[capability];
|
||||
for (const membership of subject.teams) {
|
||||
if (
|
||||
rule.teams.includes(membership.team) &&
|
||||
ROLE_RANK[membership.role] >= ROLE_RANK[rule.minimumRole]
|
||||
) {
|
||||
grants.push({ capability, team: membership.team });
|
||||
}
|
||||
}
|
||||
}
|
||||
return grants;
|
||||
}
|
||||
|
||||
export function permissionGranted(
|
||||
grants: readonly PermissionGrant[],
|
||||
capability: Capability,
|
||||
team?: Team,
|
||||
): boolean {
|
||||
return grants.some(
|
||||
(grant) =>
|
||||
grant.capability === capability &&
|
||||
(grant.team === null || team === undefined || grant.team === team),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { describe, it } from 'node:test';
|
||||
import { permissionGranted, resolvePermissionGrants } from '../src/permissions';
|
||||
|
||||
describe('role permissions', () => {
|
||||
it('keeps deal writes on the side where the person is a member', () => {
|
||||
const grants = resolvePermissionGrants({
|
||||
isPlatformAdmin: false,
|
||||
teams: [{ team: 'demand', role: 'member' }],
|
||||
});
|
||||
|
||||
assert.equal(permissionGranted(grants, 'deal:write', 'demand'), true);
|
||||
assert.equal(permissionGranted(grants, 'deal:write', 'supply'), false);
|
||||
assert.equal(permissionGranted(grants, 'commitment:write', 'demand'), false);
|
||||
});
|
||||
|
||||
it('allows supply leads to commit capacity without letting them sign contracts', () => {
|
||||
const grants = resolvePermissionGrants({
|
||||
isPlatformAdmin: false,
|
||||
teams: [{ team: 'supply', role: 'lead' }],
|
||||
});
|
||||
|
||||
assert.equal(permissionGranted(grants, 'commitment:write', 'supply'), true);
|
||||
assert.equal(permissionGranted(grants, 'contract:sign', 'supply'), false);
|
||||
});
|
||||
|
||||
it('keeps signing and bulk import at team-admin level', () => {
|
||||
const grants = resolvePermissionGrants({
|
||||
isPlatformAdmin: false,
|
||||
teams: [{ team: 'demand', role: 'admin' }],
|
||||
});
|
||||
|
||||
assert.equal(permissionGranted(grants, 'contract:sign', 'demand'), true);
|
||||
assert.equal(permissionGranted(grants, 'contract:sign', 'supply'), false);
|
||||
assert.equal(permissionGranted(grants, 'data:import', 'demand'), true);
|
||||
assert.equal(permissionGranted(grants, 'data:import', 'research'), false);
|
||||
assert.equal(permissionGranted(grants, 'settings:admin'), false);
|
||||
});
|
||||
|
||||
it('gives platform admins global grants without synthetic team memberships', () => {
|
||||
const grants = resolvePermissionGrants({ isPlatformAdmin: true, teams: [] });
|
||||
|
||||
assert.equal(permissionGranted(grants, 'deal:write', 'demand'), true);
|
||||
assert.equal(permissionGranted(grants, 'commitment:write', 'supply'), true);
|
||||
assert.equal(permissionGranted(grants, 'data:import', 'research'), true);
|
||||
assert.equal(permissionGranted(grants, 'settings:admin'), true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { SECURITY_TIERS, securityTierSatisfies } from '../src/index';
|
||||
|
||||
test('security tiers form a deliberate minimum-requirement ordering', () => {
|
||||
const expected: Record<(typeof SECURITY_TIERS)[number], (typeof SECURITY_TIERS)[number][]> = {
|
||||
community_cloud: ['community_cloud', 'secure_cloud', 'government'],
|
||||
secure_cloud: ['secure_cloud', 'government'],
|
||||
government: ['government'],
|
||||
};
|
||||
|
||||
for (const required of SECURITY_TIERS) {
|
||||
const eligible = SECURITY_TIERS.filter((available) =>
|
||||
securityTierSatisfies(available, required),
|
||||
);
|
||||
assert.deepEqual(eligible.sort(), expected[required].sort());
|
||||
}
|
||||
});
|
||||
|
||||
test('a sovereign requirement is never satisfied by community capacity', () => {
|
||||
assert.equal(securityTierSatisfies('community_cloud', 'government'), false);
|
||||
assert.equal(securityTierSatisfies('secure_cloud', 'government'), false);
|
||||
assert.equal(securityTierSatisfies('government', 'government'), true);
|
||||
});
|
||||
Reference in New Issue
Block a user