@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 +1 -1
- package/src/autotune.ts +90 -0
- package/src/index.ts +23 -1
- package/src/org/company-md.ts +6 -0
- package/src/org/departments.ts +1 -0
- package/src/org/teams.ts +1 -0
- package/src/resource-registry.ts +37 -0
- package/src/route-builder.ts +38 -0
- package/src/safe-mode.ts +48 -0
- package/src/sse.ts +28 -0
- package/src/tracing.ts +77 -0
package/package.json
CHANGED
package/src/autotune.ts
ADDED
|
@@ -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'
|
package/src/org/company-md.ts
CHANGED
|
@@ -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 {
|
package/src/org/departments.ts
CHANGED
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
|
+
}
|
package/src/safe-mode.ts
ADDED
|
@@ -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
|
+
]
|