@adhd/backlog 1.0.4 → 1.0.6
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/CHANGELOG.md +69 -0
- package/README.md +194 -39
- package/api.d.ts +87 -0
- package/api.ir.json +1 -1
- package/citation.d.ts +176 -0
- package/envelope.d.ts +36 -2
- package/index.d.ts +23 -3
- package/index.js +105 -57
- package/index.mjs +11450 -6758
- package/ir-artifact.d.ts +7 -3
- package/lifecycle.d.ts +49 -0
- package/package.json +5 -5
- package/query/canonical.d.ts +16 -0
- package/query/card.d.ts +116 -4
- package/query/get.d.ts +8 -1
- package/query/index.d.ts +1 -0
- package/query/query.d.ts +32 -22
- package/query/redirect.d.ts +30 -0
- package/query/resolve.d.ts +75 -0
- package/query/similar-clusters.d.ts +8 -0
- package/query/similarity-signals.d.ts +47 -0
- package/query/spec-staleness.d.ts +21 -0
- package/query/types.d.ts +249 -34
- package/query/verdict-core.d.ts +21 -0
- package/query/verdict.d.ts +22 -0
- package/query/views/catalog.d.ts +47 -0
- package/query/views/registry.d.ts +27 -7
- package/query/views/report.d.ts +49 -0
- package/query/views/semantic.d.ts +28 -5
- package/query/views/stats.d.ts +38 -2
- package/readiness.d.ts +29 -0
- package/retry-policy.d.ts +44 -0
- package/serve.d.ts +1 -1
- package/server.d.ts +71 -3
- package/service-config.d.ts +109 -0
- package/service-errors.d.ts +51 -0
- package/skill/SKILL.md +688 -81
- package/store/catalog-invariant-guard.d.ts +13 -7
- package/vocabulary.d.ts +48 -0
- package/write/anchor-check.d.ts +139 -0
- package/write/attestation.d.ts +64 -0
- package/write/catalog-merge.d.ts +15 -8
- package/write/catalog.d.ts +72 -2
- package/write/citation-path.d.ts +31 -0
- package/write/citation.d.ts +157 -0
- package/write/create-issue.d.ts +143 -29
- package/write/errors.d.ts +196 -1
- package/write/gate.d.ts +67 -0
- package/write/merge-project.d.ts +54 -0
- package/write/obligation.d.ts +133 -0
- package/write/relate.d.ts +1 -1
- package/write/revision.d.ts +27 -0
- package/write/similarity-scan.d.ts +84 -0
- package/write/spec-revision.d.ts +131 -0
- package/write/spec-revision.reconcile.d.ts +48 -0
- package/write/transition.d.ts +13 -2
- package/write/tx.d.ts +44 -2
- package/write/uid-prefix.d.ts +78 -0
- package/write/update.d.ts +23 -1
package/query/views/stats.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { IQueryStoreHandle } from '../query.js';
|
|
2
2
|
import { IIssueFilter } from '../types.js';
|
|
3
|
+
import { GraphBackend, NodeRecord } from '@adhd/sox-graph-store';
|
|
3
4
|
|
|
4
5
|
export interface IPriorityMatrixRow {
|
|
5
6
|
priority: string;
|
|
@@ -20,11 +21,39 @@ export interface IPriorityMatrixInput {
|
|
|
20
21
|
/** `project`/`component`/`kind`/`status` only (§6.5 rule 3's edge-scoped dimensions, restricted to what a priority breakdown composes with) — any other key throws. */
|
|
21
22
|
filter?: IIssueFilter;
|
|
22
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* An `IIssueFilter`'s placement/kind/status dimensions resolved to the
|
|
26
|
+
* concrete set of LIVE, non-superseded issue nodes they match — the exact
|
|
27
|
+
* scoping `priorityMatrix` counts over. See
|
|
28
|
+
* {@link resolveScopedLiveIssueNodes}.
|
|
29
|
+
*/
|
|
30
|
+
export interface IScopedLiveIssues {
|
|
31
|
+
nodes: NodeRecord[];
|
|
32
|
+
statusScope: NonNullable<IIssueFilter['status']>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolve an `IIssueFilter`'s placement/kind/status dimensions to the concrete
|
|
36
|
+
* set of LIVE, non-superseded issue NODES they match — the exact scoping
|
|
37
|
+
* `priorityMatrix` (and, via it, `report`) counts over. Exported so `report`
|
|
38
|
+
* composes this ONE implementation rather than re-deriving it (ADR-0002).
|
|
39
|
+
*
|
|
40
|
+
* `nodes: []` (never `undefined`) when a placement dimension resolved to an
|
|
41
|
+
* empty set or the status scope matched nothing, so callers branch on one
|
|
42
|
+
* shape. `statusScope` always names the scope applied — `'open'` when the
|
|
43
|
+
* filter omitted `status`, mirroring `priorityMatrix`'s BUG-023 default.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveScopedLiveIssueNodes(graph: GraphBackend, filter: IIssueFilter | undefined): Promise<IScopedLiveIssues>;
|
|
23
46
|
/** SPEC.md §5's status-aware priority matrix (BUG-023). */
|
|
24
47
|
export declare function priorityMatrix(handle: IQueryStoreHandle, input?: IPriorityMatrixInput): Promise<IPriorityMatrixResult>;
|
|
25
48
|
export interface IPartOfRollupInput {
|
|
26
49
|
/** The root issue's `uid` (§6.1). Throws `IssueNotFoundError` if it does not resolve to a live issue. */
|
|
27
50
|
uid: string;
|
|
51
|
+
/** Count-only mode: return the counts, omit the `childrenOpenUids` uid list entirely. */
|
|
52
|
+
countOnly?: boolean;
|
|
53
|
+
/** Max uids in `childrenOpenUids` when not count-only (default 50, max 1000). */
|
|
54
|
+
limit?: number;
|
|
55
|
+
/** Opaque cursor for the paged uid list — pass the previous page's `nextCursor`. */
|
|
56
|
+
after?: string;
|
|
28
57
|
}
|
|
29
58
|
export interface IPartOfRollupResult {
|
|
30
59
|
uid: string;
|
|
@@ -32,9 +61,16 @@ export interface IPartOfRollupResult {
|
|
|
32
61
|
childrenTotal: number;
|
|
33
62
|
childrenOpen: number;
|
|
34
63
|
childrenClosed: number;
|
|
35
|
-
/** `uid`s of the still-open descendants — the actionable half, mirroring the `IIssueRef`-shaped convention `card.ts`'s `blockers`/`related` already use. */
|
|
36
|
-
childrenOpenUids
|
|
64
|
+
/** `uid`s of the still-open descendants — the actionable half, mirroring the `IIssueRef`-shaped convention `card.ts`'s `blockers`/`related` already use. Absent in count-only mode, or present and bounded by `limit`. */
|
|
65
|
+
childrenOpenUids?: readonly string[];
|
|
66
|
+
/** Pass back as `after` for the next page; absent ⇒ no further open-descendant uids. */
|
|
67
|
+
nextCursor?: string;
|
|
68
|
+
/** True iff more open-descendant uids exist beyond this page. */
|
|
69
|
+
hasMore?: boolean;
|
|
37
70
|
}
|
|
71
|
+
/** Default/maximum uid-list page size for {@link partOfRollup}'s non-count-only mode. */
|
|
72
|
+
export declare const DEFAULT_PART_OF_ROLLUP_LIMIT = 50;
|
|
73
|
+
export declare const MAX_PART_OF_ROLLUP_LIMIT = 1000;
|
|
38
74
|
/**
|
|
39
75
|
* SPEC.md §5's `part_of` + derived two-axis rollup (FEAT-005), realized as
|
|
40
76
|
* ONE axis here — children-open/closed over the FULL transitive subtree
|
package/readiness.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { IServiceFailure } from './lifecycle.js';
|
|
2
|
+
|
|
3
|
+
export interface IReadinessTimer {
|
|
4
|
+
setTimeout(fn: () => void, ms: number): unknown;
|
|
5
|
+
clearTimeout(handle: unknown): void;
|
|
6
|
+
}
|
|
7
|
+
/** The minimal composed-server surface the probe needs. */
|
|
8
|
+
export interface IReadinessHandle {
|
|
9
|
+
pkg: {
|
|
10
|
+
fns: Record<string, (...args: unknown[]) => unknown>;
|
|
11
|
+
createClient: () => Promise<unknown>;
|
|
12
|
+
};
|
|
13
|
+
operations: readonly {
|
|
14
|
+
id: string;
|
|
15
|
+
}[];
|
|
16
|
+
store: unknown;
|
|
17
|
+
}
|
|
18
|
+
export interface IReadinessProbeOpts {
|
|
19
|
+
timeoutMs: number;
|
|
20
|
+
/** Injectable one-shot timer so a test uses a virtual clock, never sleep. */
|
|
21
|
+
timer?: IReadinessTimer;
|
|
22
|
+
/** Test seam: the exact serving-path call to drive. */
|
|
23
|
+
invoke?: () => Promise<unknown>;
|
|
24
|
+
}
|
|
25
|
+
export interface IReadinessResult {
|
|
26
|
+
ready: boolean;
|
|
27
|
+
failure?: IServiceFailure;
|
|
28
|
+
}
|
|
29
|
+
export declare function probeReadiness(handle: IReadinessHandle, opts: IReadinessProbeOpts): Promise<IReadinessResult>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface IResiliencePolicy {
|
|
2
|
+
maxAttempts: number;
|
|
3
|
+
baseMs: number;
|
|
4
|
+
maxMs: number;
|
|
5
|
+
budgetMs: number;
|
|
6
|
+
breakerFailureThreshold: number;
|
|
7
|
+
breakerResetMs: number;
|
|
8
|
+
}
|
|
9
|
+
/** Full jitter: `rand() * min(maxMs, baseMs * 2**attempt)`. */
|
|
10
|
+
export declare function fullJitterDelay(attempt: number, p: Pick<IResiliencePolicy, 'baseMs' | 'maxMs'>, rand: () => number): number;
|
|
11
|
+
export type IBreakerState = 'closed' | 'open' | 'half-open';
|
|
12
|
+
export declare class CircuitBreaker {
|
|
13
|
+
private failures;
|
|
14
|
+
private openedAt;
|
|
15
|
+
private probing;
|
|
16
|
+
private readonly failureThreshold;
|
|
17
|
+
private readonly resetMs;
|
|
18
|
+
private readonly now;
|
|
19
|
+
constructor(p: {
|
|
20
|
+
failureThreshold: number;
|
|
21
|
+
resetMs: number;
|
|
22
|
+
now?: () => number;
|
|
23
|
+
});
|
|
24
|
+
state(): IBreakerState;
|
|
25
|
+
/** open → refuse (no attempt). closed/half-open → allow one. */
|
|
26
|
+
tryPass(): boolean;
|
|
27
|
+
/** → closed. */
|
|
28
|
+
onSuccess(): void;
|
|
29
|
+
/** → open after threshold (or immediately on a failed half-open probe). */
|
|
30
|
+
onFailure(): void;
|
|
31
|
+
}
|
|
32
|
+
export interface IResilienceDeps {
|
|
33
|
+
graceMs: number;
|
|
34
|
+
rand: () => number;
|
|
35
|
+
now: () => number;
|
|
36
|
+
sleep: (ms: number) => Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Wraps the connect/handshake of ONE call. Applies `graceMs` to the FIRST
|
|
40
|
+
* attempt's deadline (the cold-start window) BEFORE any retry, then retries
|
|
41
|
+
* with full jitter inside a total `budgetMs`; refuses immediately when the
|
|
42
|
+
* breaker is open. On budget exhaustion / open breaker → {@link ServiceNotReadyError}.
|
|
43
|
+
*/
|
|
44
|
+
export declare function withResilience<T>(fn: (attempt: number, deadlineMs: number) => Promise<T>, p: IResiliencePolicy, breaker: CircuitBreaker, deps: IResilienceDeps): Promise<T>;
|
package/serve.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface RunServeCommandOpts {
|
|
|
23
23
|
* (never a bare `Error`) so `runServeCommand` can catch it and hand it to
|
|
24
24
|
* `failUsage` instead of letting it reach the bin guard's stack-trace path.
|
|
25
25
|
*/
|
|
26
|
-
export declare const SERVE_HELP_TEXT = "backlog serve [--transport mcp|http|both] [--port N] [--host H]\n\nStarts the long-lived backlog server (MCP and/or HTTP), matching one of the\nprocess's own configured transports to the way an agent host or a script\nexpects to reach it.\n\n --transport <name> mcp | http | both (default: mcp)\n --port <N> HTTP listen port (default: 3300; ignored for mcp-only)\n --host <name> HTTP listen host (default: 127.0.0.1; ignored for mcp-only)\n\nExamples:\n backlog serve\n backlog serve --transport http --port 3300\n backlog serve --transport both --host 0.0.0.0\n";
|
|
26
|
+
export declare const SERVE_HELP_TEXT = "backlog serve [--transport mcp|http|both] [--port N] [--host H]\n\nStarts the long-lived backlog server (MCP and/or HTTP), matching one of the\nprocess's own configured transports to the way an agent host or a script\nexpects to reach it.\n\n --transport <name> mcp | http | both (default: from ADHD_BACKLOG_SERVICE_TRANSPORT\n / the service.* config, else mcp)\n --port <N> HTTP listen port (default: from ADHD_BACKLOG_SERVICE_PORT\n / the service.* config, else 3300; ignored for mcp-only)\n --host <name> HTTP listen host (default: from ADHD_BACKLOG_SERVICE_HOST\n / the service.* config, else 127.0.0.1; ignored for mcp-only)\n --ready-file <path> Write the service report to <path> once the server is\n READY (the serving path answered), atomically, and\n again on every state change.\n --probe Run the serving-path readiness probe once, print the\n report as JSON, and exit 0 (ready) / 1 (not ready).\n\nExamples:\n backlog serve\n backlog serve --transport http --port 3300\n backlog serve --transport both --host 0.0.0.0\n backlog serve --transport mcp --ready-file /run/backlog/ready.json\n backlog serve --probe\n";
|
|
27
27
|
/** Runs until the process receives SIGTERM/SIGINT (the normal way a host
|
|
28
28
|
* process manager — or `.mcp.json`'s own stdio transport lifecycle — stops
|
|
29
29
|
* a long-lived MCP/HTTP server), then resolves cleanly. */
|
package/server.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { IServiceReport } from './lifecycle.js';
|
|
2
|
+
import { BACKLOG_VERBS } from './vocabulary.js';
|
|
1
3
|
import { BacklogCtx } from './api.js';
|
|
2
4
|
import { HttpVerb } from '@adhd/apigen-engine-naming';
|
|
3
5
|
import { composeSchemas, Operation, Plugin, Logger, OutputPlugin, RunInput } from '@adhd/apigen-core-client';
|
|
@@ -83,7 +85,7 @@ export declare const BACKLOG_HOST_COMMANDS: readonly string[];
|
|
|
83
85
|
*
|
|
84
86
|
* Order is the SPEC.md §4/§6 declaration order, not alphabetical; compare as sets.
|
|
85
87
|
*/
|
|
86
|
-
export
|
|
88
|
+
export { BACKLOG_VERBS };
|
|
87
89
|
/**
|
|
88
90
|
* One mounted operation, projected to all four transports backlog serves.
|
|
89
91
|
*
|
|
@@ -149,8 +151,43 @@ export declare function describeMountedSurface(operations: readonly Operation[])
|
|
|
149
151
|
* shipped surface automatically.
|
|
150
152
|
*/
|
|
151
153
|
export declare function resolveExpectedMcpToolNames(operations: readonly Operation[], usePlugins?: readonly Plugin[]): string[];
|
|
154
|
+
/**
|
|
155
|
+
* The full advertised surface, one entry per pinned data verb, each projecting
|
|
156
|
+
* to the identical `id`/`mcpTool`/`cliCommand` a real mounted operation has.
|
|
157
|
+
*
|
|
158
|
+
* Derived from {@link BACKLOG_VERBS} (the single pinned list) so it can never
|
|
159
|
+
* drift from what `server.verbs.spec.ts` checks; {@link assertSurfaceIsReal}
|
|
160
|
+
* proves each entry is actually present in the built package.
|
|
161
|
+
*/
|
|
162
|
+
export declare function describeBacklogSurface(): IMountedOperationSurface[];
|
|
163
|
+
/**
|
|
164
|
+
* Assert that every advertised verb is a REAL mounted operation in the built
|
|
165
|
+
* package — the AC3 mechanism, and the fix for the `enrich` class of defect
|
|
166
|
+
* (an agent spec that advertises a verb this tool does not implement).
|
|
167
|
+
*
|
|
168
|
+
* Reads the baked operation descriptors (`dist/api.ir.json`, the SAME artifact
|
|
169
|
+
* every real mount composes from) and compares its projected surface against
|
|
170
|
+
* {@link describeBacklogSurface} **both ways**: a verb advertised but absent
|
|
171
|
+
* from the build throws (naming it), and an operation mounted but absent from
|
|
172
|
+
* the pinned list also throws (a split brain). The artifact reader is
|
|
173
|
+
* synchronous and content-hash validated; a missing/stale artifact is a hard
|
|
174
|
+
* failure, never a silent pass (an unverifiable surface is not a real one).
|
|
175
|
+
*
|
|
176
|
+
* Library-only / NEVER mounted (`index.ts` re-exports it): it opens no store.
|
|
177
|
+
*/
|
|
178
|
+
export declare function assertSurfaceIsReal(): void;
|
|
152
179
|
export interface StartOpts {
|
|
153
|
-
|
|
180
|
+
/**
|
|
181
|
+
* OPTIONAL on purpose (D-A apply-fix): an omitted transport must NOT
|
|
182
|
+
* pre-empt the resolved `service.*` cascade. `resolveServiceConfig`'s
|
|
183
|
+
* precedence is explicit opts > `ADHD_BACKLOG_SERVICE_TRANSPORT` > layer
|
|
184
|
+
* files > the `'mcp'` code default, and it only consults `opts.transport`
|
|
185
|
+
* when it is actually present — so `serve.ts` must pass `undefined` (not a
|
|
186
|
+
* materialised `'mcp'`) when the operator did not type `--transport`.
|
|
187
|
+
* `createBacklogServer` mounts from the RESOLVED `serviceConfig.transport`,
|
|
188
|
+
* never from this raw field.
|
|
189
|
+
*/
|
|
190
|
+
transport?: 'http' | 'mcp' | 'both';
|
|
154
191
|
port?: number;
|
|
155
192
|
host?: string;
|
|
156
193
|
scope?: Scope;
|
|
@@ -205,9 +242,40 @@ export declare function buildBacklogApigenPackage(ctx: BacklogCtx | (() => Backl
|
|
|
205
242
|
surface: IMountedOperationSurface[];
|
|
206
243
|
operations: Operation[];
|
|
207
244
|
}>;
|
|
245
|
+
/**
|
|
246
|
+
* The long-lived server handle (D-A, Segment D). Additive: existing callers
|
|
247
|
+
* keep using {@link startBacklogServer} (unchanged `Promise<void>` signature).
|
|
248
|
+
*/
|
|
249
|
+
export interface IBacklogServerHandle {
|
|
250
|
+
report(): IServiceReport;
|
|
251
|
+
whenReady(): Promise<void>;
|
|
252
|
+
/** Resolves when the server has fully closed (store released). Additive. */
|
|
253
|
+
whenClosed(): Promise<void>;
|
|
254
|
+
/** Abort the transports and release the store. Idempotent. */
|
|
255
|
+
close(): Promise<void>;
|
|
256
|
+
}
|
|
208
257
|
/**
|
|
209
258
|
* Opens (or reuses) the backlog store + env, mounts every `client.ts` export
|
|
210
259
|
* live via `@adhd/apigen-plugin-api-fastify` and/or `@adhd/apigen-plugin-mcp`
|
|
211
|
-
* — no code generation.
|
|
260
|
+
* — no code generation — and returns a lifecycle handle.
|
|
261
|
+
*
|
|
262
|
+
* D-A additions: resolves + validates `service.*` config, runs the load-time
|
|
263
|
+
* artifact drift check, and drives the `starting → live → ready` lifecycle
|
|
264
|
+
* with a serving-path readiness probe. `startBacklogServer` (below) is the
|
|
265
|
+
* unchanged-signature wrapper over this function.
|
|
266
|
+
*/
|
|
267
|
+
export declare function createBacklogServer(opts: StartOpts & {
|
|
268
|
+
onStateChange?: (r: IServiceReport) => void;
|
|
269
|
+
/**
|
|
270
|
+
* `--probe` mode: compose the serving path but do NOT mount a network
|
|
271
|
+
* transport. The readiness probe drives the SAME composed invoker the
|
|
272
|
+
* tools use, so a live socket is not required — and skipping the mount
|
|
273
|
+
* avoids a one-shot probe process being kept alive by a listening socket.
|
|
274
|
+
*/
|
|
275
|
+
probeOnly?: boolean;
|
|
276
|
+
}): Promise<IBacklogServerHandle>;
|
|
277
|
+
/**
|
|
278
|
+
* UNCHANGED public signature (`Promise<void>`, resolves on close) — a thin
|
|
279
|
+
* wrapper over {@link createBacklogServer} for existing callers.
|
|
212
280
|
*/
|
|
213
281
|
export declare function startBacklogServer(opts: StartOpts): Promise<void>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { StartOpts } from './server.js';
|
|
2
|
+
import { BacklogConfig } from './env.js';
|
|
3
|
+
import { Environment } from '@adhd/environment';
|
|
4
|
+
import { Scope } from '@adhd/environment-base-spec';
|
|
5
|
+
|
|
6
|
+
export type IServiceTransport = 'mcp' | 'http' | 'both';
|
|
7
|
+
export interface IArtifactIdentity {
|
|
8
|
+
kind: 'path' | 'package';
|
|
9
|
+
/** kind:'path' — sha256 hex of the resolved entry file. */
|
|
10
|
+
sha256?: string;
|
|
11
|
+
/** kind:'package' — the published name + version to match. */
|
|
12
|
+
name?: string;
|
|
13
|
+
version?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface IServiceConfig {
|
|
16
|
+
transport: IServiceTransport;
|
|
17
|
+
port: number;
|
|
18
|
+
host: string;
|
|
19
|
+
scope: Scope;
|
|
20
|
+
/** MUST be absolute (or the literal ':memory:' test path). */
|
|
21
|
+
dbPath: string;
|
|
22
|
+
busyTimeoutMs: number;
|
|
23
|
+
server: {
|
|
24
|
+
/** Absolute path, or a PATH-resolvable bin (`npx`/`node`). `''` = unset. */
|
|
25
|
+
command: string;
|
|
26
|
+
identity: IArtifactIdentity;
|
|
27
|
+
};
|
|
28
|
+
readiness: {
|
|
29
|
+
timeoutMs: number;
|
|
30
|
+
};
|
|
31
|
+
connect: {
|
|
32
|
+
/** Pre-connect deadline extension — the cold-start grace window. */
|
|
33
|
+
graceMs: number;
|
|
34
|
+
/** Total wall-clock budget across all attempts (bounded, never unbounded). */
|
|
35
|
+
budgetMs: number;
|
|
36
|
+
maxAttempts: number;
|
|
37
|
+
baseMs: number;
|
|
38
|
+
maxMs: number;
|
|
39
|
+
breakerFailureThreshold: number;
|
|
40
|
+
breakerResetMs: number;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The schema owner for the `service.*` subtree — the allow-list unknown keys
|
|
45
|
+
* are checked against. The keys are FULL dot-paths; `env.ts`'s spec declares
|
|
46
|
+
* exactly this set and a unit test asserts the two cannot drift.
|
|
47
|
+
*
|
|
48
|
+
* D-A apply-fix (2026-09-27): four keys that were RESOLVED but consumed by
|
|
49
|
+
* nothing were REMOVED rather than shipped as silent no-ops (the finding's
|
|
50
|
+
* "wire it, or stop declaring it" rule):
|
|
51
|
+
* - `service.namespace` — directly contradicts `env.ts`'s own contract that
|
|
52
|
+
* namespace selection is explicit-parameter-only
|
|
53
|
+
* (`BuildBacklogEnvOptions.namespace`), never resolved from an env var or
|
|
54
|
+
* config file; a key that can never be honored only misleads.
|
|
55
|
+
* - `service.serverArgs` — this process IS the server; nothing ever LAUNCHES
|
|
56
|
+
* `server.command` (its sole consumer is the load-time artifact-drift
|
|
57
|
+
* check, which ignores args), so args had no role.
|
|
58
|
+
* - `service.readinessIntervalMs` / `service.readinessMaxMissedTicks` — they
|
|
59
|
+
* describe a serving-loop watchdog TICK, but no such tick exists here and
|
|
60
|
+
* a same-process watchdog cannot observe its own frozen event loop;
|
|
61
|
+
* genuinely a later (external-supervisor) slice.
|
|
62
|
+
* The remaining keys are all APPLIED: transport/port/host mount the server,
|
|
63
|
+
* `serverCommand` drives `assertServerArtifact`, `connect*` drives
|
|
64
|
+
* `withResilience` around the serving-path readiness probe, and
|
|
65
|
+
* `readinessTimeoutMs` caps one probe.
|
|
66
|
+
*/
|
|
67
|
+
export declare const SERVICE_CONFIG_KEYS: readonly string[];
|
|
68
|
+
/**
|
|
69
|
+
* True when `candidate` is an executable bin reachable on `PATH`. An absolute
|
|
70
|
+
* path is NOT automatically accepted here — the drift check owns absolute
|
|
71
|
+
* existence; this helper only answers "can the shell find this bare name".
|
|
72
|
+
*/
|
|
73
|
+
export declare function isPathResolvableBin(candidate: string): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Resolves, validates and freezes the `service.*` configuration for one
|
|
76
|
+
* process. Throws {@link UnknownConfigKeyError} on an unknown key in ANY
|
|
77
|
+
* layer file and {@link NonAbsolutePathError} on a relative path field.
|
|
78
|
+
*/
|
|
79
|
+
export declare function resolveServiceConfig(opts: StartOpts, env: Environment<BacklogConfig>): IServiceConfig;
|
|
80
|
+
export interface IMcpServerEntry {
|
|
81
|
+
type?: string;
|
|
82
|
+
command?: string | string[];
|
|
83
|
+
args?: string[];
|
|
84
|
+
env?: Record<string, string>;
|
|
85
|
+
url?: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Validates ONE host MCP server entry the tool writes or reads. Unknown keys
|
|
89
|
+
* are a HARD ERROR (the recorded `environment` vs `env` silent no-op is the
|
|
90
|
+
* exact failure): a key outside the entry's own schema
|
|
91
|
+
* (`type/command/args/env/url`) throws {@link UnknownMcpConfigKeyError}; a
|
|
92
|
+
* relative `command` path that is not a PATH-resolvable bin throws
|
|
93
|
+
* {@link NonAbsolutePathError}.
|
|
94
|
+
*/
|
|
95
|
+
export declare function assertMcpEntryValid(entry: unknown, hostPath: string): asserts entry is IMcpServerEntry;
|
|
96
|
+
/**
|
|
97
|
+
* Load-time drift check. Resolves `server.command` to an absolute path (or a
|
|
98
|
+
* PATH-resolvable bin recorded with its resolved path), asserts it EXISTS, is
|
|
99
|
+
* EXECUTABLE, and that its content identity matches `server.identity`:
|
|
100
|
+
*
|
|
101
|
+
* - `kind:'path'` → sha256(resolved file) === identity.sha256
|
|
102
|
+
* - `kind:'package'` → the resolved package's package.json version === identity.version
|
|
103
|
+
*
|
|
104
|
+
* On missing/drift → {@link ArtifactDriftError} naming the resolved absolute
|
|
105
|
+
* path — it refuses to start rather than launch a phantom. A PURE read: it
|
|
106
|
+
* mutates nothing. A `command` that is unset (`''`) is a no-op, so a process
|
|
107
|
+
* that is not configured to launch an external artifact is never blocked.
|
|
108
|
+
*/
|
|
109
|
+
export declare function assertServerArtifact(server: IServiceConfig['server']): void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { BacklogWriteError } from './write/errors.js';
|
|
2
|
+
|
|
3
|
+
/** An unknown key under the `service.*` config subtree in a layer file. */
|
|
4
|
+
export declare class UnknownConfigKeyError extends BacklogWriteError {
|
|
5
|
+
readonly code: "E_VALIDATION";
|
|
6
|
+
readonly retryable = false;
|
|
7
|
+
/** The offending key, WITHOUT the `service.` prefix (e.g. `bogusKey`). */
|
|
8
|
+
readonly key: string;
|
|
9
|
+
/** The absolute layer file the key was found in. */
|
|
10
|
+
readonly file: string;
|
|
11
|
+
constructor(key: string, file: string);
|
|
12
|
+
}
|
|
13
|
+
/** A path-valued config field that is neither absolute nor a PATH bin. */
|
|
14
|
+
export declare class NonAbsolutePathError extends BacklogWriteError {
|
|
15
|
+
readonly code: "E_VALIDATION";
|
|
16
|
+
readonly retryable = false;
|
|
17
|
+
readonly field: string;
|
|
18
|
+
readonly value: string;
|
|
19
|
+
constructor(field: string, value: string);
|
|
20
|
+
}
|
|
21
|
+
/** An unknown key inside a host MCP server entry (the `environment` vs `env`
|
|
22
|
+
* silent no-op is the exact failure this prevents). */
|
|
23
|
+
export declare class UnknownMcpConfigKeyError extends BacklogWriteError {
|
|
24
|
+
readonly code: "E_VALIDATION";
|
|
25
|
+
readonly retryable = false;
|
|
26
|
+
/** The host config file the entry lives in. */
|
|
27
|
+
readonly entry: string;
|
|
28
|
+
readonly key: string;
|
|
29
|
+
/** Closest allowed key, when one is close enough to suggest. */
|
|
30
|
+
readonly suggestion?: string;
|
|
31
|
+
constructor(entry: string, key: string, suggestion?: string);
|
|
32
|
+
}
|
|
33
|
+
/** The configured server artifact is missing, non-executable, or its content
|
|
34
|
+
* identity does not match what was installed. */
|
|
35
|
+
export declare class ArtifactDriftError extends BacklogWriteError {
|
|
36
|
+
readonly code: "E_VALIDATION";
|
|
37
|
+
readonly retryable = false;
|
|
38
|
+
readonly expected: string;
|
|
39
|
+
readonly actual: string;
|
|
40
|
+
readonly resolvedPath: string;
|
|
41
|
+
constructor(expected: string, actual: string, resolvedPath: string);
|
|
42
|
+
}
|
|
43
|
+
/** A readiness/liveness failure surfaced by the resilience layer (a bounded
|
|
44
|
+
* budget was exhausted, or the breaker refused the attempt). */
|
|
45
|
+
export declare class ServiceNotReadyError extends BacklogWriteError {
|
|
46
|
+
readonly code: "E_VALIDATION";
|
|
47
|
+
readonly retryable = true;
|
|
48
|
+
readonly state: string;
|
|
49
|
+
readonly failure?: string;
|
|
50
|
+
constructor(state: string, failure?: string);
|
|
51
|
+
}
|