@company-semantics/contracts 27.6.0 → 27.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@company-semantics/contracts",
3
- "version": "27.6.0",
3
+ "version": "27.8.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -128,12 +128,12 @@
128
128
  "@types/node": "^22.20.0",
129
129
  "husky": "^9.1.7",
130
130
  "lint-staged": "^17.0.8",
131
- "markdownlint-cli2": "^0.22.1",
131
+ "markdownlint-cli2": "^0.23.0",
132
132
  "openapi-typescript": "^7.13.0",
133
133
  "prettier": "^3.9.4",
134
- "tsx": "^4.22.4",
134
+ "tsx": "^4.23.0",
135
135
  "typescript": "^5.8.3",
136
- "vitest": "^4.1.9",
136
+ "vitest": "^4.1.10",
137
137
  "yaml": "^2.9.0"
138
138
  },
139
139
  "pnpm": {
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
2
- export const SPEC_HASH = '628fe99ff916' as const;
3
- export const SPEC_HASH_FULL = '628fe99ff9166b9eb6c0b2bab2794f4e730ceabff0785b7b54b8015897e9551d' as const;
2
+ export const SPEC_HASH = '1c5dcd23d461' as const;
3
+ export const SPEC_HASH_FULL = '1c5dcd23d46109024a008dea6440892f9a1ccd0b725fc6e0ed0bb3abe28bf78d' as const;
@@ -3962,6 +3962,12 @@ export interface components {
3962
3962
  /** Format: uuid */
3963
3963
  positionId: string;
3964
3964
  title: string;
3965
+ } | {
3966
+ /** @constant */
3967
+ kind: "reporting.setManagerBulk";
3968
+ reportPositionIds: string[];
3969
+ /** Format: uuid */
3970
+ managerPositionId: string;
3965
3971
  };
3966
3972
  MoveApplyRequest: {
3967
3973
  mutations: ({
@@ -62,6 +62,24 @@ describe("EMAIL_KINDS golden snapshot", () => {
62
62
  plainTextRequired: true,
63
63
  htmlSupported: true,
64
64
  },
65
+ "companyMd.access_requested": {
66
+ kind: "companyMd.access_requested",
67
+ subject: "Someone requested access to a document",
68
+ plainTextRequired: true,
69
+ htmlSupported: true,
70
+ },
71
+ "companyMd.access_request_approved": {
72
+ kind: "companyMd.access_request_approved",
73
+ subject: "Your access request was approved",
74
+ plainTextRequired: true,
75
+ htmlSupported: true,
76
+ },
77
+ "companyMd.access_request_denied": {
78
+ kind: "companyMd.access_request_denied",
79
+ subject: "Your access request was reviewed",
80
+ plainTextRequired: true,
81
+ htmlSupported: true,
82
+ },
65
83
  });
66
84
  });
67
85
  });
@@ -8,7 +8,36 @@
8
8
  * Each repo provides its own config values; shared guards consume them.
9
9
  */
10
10
 
11
- import type { CheckResult, Soc2ControlArea } from "./types";
11
+ import type { CheckResult } from "./types";
12
+ // The EvolutionBaselines interface below references these evolution baseline
13
+ // types, which now live in ./evolution-config.
14
+ import type {
15
+ CoverageBaseline,
16
+ FileClusterBaseline,
17
+ SubdirectoryAffinityBaseline,
18
+ } from "./evolution-config";
19
+
20
+ // SOC 2 guard configuration types live in a sibling module (extracted to keep
21
+ // this file within its size envelope). Re-exported here so the package public
22
+ // surface — consumed via the guards barrel — is unchanged.
23
+ export type {
24
+ SecretsDetectionConfig,
25
+ StructuredLoggingConfig,
26
+ AlertsConfigGuardConfig,
27
+ BackupConfigGuardConfig,
28
+ Soc2Baselines,
29
+ } from "./soc2-config";
30
+
31
+ // Evolution & drift-detection baseline config types live in a sibling module
32
+ // (extracted to keep this file within its size envelope). Re-exported here so
33
+ // the package public surface — consumed via the guards barrel — is unchanged.
34
+ export type {
35
+ DomainAggregateBaseline,
36
+ AggregateEvolutionConfig,
37
+ CoverageBaseline,
38
+ FileClusterBaseline,
39
+ SubdirectoryAffinityBaseline,
40
+ } from "./evolution-config";
12
41
 
13
42
  // =============================================================================
14
43
  // Size Limits
@@ -299,222 +328,6 @@ export interface EvolutionBaselines {
299
328
  subdirectoryAffinity?: SubdirectoryAffinityBaseline;
300
329
  }
301
330
 
302
- // =============================================================================
303
- // Aggregate Evolution Types
304
- // =============================================================================
305
-
306
- /**
307
- * Domain-level aggregate baseline for evolution tracking.
308
- * Replaces per-file baselines with domain-aggregated metrics.
309
- *
310
- * Design: Domains are extracted from file paths (first 2 segments).
311
- * Example: 'src/chat/execution/types.ts' → domain 'chat/execution'
312
- */
313
- export interface DomainAggregateBaseline {
314
- /** Total exports across all files in domain */
315
- exports: number;
316
- /** Total imports across all files in domain */
317
- imports: number;
318
- /** Diagnostic only, not thresholded */
319
- fileCount: number;
320
- /** ISO timestamp when baseline was captured */
321
- capturedAt?: string;
322
- }
323
-
324
- /**
325
- * Configuration for aggregate evolution guard.
326
- * Uses domain-level metrics instead of per-file tracking.
327
- *
328
- * Benefits over per-file:
329
- * - ~22 entries vs ~300 entries
330
- * - Stable under refactors within domains
331
- * - Grows O(domains) not O(files)
332
- */
333
- export interface AggregateEvolutionConfig {
334
- /** Source directory to scan */
335
- srcDir: string;
336
- /** Domain baselines keyed by domain path */
337
- domains: Record<string, DomainAggregateBaseline>;
338
- /** Global totals for sanity checking */
339
- totals?: {
340
- exports: number;
341
- imports: number;
342
- files: number;
343
- capturedAt?: string;
344
- };
345
- /** Warning thresholds (sensible defaults applied if omitted) */
346
- thresholds?: {
347
- /** Absolute export growth before warning (default: 10) */
348
- domainExportGrowth?: number;
349
- /** Percentage growth before warning (default: 0.25 = 25%) */
350
- domainGrowthPercent?: number;
351
- };
352
- }
353
-
354
- // =============================================================================
355
- // Coverage Baseline Types
356
- // =============================================================================
357
-
358
- /**
359
- * Coverage baseline configuration.
360
- * Used by CI orchestrator to inject coverage drift guard.
361
- */
362
- export interface CoverageBaseline {
363
- /** Path to coverage-summary.json (default: 'coverage/coverage-summary.json') */
364
- coverageFile?: string;
365
- /** Baseline coverage percentage (lines) */
366
- baseline: number;
367
- /** Allowed drop threshold before error (default: 0 = any drop errors) */
368
- dropThreshold?: number;
369
- }
370
-
371
- /**
372
- * File cluster detection configuration.
373
- * Detects clusters of related files that could benefit from
374
- * parent-child folder organization.
375
- *
376
- * @see ADR-CI-010
377
- */
378
- export interface FileClusterBaseline {
379
- /** Minimum files to form a cluster */
380
- minClusterSize?: number;
381
- /** Minimum stragglers to suggest reorganization */
382
- minStragglers?: number;
383
- /** Suffixes to strip when detecting clusters (e.g., '.test', '.spec') */
384
- stripSuffixes?: string[];
385
- /** Role words to strip (e.g., 'Service', 'Handler') */
386
- stripRoleWords?: string[];
387
- /** Directories to ignore */
388
- ignoredDirectories?: string[];
389
- }
390
-
391
- /**
392
- * Subdirectory affinity detection configuration.
393
- * Detects files in a parent directory that are only imported by
394
- * files within a single subdirectory, suggesting they should move.
395
- */
396
- export interface SubdirectoryAffinityBaseline {
397
- /** Minimum importers required */
398
- minImporters?: number;
399
- /** Maximum non-subdirectory imports allowed */
400
- maxNonSubdirImports?: number;
401
- /** Patterns to exclude from analysis */
402
- excludePatterns?: RegExp[];
403
- /** Importer patterns to ignore */
404
- ignoredImporterPatterns?: RegExp[];
405
- /** Directories to ignore */
406
- ignoredDirectories?: string[];
407
- }
408
-
409
- // =============================================================================
410
- // SOC 2 Guard Configuration Types
411
- // =============================================================================
412
-
413
- /**
414
- * Configuration for the secrets detection guard.
415
- * This is a blocking control for SOC 2 Access Control (AC).
416
- */
417
- export interface SecretsDetectionConfig {
418
- /** Source directory to scan (relative to repo root) */
419
- srcDir?: string;
420
- /** Additional directories to scan */
421
- additionalDirs?: string[];
422
- /** File patterns to exclude (globs) */
423
- excludePatterns?: string[];
424
- /** Additional secret patterns to detect (regex strings) */
425
- additionalPatterns?: string[];
426
- }
427
-
428
- /**
429
- * Configuration for the structured logging guard.
430
- * This is a non-blocking control for SOC 2 Logging & Monitoring (LM).
431
- */
432
- export interface StructuredLoggingConfig {
433
- /** Entry point files to check (relative to repo root) */
434
- entryPoints?: string[];
435
- /** Source directory to scan for logging (fallback if no entry points) */
436
- srcDir?: string;
437
- /** Recognized logging libraries */
438
- loggingLibraries?: string[];
439
- /** Logger instantiation patterns (regex strings) */
440
- instantiationPatterns?: string[];
441
- }
442
-
443
- /**
444
- * Configuration for the alerts config guard.
445
- * This is a non-blocking control for SOC 2 Logging & Monitoring (LM).
446
- */
447
- export interface AlertsConfigGuardConfig {
448
- /**
449
- * Files or directories to check for alerting configuration.
450
- * Supports both file paths and directory paths.
451
- */
452
- files?: string[];
453
- /**
454
- * Whether this check should be skipped.
455
- * Use for repos that don't have alerting (e.g., pure libraries).
456
- */
457
- skip?: boolean;
458
- /**
459
- * Evidence string when alerting is explicitly skipped.
460
- */
461
- skipEvidence?: string;
462
- }
463
-
464
- /**
465
- * Configuration for the backup config guard.
466
- * This is a non-blocking control for SOC 2 Backup & Recovery (BR).
467
- */
468
- export interface BackupConfigGuardConfig {
469
- /**
470
- * Files or directories to check for backup configuration.
471
- */
472
- files?: string[];
473
- /**
474
- * Whether this check should be skipped.
475
- * Use for repos that don't manage backups (e.g., frontend-only).
476
- */
477
- skip?: boolean;
478
- /**
479
- * Evidence string when backup check is explicitly skipped.
480
- */
481
- skipEvidence?: string;
482
- }
483
-
484
- /**
485
- * SOC 2 compliance baselines.
486
- * Configures SOC 2 control guards for a repository.
487
- *
488
- * Design: Product repos provide data only; CI orchestrator owns guard implementations.
489
- */
490
- export interface Soc2Baselines {
491
- /** Enable SOC 2 compliance reporting */
492
- enabled?: boolean;
493
- /** Advisory mode: all controls become non-blocking for visibility-only rollout */
494
- advisoryMode?: boolean;
495
- /** Repository name (for evidence) */
496
- repository?: string;
497
- /** Secrets detection configuration */
498
- secretsDetection?: SecretsDetectionConfig;
499
- /** Structured logging configuration */
500
- structuredLogging?: StructuredLoggingConfig;
501
- /** Alerts config configuration */
502
- alertsConfig?: AlertsConfigGuardConfig;
503
- /** Backup config configuration */
504
- backupConfig?: BackupConfigGuardConfig;
505
- /** Controls to explicitly skip (with evidence) */
506
- skipControls?: {
507
- area: Soc2ControlArea;
508
- evidence: string;
509
- }[];
510
- /**
511
- * Controls that remain blocking even in advisory mode.
512
- * Use to gradually promote controls from advisory to blocking.
513
- * Example: ['AC'] to make secrets-detection blocking first.
514
- */
515
- alwaysBlockingControls?: Soc2ControlArea[];
516
- }
517
-
518
331
  /**
519
332
  * Registry of checks grouped by tier.
520
333
  * Each repo exports this from guard-entries.ts for universal orchestration.
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Evolution & Drift-Detection Guard Configuration Vocabulary
3
+ *
4
+ * Polymorphic configuration types for the evolution/drift guards: domain-level
5
+ * aggregate tracking plus the coverage, file-cluster, and subdirectory-affinity
6
+ * baselines referenced by EvolutionBaselines.
7
+ *
8
+ * Extracted from ./config.ts as a cohesive sub-module (the evolution guard
9
+ * baseline family) to keep config.ts within its file-size envelope. This is a
10
+ * pure, behavior-preserving move: the identifiers are re-exported from
11
+ * ./config.ts so the package public surface is unchanged.
12
+ *
13
+ * Design principle: Configuration is data, not code.
14
+ *
15
+ * @architecture-role universal
16
+ */
17
+
18
+ // =============================================================================
19
+ // Aggregate Evolution Types
20
+ // =============================================================================
21
+
22
+ /**
23
+ * Domain-level aggregate baseline for evolution tracking.
24
+ * Replaces per-file baselines with domain-aggregated metrics.
25
+ *
26
+ * Design: Domains are extracted from file paths (first 2 segments).
27
+ * Example: 'src/chat/execution/types.ts' → domain 'chat/execution'
28
+ */
29
+ export interface DomainAggregateBaseline {
30
+ /** Total exports across all files in domain */
31
+ exports: number;
32
+ /** Total imports across all files in domain */
33
+ imports: number;
34
+ /** Diagnostic only, not thresholded */
35
+ fileCount: number;
36
+ /** ISO timestamp when baseline was captured */
37
+ capturedAt?: string;
38
+ }
39
+
40
+ /**
41
+ * Configuration for aggregate evolution guard.
42
+ * Uses domain-level metrics instead of per-file tracking.
43
+ *
44
+ * Benefits over per-file:
45
+ * - ~22 entries vs ~300 entries
46
+ * - Stable under refactors within domains
47
+ * - Grows O(domains) not O(files)
48
+ */
49
+ export interface AggregateEvolutionConfig {
50
+ /** Source directory to scan */
51
+ srcDir: string;
52
+ /** Domain baselines keyed by domain path */
53
+ domains: Record<string, DomainAggregateBaseline>;
54
+ /** Global totals for sanity checking */
55
+ totals?: {
56
+ exports: number;
57
+ imports: number;
58
+ files: number;
59
+ capturedAt?: string;
60
+ };
61
+ /** Warning thresholds (sensible defaults applied if omitted) */
62
+ thresholds?: {
63
+ /** Absolute export growth before warning (default: 10) */
64
+ domainExportGrowth?: number;
65
+ /** Percentage growth before warning (default: 0.25 = 25%) */
66
+ domainGrowthPercent?: number;
67
+ };
68
+ }
69
+
70
+ // =============================================================================
71
+ // Coverage Baseline Types
72
+ // =============================================================================
73
+
74
+ /**
75
+ * Coverage baseline configuration.
76
+ * Used by CI orchestrator to inject coverage drift guard.
77
+ */
78
+ export interface CoverageBaseline {
79
+ /** Path to coverage-summary.json (default: 'coverage/coverage-summary.json') */
80
+ coverageFile?: string;
81
+ /** Baseline coverage percentage (lines) */
82
+ baseline: number;
83
+ /** Allowed drop threshold before error (default: 0 = any drop errors) */
84
+ dropThreshold?: number;
85
+ }
86
+
87
+ /**
88
+ * File cluster detection configuration.
89
+ * Detects clusters of related files that could benefit from
90
+ * parent-child folder organization.
91
+ *
92
+ * @see ADR-CI-010
93
+ */
94
+ export interface FileClusterBaseline {
95
+ /** Minimum files to form a cluster */
96
+ minClusterSize?: number;
97
+ /** Minimum stragglers to suggest reorganization */
98
+ minStragglers?: number;
99
+ /** Suffixes to strip when detecting clusters (e.g., '.test', '.spec') */
100
+ stripSuffixes?: string[];
101
+ /** Role words to strip (e.g., 'Service', 'Handler') */
102
+ stripRoleWords?: string[];
103
+ /** Directories to ignore */
104
+ ignoredDirectories?: string[];
105
+ }
106
+
107
+ /**
108
+ * Subdirectory affinity detection configuration.
109
+ * Detects files in a parent directory that are only imported by
110
+ * files within a single subdirectory, suggesting they should move.
111
+ */
112
+ export interface SubdirectoryAffinityBaseline {
113
+ /** Minimum importers required */
114
+ minImporters?: number;
115
+ /** Maximum non-subdirectory imports allowed */
116
+ maxNonSubdirImports?: number;
117
+ /** Patterns to exclude from analysis */
118
+ excludePatterns?: RegExp[];
119
+ /** Importer patterns to ignore */
120
+ ignoredImporterPatterns?: RegExp[];
121
+ /** Directories to ignore */
122
+ ignoredDirectories?: string[];
123
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * SOC 2 Guard Configuration Vocabulary
3
+ *
4
+ * Polymorphic configuration types for the SOC 2 compliance guards.
5
+ * Each repo provides its own control config values; shared guards consume them.
6
+ *
7
+ * Extracted from ./config.ts as a cohesive sub-module (the SOC 2 control
8
+ * config family) to keep config.ts within its file-size envelope. This is a
9
+ * pure, behavior-preserving move: the identifiers are re-exported from
10
+ * ./config.ts so the package public surface is unchanged.
11
+ *
12
+ * Design principle: Configuration is data, not code.
13
+ *
14
+ * @architecture-role universal
15
+ */
16
+
17
+ import type { Soc2ControlArea } from "./types";
18
+
19
+ /**
20
+ * Configuration for the secrets detection guard.
21
+ * This is a blocking control for SOC 2 Access Control (AC).
22
+ */
23
+ export interface SecretsDetectionConfig {
24
+ /** Source directory to scan (relative to repo root) */
25
+ srcDir?: string;
26
+ /** Additional directories to scan */
27
+ additionalDirs?: string[];
28
+ /** File patterns to exclude (globs) */
29
+ excludePatterns?: string[];
30
+ /** Additional secret patterns to detect (regex strings) */
31
+ additionalPatterns?: string[];
32
+ }
33
+
34
+ /**
35
+ * Configuration for the structured logging guard.
36
+ * This is a non-blocking control for SOC 2 Logging & Monitoring (LM).
37
+ */
38
+ export interface StructuredLoggingConfig {
39
+ /** Entry point files to check (relative to repo root) */
40
+ entryPoints?: string[];
41
+ /** Source directory to scan for logging (fallback if no entry points) */
42
+ srcDir?: string;
43
+ /** Recognized logging libraries */
44
+ loggingLibraries?: string[];
45
+ /** Logger instantiation patterns (regex strings) */
46
+ instantiationPatterns?: string[];
47
+ }
48
+
49
+ /**
50
+ * Configuration for the alerts config guard.
51
+ * This is a non-blocking control for SOC 2 Logging & Monitoring (LM).
52
+ */
53
+ export interface AlertsConfigGuardConfig {
54
+ /**
55
+ * Files or directories to check for alerting configuration.
56
+ * Supports both file paths and directory paths.
57
+ */
58
+ files?: string[];
59
+ /**
60
+ * Whether this check should be skipped.
61
+ * Use for repos that don't have alerting (e.g., pure libraries).
62
+ */
63
+ skip?: boolean;
64
+ /**
65
+ * Evidence string when alerting is explicitly skipped.
66
+ */
67
+ skipEvidence?: string;
68
+ }
69
+
70
+ /**
71
+ * Configuration for the backup config guard.
72
+ * This is a non-blocking control for SOC 2 Backup & Recovery (BR).
73
+ */
74
+ export interface BackupConfigGuardConfig {
75
+ /**
76
+ * Files or directories to check for backup configuration.
77
+ */
78
+ files?: string[];
79
+ /**
80
+ * Whether this check should be skipped.
81
+ * Use for repos that don't manage backups (e.g., frontend-only).
82
+ */
83
+ skip?: boolean;
84
+ /**
85
+ * Evidence string when backup check is explicitly skipped.
86
+ */
87
+ skipEvidence?: string;
88
+ }
89
+
90
+ /**
91
+ * SOC 2 compliance baselines.
92
+ * Configures SOC 2 control guards for a repository.
93
+ *
94
+ * Design: Product repos provide data only; CI orchestrator owns guard implementations.
95
+ */
96
+ export interface Soc2Baselines {
97
+ /** Enable SOC 2 compliance reporting */
98
+ enabled?: boolean;
99
+ /** Advisory mode: all controls become non-blocking for visibility-only rollout */
100
+ advisoryMode?: boolean;
101
+ /** Repository name (for evidence) */
102
+ repository?: string;
103
+ /** Secrets detection configuration */
104
+ secretsDetection?: SecretsDetectionConfig;
105
+ /** Structured logging configuration */
106
+ structuredLogging?: StructuredLoggingConfig;
107
+ /** Alerts config configuration */
108
+ alertsConfig?: AlertsConfigGuardConfig;
109
+ /** Backup config configuration */
110
+ backupConfig?: BackupConfigGuardConfig;
111
+ /** Controls to explicitly skip (with evidence) */
112
+ skipControls?: {
113
+ area: Soc2ControlArea;
114
+ evidence: string;
115
+ }[];
116
+ /**
117
+ * Controls that remain blocking even in advisory mode.
118
+ * Use to gradually promote controls from advisory to blocking.
119
+ * Example: ['AC'] to make secrets-detection blocking first.
120
+ */
121
+ alwaysBlockingControls?: Soc2ControlArea[];
122
+ }
@@ -106,6 +106,55 @@ describe("OrgIntentSchema", () => {
106
106
  }).success,
107
107
  ).toBe(false);
108
108
  });
109
+
110
+ it("accepts a reporting.setManagerBulk intent (N seats -> one manager)", () => {
111
+ const intent = {
112
+ kind: "reporting.setManagerBulk" as const,
113
+ reportPositionIds: [REPORT, POSITION],
114
+ managerPositionId: MANAGER,
115
+ };
116
+ expect(OrgIntentSchema.parse(intent)).toEqual(intent);
117
+ });
118
+
119
+ it("rejects an empty bulk reportPositionIds array", () => {
120
+ expect(
121
+ OrgIntentSchema.safeParse({
122
+ kind: "reporting.setManagerBulk",
123
+ reportPositionIds: [],
124
+ managerPositionId: MANAGER,
125
+ }).success,
126
+ ).toBe(false);
127
+ });
128
+
129
+ it("rejects a bulk array beyond the 500-seat transport cap", () => {
130
+ expect(
131
+ OrgIntentSchema.safeParse({
132
+ kind: "reporting.setManagerBulk",
133
+ reportPositionIds: Array.from({ length: 501 }, () => REPORT),
134
+ managerPositionId: MANAGER,
135
+ }).success,
136
+ ).toBe(false);
137
+ });
138
+
139
+ it("rejects a non-uuid entry in the bulk report set", () => {
140
+ expect(
141
+ OrgIntentSchema.safeParse({
142
+ kind: "reporting.setManagerBulk",
143
+ reportPositionIds: [REPORT, "x"],
144
+ managerPositionId: MANAGER,
145
+ }).success,
146
+ ).toBe(false);
147
+ });
148
+
149
+ it("rejects a null bulk managerPositionId (intents always set a manager)", () => {
150
+ expect(
151
+ OrgIntentSchema.safeParse({
152
+ kind: "reporting.setManagerBulk",
153
+ reportPositionIds: [REPORT],
154
+ managerPositionId: null,
155
+ }).success,
156
+ ).toBe(false);
157
+ });
109
158
  });
110
159
 
111
160
  describe("FactMutationSchema", () => {
@@ -436,4 +485,23 @@ describe("TransformationPreviewSchema", () => {
436
485
  false,
437
486
  );
438
487
  });
488
+
489
+ it("accepts a multi-headline preview carrying primaryMutations (bulk intent)", () => {
490
+ const second = { ...setManagerMutation, reportPositionId: POSITION };
491
+ expect(
492
+ TransformationPreviewSchema.safeParse({
493
+ ...preview,
494
+ primaryMutations: [setManagerMutation, second],
495
+ }).success,
496
+ ).toBe(true);
497
+ });
498
+
499
+ it("rejects a single-element primaryMutations (single moves omit the field)", () => {
500
+ expect(
501
+ TransformationPreviewSchema.safeParse({
502
+ ...preview,
503
+ primaryMutations: [setManagerMutation],
504
+ }).success,
505
+ ).toBe(false);
506
+ });
439
507
  });
@@ -31,10 +31,20 @@ export interface AclEntry {
31
31
  * unit via the authority projection (ADR-BE-169). Distinct from
32
32
  * `unit_baseline` (which is membership-role-based) — delegations live on the
33
33
  * `org_unit_authority_grants` table and carry an explicit scope set.
34
+ *
35
+ * `visibility` covers the general-access band (`private` | `unit` | `org` on
36
+ * the entity's `visibility` column) — the "General access" tier in the share
37
+ * dialog. `unit` grants viewer to everyone home-in or matrixed-into the owning
38
+ * unit's subtree (members ∪ contributors, flowing down); at the org root `unit`
39
+ * normalizes to org-wide. `org` grants viewer to every org member. This is a
40
+ * first-class source so the deterministic evaluator, the in-memory open gate,
41
+ * and the materialized grant-compiler all resolve the band identically. See
42
+ * ADR-CONTRACTS-075 and backend ADR-BE-374.
34
43
  */
35
44
  export type AccessSource =
36
45
  | "org_rbac"
37
46
  | "sharing_policy"
47
+ | "visibility"
38
48
  | "unit_baseline"
39
49
  | "unit_delegation"
40
50
  | "acl_grant"
@@ -95,6 +95,25 @@ export const OrgIntentSchema = z.discriminatedUnion("kind", [
95
95
  /** The new human-facing title for the seat (non-empty, matching PositionSchema.title). */
96
96
  title: z.string().min(1),
97
97
  }),
98
+ // reporting.setManagerBulk — N reporting seats re-pointed at ONE manager as a
99
+ // single atomic intent (the org-chart cluster-pill drop). The engine expands
100
+ // it into N `reporting.setManager` FactMutations — the existing
101
+ // Intent-vs-Mutation asymmetry; no bulk mutation kind exists or is needed.
102
+ // Cross-field rules (manager ∉ reports, dedupe) are enforced at the route: a
103
+ // discriminatedUnion member cannot carry a ZodEffects refinement.
104
+ z.object({
105
+ kind: z.literal("reporting.setManagerBulk"),
106
+ /**
107
+ * The reporting seats being re-pointed together (e.g. a unit cluster's
108
+ * direct members; their subtrees follow implicitly). Order-insensitive;
109
+ * the engine treats the set as one atomic intent. The cap mirrors the
110
+ * bulk-reparent transport bound (ADR-BE-318).
111
+ */
112
+ reportPositionIds: z.array(z.string().uuid()).min(1).max(500),
113
+ /** The ONE manager seat every report re-points to. Non-null on an intent
114
+ * (un-setting is a mutation concern, exactly as on `reporting.setManager`). */
115
+ managerPositionId: z.string().uuid(),
116
+ }),
98
117
  ]);
99
118
  export type OrgIntent = z.infer<typeof OrgIntentSchema>;
100
119
 
@@ -336,6 +355,15 @@ export const TransformationPreviewSchema = z.object({
336
355
  intentLabel: z.string().min(1),
337
356
  /** The headline fact change the preview is centered on. */
338
357
  primary: FactMutationSchema,
358
+ /**
359
+ * Present only when the intent expands into MORE THAN ONE headline mutation
360
+ * (e.g. `reporting.setManagerBulk`): the full primary bundle, of which
361
+ * `primary` is the first element. Absent on single-mutation previews so
362
+ * existing consumers and goldens see byte-identical output. These are the
363
+ * INTENT's own edges — engine repair splices stay in the strategies'
364
+ * recommendations, never here.
365
+ */
366
+ primaryMutations: z.array(FactMutationSchema).min(2).optional(),
339
367
  /** The strategy the engine recommends by default. */
340
368
  defaultStrategy: TransformStrategySchema,
341
369
  /** Other viable strategies the operator may pick instead. */