@company-semantics/contracts 0.142.0 → 0.143.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": "0.142.0",
3
+ "version": "0.143.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Damped auto-tuning controller vocabulary (PRD-00500 a1).
3
+ *
4
+ * Replaces PRD-00496 rule 9's reactive feedback loop with a damped controller
5
+ * primitive that avoids oscillation. Shared by client-concurrency (app),
6
+ * SSE-poll-rate (app), and early-shed-threshold (backend) controllers.
7
+ *
8
+ * Bounded<Min, Max> brands a numeric range at the type level — the only way
9
+ * to construct one is via `bounded(min, max, v)`, which throws at runtime if
10
+ * `v` is outside [min, max]. Consumers use the branded type in their public
11
+ * surface so a controller cannot emit an out-of-range value without a
12
+ * compile error at the call site.
13
+ *
14
+ * The `ci:autotune-bounds` guard (PRD-00500 a6) additionally enforces that
15
+ * each `ControllerConfig.min`/`max` is a numeric literal, not an identifier
16
+ * reference, preventing drift between the declared branded range and the
17
+ * runtime bounds check.
18
+ */
19
+
20
+ declare const BoundedBrand: unique symbol;
21
+
22
+ /**
23
+ * A numeric value proven at the type level to lie within [Min, Max].
24
+ *
25
+ * Constructed only via `bounded(min, max, v)`. Direct casts are intentionally
26
+ * impossible from outside this module because the brand symbol is not
27
+ * exported.
28
+ */
29
+ export type Bounded<Min extends number, Max extends number> = number & {
30
+ readonly [BoundedBrand]: [Min, Max];
31
+ };
32
+
33
+ /**
34
+ * Construct a Bounded<Min, Max> or throw RangeError at the boundary.
35
+ *
36
+ * Callers should pass `min` and `max` as literal types (`as const`) so the
37
+ * returned value's brand captures the compile-time range, not `number`.
38
+ */
39
+ export function bounded<Min extends number, Max extends number>(
40
+ min: Min,
41
+ max: Max,
42
+ v: number,
43
+ ): Bounded<Min, Max> {
44
+ if (v < min || v > max) {
45
+ throw new RangeError(`value ${v} out of bounds [${min}, ${max}]`);
46
+ }
47
+ return v as Bounded<Min, Max>;
48
+ }
49
+
50
+ /**
51
+ * Static configuration for a damped controller instance. `min`/`max` are
52
+ * literal-typed so the compile-time bounds match the runtime brand.
53
+ */
54
+ export type ControllerConfig<Min extends number, Max extends number> = {
55
+ readonly name: string;
56
+ readonly min: Min;
57
+ readonly max: Max;
58
+ readonly deadband: number;
59
+ readonly smoothingAlpha: number;
60
+ readonly changeCap: number;
61
+ readonly adjustmentWindowMs: number;
62
+ readonly setpoint: number;
63
+ };
64
+
65
+ /**
66
+ * Single controller reading: the raw measurement and the timestamp it was
67
+ * sampled at. Timestamps drive windowing and settling logic, so they must
68
+ * come from a single monotonic clock per controller instance.
69
+ */
70
+ export type ControllerInput = {
71
+ readonly measurement: number;
72
+ readonly timestamp: number;
73
+ };
74
+
75
+ /**
76
+ * Controller step result. `value` is branded with the configured range so
77
+ * downstream consumers (scheduler, SSE cadence, load-shed threshold) cannot
78
+ * accept an unbounded number by mistake.
79
+ *
80
+ * `reason` explains why the controller produced this value:
81
+ * - `deadband-hold`: input is inside the deadband, no adjustment needed
82
+ * - `window-hold`: last adjustment was within the adjustment window
83
+ * - `adjusted`: a normal damped adjustment occurred
84
+ * - `bounded-clamp`: the desired value was clamped to [min, max]
85
+ */
86
+ export type ControllerOutput<Min extends number, Max extends number> = {
87
+ readonly value: Bounded<Min, Max>;
88
+ readonly reason: 'deadband-hold' | 'window-hold' | 'adjusted' | 'bounded-clamp';
89
+ readonly adjustmentWindowId: string;
90
+ };
package/src/index.ts CHANGED
@@ -621,7 +621,12 @@ export { REQUEST_ID_HEADER } from './observability'
621
621
  export type { Labels, MetricEnvelope, SchedulerMetricName } from './observability'
622
622
 
623
623
  // Long-lived route vocabulary (PRD-00485 SSE / event-stream isolation)
624
- export type { LongLivedRouteOptions } from './sse'
624
+ export type { LongLivedRouteOptions, LongLivedPlugin } from './sse'
625
+ export { longLivedPlugin } from './sse'
626
+
627
+ // Route builder (PRD-00499 h5): required tier + resourceKey at the type level
628
+ export type { HttpMethod, RouteDefinition } from './route-builder'
629
+ export { routeBuilder } from './route-builder'
625
630
 
626
631
  // Query intent vocabulary (PRD-00486 per-tier DB query timeouts + circuit breaker)
627
632
  export type { QueryIntent } from './queryIntent'
@@ -635,5 +640,22 @@ export {
635
640
  } from './pressure'
636
641
  export type { SystemPressureValue, ShedResponse } from './pressure'
637
642
 
643
+ // Safe-mode vocabulary (PRD-00495 global freeze / circuit breaker)
644
+ export { SYSTEM_SAFE_MODE_HEADER } from './safe-mode'
645
+ export type { SafeModeValue, SafeModeState } from './safe-mode'
646
+
638
647
  // Timeout ladder (PRD-00491 end-to-end timing budgets)
639
648
  export { TIMEOUT_LADDER_MS, getTimeoutForTier } from './timeouts'
649
+
650
+ // Tracing vocabulary (PRD-00497 canonical request lifecycle)
651
+ export { LIFECYCLE_PHASES } from './tracing'
652
+ export type { LifecyclePhase, TraceId, RequestId, TraceMetadata } from './tracing'
653
+
654
+ // Damped auto-tuning controller vocabulary (PRD-00500 a1)
655
+ export { bounded } from './autotune'
656
+ export type {
657
+ Bounded,
658
+ ControllerConfig,
659
+ ControllerInput,
660
+ ControllerOutput,
661
+ } from './autotune'
@@ -53,6 +53,12 @@ export interface CompanyMdTreeNode extends CompanyMdNodeIdentity {
53
53
  readonly description?: string;
54
54
  /** Department IDs this node is associated with (for cross-team projections). */
55
55
  readonly departmentIds?: readonly string[];
56
+ /**
57
+ * Fractional-index key for stable ordering. Present on ordered entity
58
+ * wrappers (departments, teams, context docs); absent on wrappers for
59
+ * unordered types.
60
+ */
61
+ readonly orderKey?: string;
56
62
  }
57
63
 
58
64
  export interface CompanyMdDocCore extends CompanyMdNodeIdentity {
@@ -22,6 +22,7 @@ export interface Department {
22
22
  readonly slug: string;
23
23
  readonly description: string | null;
24
24
  readonly memberCount: number;
25
+ readonly orderKey: string;
25
26
  }
26
27
 
27
28
  /**
package/src/org/teams.ts CHANGED
@@ -31,6 +31,7 @@ export interface Team {
31
31
  readonly scope: import('./departments').TeamScope;
32
32
  readonly department: { readonly id: string; readonly name: string; readonly slug: string } | null;
33
33
  readonly homeDepartment: { readonly id: string; readonly name: string; readonly slug: string } | null;
34
+ readonly orderKey: string;
34
35
  }
35
36
 
36
37
  /**
@@ -0,0 +1,37 @@
1
+ import { z } from 'zod';
2
+ import type { Tier } from './requests';
3
+
4
+ declare const ResourceKeyBrand: unique symbol;
5
+ declare const SectionKeyBrand: unique symbol;
6
+ declare const DurationBrand: unique symbol;
7
+
8
+ export type ResourceKey = string & { readonly [ResourceKeyBrand]: true };
9
+ export type SectionKey = string & { readonly [SectionKeyBrand]: true };
10
+ export type Duration = number & { readonly [DurationBrand]: true };
11
+
12
+ const TIERS = ['P0', 'P1', 'P2', 'P3'] as const satisfies readonly Tier[];
13
+
14
+ export const ResourceEntrySchema = z
15
+ .object({
16
+ resource: z.string().transform((s) => s as ResourceKey),
17
+ priority: z.enum(TIERS),
18
+ hydrationPhase: z.enum(['critical', 'interactive', 'background']),
19
+ hydrationDepends: z.array(z.string().transform((s) => s as ResourceKey)),
20
+ mutationBehavior: z.enum(['collapse', 'queue', 'serial']),
21
+ staleTimeMs: z
22
+ .number()
23
+ .int()
24
+ .nonnegative()
25
+ .transform((n) => n as Duration),
26
+ degradationSection: z.string().transform((s) => s as SectionKey),
27
+ queryBudgetMs: z
28
+ .number()
29
+ .int()
30
+ .positive()
31
+ .transform((n) => n as Duration),
32
+ resourceFairnessCap: z.number().int().positive(),
33
+ })
34
+ .strict();
35
+
36
+ export type ResourceEntry = z.infer<typeof ResourceEntrySchema>;
37
+ export type ResourceRegistry = ReadonlyMap<ResourceKey, ResourceEntry>;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Route builder vocabulary (PRD-00499 h5).
3
+ *
4
+ * `RouteDefinition` requires `tier` and `resourceKey` at the type level —
5
+ * a route declaration that omits either field is a compile error, not a
6
+ * CI-scan warning. This replaces the PRD-00484 route-tier CI scan with a
7
+ * type-level constraint the compiler enforces.
8
+ *
9
+ * Fastify-agnostic on purpose (same as `sse.ts`): contracts does not
10
+ * depend on `fastify`. The `Handler` type parameter is bound at the
11
+ * backend call site to the appropriate FastifyRequest/FastifyReply
12
+ * signature.
13
+ */
14
+ import type { Tier } from './requests';
15
+ import type { ResourceKey } from './resource-keys';
16
+
17
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
18
+
19
+ export type RouteDefinition<Handler = unknown> = {
20
+ readonly tier: Tier;
21
+ readonly resourceKey: ResourceKey;
22
+ readonly method: HttpMethod;
23
+ readonly path: string;
24
+ readonly handler: Handler;
25
+ };
26
+
27
+ /**
28
+ * Identity wrapper that preserves the inferred RouteDefinition shape.
29
+ *
30
+ * The value is returned unchanged at runtime; the value exists purely to
31
+ * anchor type inference so that `tier` and `resourceKey` are required at
32
+ * every call site.
33
+ */
34
+ export function routeBuilder<Handler>(
35
+ def: RouteDefinition<Handler>,
36
+ ): RouteDefinition<Handler> {
37
+ return def;
38
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Safe-mode vocabulary (PRD-00495 global freeze / circuit breaker).
3
+ *
4
+ * Canonical contract shared between backend (source of truth) and the app
5
+ * (frontend consumer). The backend flips a single global flag on severe
6
+ * anomaly (invalidation storm, retry loop, heap critical, or manual admin
7
+ * action) and surfaces it through the `X-System-Safe-Mode` response header
8
+ * on every response. The app observes the header in its fetch layer and
9
+ * routes the signal to a single SafeModeProvider.
10
+ *
11
+ * Per feedback_env_var_bypasses_are_backdoors: the flag is never settable
12
+ * via environment variable — only via authenticated admin route.
13
+ *
14
+ * Per feedback_single_instrumentation_layer: the state is read through a
15
+ * single module (backend) and a single context (app). Do not duplicate.
16
+ */
17
+
18
+ /**
19
+ * Canonical response header signalling safe-mode state to clients.
20
+ * Frontend imports this to avoid stringly-typed header lookups.
21
+ */
22
+ export const SYSTEM_SAFE_MODE_HEADER = 'X-System-Safe-Mode';
23
+
24
+ /**
25
+ * Values carried on the `X-System-Safe-Mode` header.
26
+ * `on` indicates the backend has entered safe-mode and is rejecting all
27
+ * non-P0 traffic with 503. `off` is the normal quiescent state.
28
+ */
29
+ export type SafeModeValue = 'on' | 'off';
30
+
31
+ /**
32
+ * Observable safe-mode state. Shared verbatim between backend (source of
33
+ * truth, in-memory) and frontend (consumed via admin endpoint).
34
+ *
35
+ * - `active`: whether safe-mode is currently engaged
36
+ * - `reason`: human-readable reason for the current state, or null when
37
+ * inactive. Surfaced on the banner.
38
+ * - `since`: epoch ms when the current active period began, or null when
39
+ * inactive.
40
+ * - `autoTriggered`: true if the current active period was engaged by the
41
+ * error-rate threshold watcher; false if engaged by a human admin.
42
+ */
43
+ export type SafeModeState = {
44
+ active: boolean;
45
+ reason: string | null;
46
+ since: number | null;
47
+ autoTriggered: boolean;
48
+ };
package/src/sse.ts CHANGED
@@ -8,3 +8,31 @@
8
8
  * pool (PRD-00485) instead of the transactional semaphore.
9
9
  */
10
10
  export type LongLivedRouteOptions = { longLived: true };
11
+
12
+ /**
13
+ * LongLivedPlugin brand (PRD-00499 h4).
14
+ *
15
+ * A branded type that can only be produced by `longLivedPlugin(fn)`. The SSE
16
+ * plugin registration surface in company-semantics-backend accepts only
17
+ * `LongLivedPlugin<T>` — passing an unbranded Fastify plugin is a compile
18
+ * error. This replaces the scan-based @longLived CI check with a type-level
19
+ * constraint the compiler enforces.
20
+ *
21
+ * Fastify-agnostic on purpose: contracts does not depend on `fastify`. The
22
+ * `Plugin` type parameter is bound at the backend call site to the appropriate
23
+ * FastifyPluginAsync signature.
24
+ */
25
+ declare const LongLivedBrand: unique symbol;
26
+ export type LongLivedPlugin<Plugin> = Plugin & { readonly [LongLivedBrand]: true };
27
+
28
+ /**
29
+ * Brand a Fastify plugin as long-lived (SSE-capable).
30
+ *
31
+ * The backing symbol is compile-time only; runtime behavior is identity.
32
+ * A function that opts into long-lived registration must funnel through this
33
+ * helper — producing a `LongLivedPlugin<T>` directly requires access to the
34
+ * brand symbol, which is declared-only.
35
+ */
36
+ export function longLivedPlugin<Plugin>(fn: Plugin): LongLivedPlugin<Plugin> {
37
+ return fn as LongLivedPlugin<Plugin>;
38
+ }
package/src/tracing.ts ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Tracing vocabulary (PRD-00497 canonical request lifecycle).
3
+ *
4
+ * Structural types that make the runtime phase-boundary assertion possible.
5
+ * Every phase chokepoint in the app (see ADR-CTRL-064) accepts `TraceMetadata`
6
+ * whose `phase` field equals the expected phase for that chokepoint. The 10
7
+ * phases in `LIFECYCLE_PHASES` are the single source of truth — do not
8
+ * redefine this list elsewhere (per memory feedback_derive_dont_duplicate).
9
+ *
10
+ * `TraceId` and `RequestId` are branded strings: at runtime they are plain
11
+ * strings, but the type system prevents accidental swapping or construction
12
+ * from unbranded literals.
13
+ *
14
+ * @see decisions/ADR-CTRL-NNN-request-lifecycle.md in company-semantics-control
15
+ * @see src/lib/tracing/assert-phase-metadata.ts in company-semantics-app
16
+ */
17
+
18
+ /**
19
+ * Canonical request lifecycle phases, in order. Every request flows through
20
+ * these phases from intent capture to UI render. Each phase has exactly one
21
+ * chokepoint file in the app that asserts `TraceMetadata.phase` matches.
22
+ */
23
+ export type LifecyclePhase =
24
+ | 'intent'
25
+ | 'scheduler'
26
+ | 'mutation-layer'
27
+ | 'dedupe'
28
+ | 'concurrency-gate'
29
+ | 'retry'
30
+ | 'backend-backpressure'
31
+ | 'versioning'
32
+ | 'cache-apply'
33
+ | 'render'
34
+
35
+ declare const TraceIdBrand: unique symbol
36
+ declare const RequestIdBrand: unique symbol
37
+
38
+ /**
39
+ * Branded trace identifier. Stable across all phases of a single user-visible
40
+ * interaction (may span multiple requests on retry).
41
+ */
42
+ export type TraceId = string & { readonly [TraceIdBrand]: true }
43
+
44
+ /**
45
+ * Branded request identifier. Unique per outgoing request; does NOT rotate
46
+ * across retries of the same logical intent.
47
+ */
48
+ export type RequestId = string & { readonly [RequestIdBrand]: true }
49
+
50
+ /**
51
+ * Tracing metadata carried through every phase boundary. `phase` must match
52
+ * the expected phase for the chokepoint receiving this metadata; `parentPhase`
53
+ * (if present) must be an earlier phase in `LIFECYCLE_PHASES`.
54
+ */
55
+ export type TraceMetadata = {
56
+ readonly traceId: TraceId
57
+ readonly requestId: RequestId
58
+ readonly phase: LifecyclePhase
59
+ readonly parentPhase?: LifecyclePhase
60
+ }
61
+
62
+ /**
63
+ * Ordered list of the 10 canonical lifecycle phases. Consumers may derive a
64
+ * phase-index map from this array; do not maintain a parallel list.
65
+ */
66
+ export const LIFECYCLE_PHASES: readonly LifecyclePhase[] = [
67
+ 'intent',
68
+ 'scheduler',
69
+ 'mutation-layer',
70
+ 'dedupe',
71
+ 'concurrency-gate',
72
+ 'retry',
73
+ 'backend-backpressure',
74
+ 'versioning',
75
+ 'cache-apply',
76
+ 'render',
77
+ ]