@blokjs/capabilities 2.2.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.
@@ -0,0 +1,280 @@
1
+ import { GRAPH_MAX_DEPTH, parseGraphFreshnessRequest, parseGraphImpactRequest, parseGraphIndexRequest, parseGraphRelationRequest, parseGraphSearchRequest, parseGraphSymbolRequest, } from "@blokjs/shared";
2
+ import { GraphProviderError } from "./GraphProviderError.js";
3
+ import { checkCancelled, compareScope, indexResponse, makeError, makeFreshness, makeProvenance, makeStatus, response, } from "./GraphProviderSupport.js";
4
+ const DEFAULT_SUPPORTED = ["search", "symbol", "relations", "impact", "freshness", "index"];
5
+ function operationError(operation) {
6
+ return new GraphProviderError("unsupported", "GRAPH_UNSUPPORTED_OPERATION", `Graph operation '${operation}' is unsupported`, {
7
+ guidance: "inspect-provider",
8
+ });
9
+ }
10
+ function invalidRequest(error) {
11
+ return new GraphProviderError("invalid-query", "GRAPH_INVALID_REQUEST", error instanceof Error ? error.message : "Invalid graph request", { guidance: "narrow-query" });
12
+ }
13
+ function missingError(message) {
14
+ return new GraphProviderError("not-found", "GRAPH_NOT_FOUND", message, {
15
+ guidance: "reread-authoritative-source",
16
+ });
17
+ }
18
+ /** Deterministic in-memory provider used for contract tests and local adapters. */
19
+ export class FakeGraphProvider {
20
+ id = "fake";
21
+ version = "fake-1";
22
+ symbols = new Map();
23
+ relationsById = new Map();
24
+ indexedHashes = new Map();
25
+ supported;
26
+ now;
27
+ indexedScope;
28
+ indexedAt;
29
+ indexVersion = "fake-index-1";
30
+ constructor(options = {}) {
31
+ this.supported = new Set(options.supportedOperations ?? DEFAULT_SUPPORTED);
32
+ this.now = options.now ?? (() => new Date().toISOString());
33
+ if (options.files && options.scope) {
34
+ this.seed(options.scope, options.files);
35
+ }
36
+ }
37
+ seed(scope, files) {
38
+ const request = { scope, files, reason: "initial" };
39
+ this.applyIndex(request);
40
+ }
41
+ async search(input, options) {
42
+ checkCancelled(options?.signal);
43
+ const request = this.parse(() => parseGraphSearchRequest(input));
44
+ if (!this.supported.has("search"))
45
+ return this.unsupported("search");
46
+ const limit = request.limit ?? 50;
47
+ const freshness = this.scopeMetadata(request.scope);
48
+ const query = request.query.toLowerCase();
49
+ const kinds = request.kinds ? new Set(request.kinds) : undefined;
50
+ const hits = [...this.symbols.values()]
51
+ .filter((symbol) => !kinds || kinds.has("symbol"))
52
+ .filter((symbol) => !request.pathPrefix || symbol.location.path.startsWith(request.pathPrefix))
53
+ .map((symbol) => ({ symbol, score: this.score(symbol, query) }))
54
+ .filter((match) => match.score > 0)
55
+ .sort((left, right) => right.score - left.score || left.symbol.id.localeCompare(right.symbol.id))
56
+ .map(({ symbol, score }) => ({
57
+ id: symbol.id,
58
+ kind: "symbol",
59
+ name: symbol.name,
60
+ score,
61
+ symbol,
62
+ location: symbol.location,
63
+ }));
64
+ const truncated = hits.length > limit;
65
+ const items = hits.slice(0, limit);
66
+ const states = truncated ? [freshness.state, "truncated"] : [freshness.state];
67
+ if (items.length === 0)
68
+ states.push("missing");
69
+ return response(items, makeStatus(states[0], states.slice(1)), freshness.freshness, freshness.provenance, items.length === 0 ? [makeError(missingError(`No graph symbols matched '${request.query}'`))] : [], truncated ? String(limit) : undefined);
70
+ }
71
+ async findSymbol(input, options) {
72
+ checkCancelled(options?.signal);
73
+ const request = this.parse(() => parseGraphSymbolRequest(input));
74
+ if (!this.supported.has("symbol"))
75
+ return this.unsupported("symbol");
76
+ const limit = request.limit ?? 50;
77
+ const freshness = this.scopeMetadata(request.scope, request.path);
78
+ const items = [...this.symbols.values()]
79
+ .filter((symbol) => (request.symbolId ? symbol.id === request.symbolId : symbol.name === request.name))
80
+ .filter((symbol) => !request.path || symbol.location.path === request.path)
81
+ .sort((left, right) => left.id.localeCompare(right.id))
82
+ .slice(0, limit);
83
+ const states = items.length === 0 ? [freshness.state, "missing"] : [freshness.state];
84
+ return response(items, makeStatus(states[0], states.slice(1)), freshness.freshness, freshness.provenance, items.length === 0 ? [makeError(missingError("Requested symbol is not present in the graph"))] : []);
85
+ }
86
+ async relations(input, options) {
87
+ checkCancelled(options?.signal);
88
+ const request = this.parse(() => parseGraphRelationRequest(input));
89
+ if (!this.supported.has("relations"))
90
+ return this.unsupported("relations");
91
+ const limit = request.limit ?? 100;
92
+ const direction = request.direction ?? "outbound";
93
+ const kinds = request.kinds ? new Set(request.kinds) : undefined;
94
+ const freshness = this.scopeMetadata(request.scope);
95
+ const items = [...this.relationsById.values()]
96
+ .filter((relation) => (direction === "outbound" && relation.from === request.symbolId) ||
97
+ (direction === "inbound" && relation.to === request.symbolId) ||
98
+ (direction === "both" && (relation.from === request.symbolId || relation.to === request.symbolId)))
99
+ .filter((relation) => !kinds || kinds.has(relation.kind))
100
+ .sort((left, right) => left.id.localeCompare(right.id));
101
+ const truncated = items.length > limit;
102
+ const visible = items.slice(0, limit);
103
+ const states = [freshness.state];
104
+ if (truncated)
105
+ states.push("truncated");
106
+ if (visible.length === 0)
107
+ states.push("missing");
108
+ return response(visible, makeStatus(states[0], states.slice(1)), freshness.freshness, freshness.provenance, visible.length === 0 ? [makeError(missingError("No requested graph relations were found"))] : [], truncated ? String(limit) : undefined);
109
+ }
110
+ async impact(input, options) {
111
+ checkCancelled(options?.signal);
112
+ const request = this.parse(() => parseGraphImpactRequest(input));
113
+ if (!this.supported.has("impact"))
114
+ return this.unsupported("impact");
115
+ const direction = request.direction ?? "outbound";
116
+ const maxDepth = request.maxDepth ?? GRAPH_MAX_DEPTH;
117
+ const limit = request.limit ?? 100;
118
+ const kinds = request.relationKinds ? new Set(request.relationKinds) : undefined;
119
+ const freshness = this.scopeMetadata(request.scope);
120
+ const found = new Set();
121
+ const frontier = [{ id: request.symbolId, depth: 0 }];
122
+ while (frontier.length > 0) {
123
+ const current = frontier.shift();
124
+ if (!current || current.depth >= maxDepth)
125
+ continue;
126
+ for (const relation of this.relationsById.values()) {
127
+ if (kinds && !kinds.has(relation.kind))
128
+ continue;
129
+ const next = direction === "outbound"
130
+ ? relation.from === current.id
131
+ ? relation.to
132
+ : undefined
133
+ : relation.to === current.id
134
+ ? relation.from
135
+ : undefined;
136
+ if (!next || found.has(next))
137
+ continue;
138
+ found.add(next);
139
+ frontier.push({ id: next, depth: current.depth + 1 });
140
+ }
141
+ }
142
+ const items = [...found]
143
+ .map((id) => this.symbols.get(id))
144
+ .filter((symbol) => symbol !== undefined)
145
+ .sort((left, right) => left.id.localeCompare(right.id));
146
+ const truncated = items.length > limit;
147
+ const visible = items.slice(0, limit);
148
+ const states = [freshness.state];
149
+ if (truncated)
150
+ states.push("truncated");
151
+ if (visible.length === 0)
152
+ states.push("missing");
153
+ return response(visible, makeStatus(states[0], states.slice(1)), freshness.freshness, freshness.provenance, visible.length === 0 ? [makeError(missingError("No impacted symbols were found"))] : [], truncated ? String(limit) : undefined);
154
+ }
155
+ async freshness(input, options) {
156
+ checkCancelled(options?.signal);
157
+ const request = this.parse(() => parseGraphFreshnessRequest(input));
158
+ if (!this.supported.has("freshness"))
159
+ return this.unsupported("freshness");
160
+ const paths = request.paths ?? [...this.indexedHashes.keys()].sort();
161
+ const metadata = this.scopeMetadata(request.scope);
162
+ const items = paths
163
+ .filter((candidate) => this.indexedHashes.has(candidate))
164
+ .map((candidate) => ({ path: candidate, contentHash: this.indexedHashes.get(candidate) }));
165
+ const missingPaths = paths.filter((candidate) => !this.indexedHashes.has(candidate));
166
+ const states = [metadata.state];
167
+ if (missingPaths.length > 0)
168
+ states.push("missing");
169
+ return response(items, makeStatus(states[0], states.slice(1)), metadata.freshness, metadata.provenance, missingPaths.map((path) => makeError(missingError("Path is not present in the graph"), path)));
170
+ }
171
+ async index(input, options) {
172
+ checkCancelled(options?.signal);
173
+ if (!this.supported.has("index")) {
174
+ const error = operationError("index");
175
+ return indexResponse([], [], makeStatus("unsupported"), makeFreshness("unknown", this.now(), undefined, { reason: error.message }), undefined, [makeError(error)]);
176
+ }
177
+ let request;
178
+ try {
179
+ request = parseGraphIndexRequest(input);
180
+ }
181
+ catch (error) {
182
+ throw invalidRequest(error);
183
+ }
184
+ checkCancelled(options?.signal);
185
+ this.applyIndex(request);
186
+ const provenance = this.indexedScope
187
+ ? makeProvenance(this.id, this.version, this.indexVersion, this.indexedScope, this.indexedAt ?? this.now())
188
+ : undefined;
189
+ return indexResponse(request.files.map((file) => file.path).sort(), [], makeStatus("fresh"), makeFreshness("fresh", this.now(), this.indexedAt), provenance);
190
+ }
191
+ applyIndex(request) {
192
+ for (const file of request.files) {
193
+ for (const [id, symbol] of this.symbols)
194
+ if (symbol.location.path === file.path)
195
+ this.symbols.delete(id);
196
+ for (const [id, relation] of this.relationsById)
197
+ if (relation.location?.path === file.path)
198
+ this.relationsById.delete(id);
199
+ for (const symbol of file.symbols)
200
+ this.symbols.set(symbol.id, symbol);
201
+ for (const relation of file.relations)
202
+ this.relationsById.set(relation.id, relation);
203
+ this.indexedHashes.set(file.path, file.contentHash);
204
+ }
205
+ this.indexedScope = {
206
+ repository: request.scope.repository,
207
+ worktree: request.scope.worktree,
208
+ commit: request.scope.commit ?? request.scope.worktree?.commit,
209
+ };
210
+ this.indexVersion = request.indexVersion ?? this.indexVersion;
211
+ this.indexedAt = this.now();
212
+ }
213
+ score(symbol, query) {
214
+ const name = symbol.name.toLowerCase();
215
+ const id = symbol.id.toLowerCase();
216
+ if (name === query || id === query)
217
+ return 1;
218
+ if (name.startsWith(query) || id.startsWith(query))
219
+ return 0.9;
220
+ if (name.includes(query) || id.includes(query) || symbol.location.path.toLowerCase().includes(query))
221
+ return 0.75;
222
+ return symbol.signature?.toLowerCase().includes(query) ? 0.6 : 0;
223
+ }
224
+ parse(parser) {
225
+ try {
226
+ return parser();
227
+ }
228
+ catch (error) {
229
+ throw invalidRequest(error);
230
+ }
231
+ }
232
+ unsupported(operation) {
233
+ const error = operationError(operation);
234
+ return response([], makeStatus("unsupported"), makeFreshness("unknown", this.now(), undefined, { reason: error.message }), undefined, [makeError(error)]);
235
+ }
236
+ scopeMetadata(scope, path) {
237
+ const checkedAt = this.now();
238
+ if (!this.indexedScope || !this.indexedAt) {
239
+ return {
240
+ state: "missing",
241
+ freshness: makeFreshness("unknown", checkedAt, undefined, { reason: "index is unavailable" }),
242
+ };
243
+ }
244
+ const comparison = compareScope(this.indexedScope, scope);
245
+ let state = comparison.state === "unknown" ? "missing" : comparison.state;
246
+ let reason = comparison.reason;
247
+ let observedContentHash;
248
+ let indexedContentHash;
249
+ const paths = path ? [path] : Object.keys(scope.contentHashes ?? {});
250
+ for (const candidate of paths) {
251
+ const observed = scope.contentHashes?.[candidate];
252
+ const indexed = this.indexedHashes.get(candidate);
253
+ if (observed && indexed && observed !== indexed) {
254
+ state = "conflict";
255
+ reason = "authoritative content hash differs from the index";
256
+ observedContentHash = observed;
257
+ indexedContentHash = indexed;
258
+ break;
259
+ }
260
+ if (observed && !indexed) {
261
+ state = "missing";
262
+ reason = "path is not indexed";
263
+ observedContentHash = observed;
264
+ break;
265
+ }
266
+ }
267
+ const freshnessState = state === "fresh" ? "fresh" : "stale";
268
+ return {
269
+ state,
270
+ freshness: makeFreshness(freshnessState, checkedAt, this.indexedAt, {
271
+ indexedCommit: comparison.indexedCommit,
272
+ observedCommit: comparison.observedCommit,
273
+ indexedContentHash,
274
+ observedContentHash,
275
+ reason,
276
+ }),
277
+ provenance: makeProvenance(this.id, this.version, this.indexVersion, this.indexedScope, this.indexedAt),
278
+ };
279
+ }
280
+ }
@@ -0,0 +1,5 @@
1
+ import type { CapabilityManifestV1 } from "@blokjs/shared";
2
+ /** Existing H0-02 vocabulary for graph queries; authorization stays with PolicyProvider. */
3
+ export declare const GRAPH_QUERY_CAPABILITY_MANIFEST: CapabilityManifestV1;
4
+ /** Index persistence is a derived-index write, never a workspace/source write. */
5
+ export declare const GRAPH_INDEX_CAPABILITY_MANIFEST: CapabilityManifestV1;
@@ -0,0 +1,22 @@
1
+ /** Existing H0-02 vocabulary for graph queries; authorization stays with PolicyProvider. */
2
+ export const GRAPH_QUERY_CAPABILITY_MANIFEST = Object.freeze({
3
+ version: "1",
4
+ classification: "agent-compatible",
5
+ effects: ["read"],
6
+ capabilities: ["graph.query"],
7
+ secrets: [],
8
+ determinism: "external",
9
+ idempotency: "idempotent",
10
+ maturity: "experimental",
11
+ });
12
+ /** Index persistence is a derived-index write, never a workspace/source write. */
13
+ export const GRAPH_INDEX_CAPABILITY_MANIFEST = Object.freeze({
14
+ version: "1",
15
+ classification: "agent-compatible",
16
+ effects: ["read", "write"],
17
+ capabilities: ["graph.index"],
18
+ secrets: [],
19
+ determinism: "external",
20
+ idempotency: "conditionally-idempotent",
21
+ maturity: "experimental",
22
+ });
@@ -0,0 +1,12 @@
1
+ import type { GraphErrorCategory } from "@blokjs/shared";
2
+ export declare class GraphProviderError extends Error {
3
+ readonly code: string;
4
+ readonly category: GraphErrorCategory;
5
+ readonly retryable: boolean;
6
+ readonly guidance: "reread-authoritative-source" | "retry" | "narrow-query" | "inspect-provider" | "none";
7
+ constructor(category: GraphErrorCategory, code: string, message: string, options?: {
8
+ retryable?: boolean;
9
+ guidance?: "reread-authoritative-source" | "retry" | "narrow-query" | "inspect-provider" | "none";
10
+ });
11
+ static cancelled(): GraphProviderError;
12
+ }
@@ -0,0 +1,20 @@
1
+ export class GraphProviderError extends Error {
2
+ code;
3
+ category;
4
+ retryable;
5
+ guidance;
6
+ constructor(category, code, message, options = {}) {
7
+ super(message);
8
+ this.name = "GraphProviderError";
9
+ this.category = category;
10
+ this.code = code;
11
+ this.retryable = options.retryable ?? false;
12
+ this.guidance = options.guidance ?? "none";
13
+ }
14
+ static cancelled() {
15
+ return new GraphProviderError("cancelled", "GRAPH_CANCELLED", "Graph operation was cancelled", {
16
+ retryable: true,
17
+ guidance: "retry",
18
+ });
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ import { type GraphError, type GraphFreshness, type GraphIndexResponse, type GraphProvenance, type GraphQueryResponse, type GraphRepositoryIdentity, type GraphResultState, type GraphResultStatus, type GraphScope, type GraphWorktreeIdentity } from "@blokjs/shared";
2
+ import { GraphProviderError } from "./GraphProviderError.js";
3
+ export interface IndexedScope {
4
+ readonly repository: GraphRepositoryIdentity;
5
+ readonly worktree?: GraphWorktreeIdentity;
6
+ readonly commit?: string;
7
+ }
8
+ export declare function checkCancelled(signal?: AbortSignal): void;
9
+ export declare function makeStatus(primary: GraphResultState, additional?: readonly GraphResultState[]): GraphResultStatus;
10
+ export declare function makeError(error: GraphProviderError, path?: string): GraphError;
11
+ export declare function makeProvenance(provider: string, providerVersion: string, indexVersion: string, scope: IndexedScope, indexedAt: string): GraphProvenance;
12
+ export declare function makeFreshness(state: GraphFreshness["state"], checkedAt: string, indexedAt?: string, comparison?: Partial<Pick<GraphFreshness, "indexedCommit" | "observedCommit" | "indexedContentHash" | "observedContentHash" | "reason">>): GraphFreshness;
13
+ export declare function response<T>(items: readonly T[], status: GraphResultStatus, freshness: GraphFreshness, provenance: GraphProvenance | undefined, errors?: readonly GraphError[], nextCursor?: string): GraphQueryResponse<T>;
14
+ export declare function indexResponse(indexedFiles: readonly string[], skippedFiles: readonly string[], status: GraphResultStatus, freshness: GraphFreshness, provenance: GraphProvenance | undefined, errors?: readonly GraphError[]): GraphIndexResponse;
15
+ export declare function compareScope(indexed: IndexedScope | undefined, requested: GraphScope): {
16
+ state: "fresh" | "stale" | "conflict" | "unknown";
17
+ reason?: string;
18
+ indexedCommit?: string;
19
+ observedCommit?: string;
20
+ };
@@ -0,0 +1,95 @@
1
+ import { GRAPH_CONTRACT_VERSION, } from "@blokjs/shared";
2
+ import { GraphProviderError } from "./GraphProviderError.js";
3
+ export function checkCancelled(signal) {
4
+ if (signal?.aborted)
5
+ throw GraphProviderError.cancelled();
6
+ }
7
+ export function makeStatus(primary, additional = []) {
8
+ const states = [...new Set([primary, ...additional])];
9
+ return { primary, states, complete: states.every((state) => state === "fresh") };
10
+ }
11
+ export function makeError(error, path) {
12
+ return {
13
+ code: error.code,
14
+ category: error.category,
15
+ message: error.message,
16
+ retryable: error.retryable,
17
+ guidance: error.guidance,
18
+ ...(path ? { path } : {}),
19
+ };
20
+ }
21
+ export function makeProvenance(provider, providerVersion, indexVersion, scope, indexedAt) {
22
+ return {
23
+ source: "derived-index",
24
+ provider,
25
+ providerVersion,
26
+ indexVersion,
27
+ repository: scope.repository,
28
+ ...(scope.worktree ? { worktree: scope.worktree } : {}),
29
+ ...(scope.commit ? { commit: scope.commit } : {}),
30
+ indexedAt,
31
+ };
32
+ }
33
+ export function makeFreshness(state, checkedAt, indexedAt, comparison = {}) {
34
+ return {
35
+ state,
36
+ checkedAt,
37
+ ...(indexedAt ? { indexedAt } : {}),
38
+ ...comparison,
39
+ };
40
+ }
41
+ export function response(items, status, freshness, provenance, errors = [], nextCursor) {
42
+ return {
43
+ version: GRAPH_CONTRACT_VERSION,
44
+ authority: "navigation-only",
45
+ items,
46
+ status,
47
+ freshness,
48
+ ...(provenance ? { provenance } : {}),
49
+ errors,
50
+ ...(nextCursor ? { nextCursor } : {}),
51
+ };
52
+ }
53
+ export function indexResponse(indexedFiles, skippedFiles, status, freshness, provenance, errors = []) {
54
+ return {
55
+ version: GRAPH_CONTRACT_VERSION,
56
+ authority: "navigation-only",
57
+ indexedFiles,
58
+ skippedFiles,
59
+ status,
60
+ freshness,
61
+ ...(provenance ? { provenance } : {}),
62
+ errors,
63
+ };
64
+ }
65
+ function sameIdentity(left, right) {
66
+ return left.provider === right.provider && left.id === right.id && left.revision === right.revision;
67
+ }
68
+ function sameWorktree(left, right) {
69
+ if (!left || !right)
70
+ return left === right;
71
+ return left.id === right.id && left.branch === right.branch;
72
+ }
73
+ export function compareScope(indexed, requested) {
74
+ if (!indexed)
75
+ return { state: "unknown", reason: "index is unavailable" };
76
+ if (!sameIdentity(indexed.repository, requested.repository)) {
77
+ return { state: "stale", reason: "repository identity differs" };
78
+ }
79
+ if (!sameWorktree(indexed.worktree, requested.worktree)) {
80
+ return { state: "stale", reason: "worktree identity differs" };
81
+ }
82
+ const requestedCommit = requested.commit ?? requested.worktree?.commit;
83
+ if (indexed.commit !== requestedCommit) {
84
+ return {
85
+ state: "stale",
86
+ reason: "indexed commit differs",
87
+ indexedCommit: indexed.commit,
88
+ observedCommit: requestedCommit,
89
+ };
90
+ }
91
+ if (requested.worktree?.overlay === "uncommitted" || requested.worktree?.dirty === true) {
92
+ return { state: "stale", reason: "working tree has an uncommitted overlay" };
93
+ }
94
+ return { state: "fresh" };
95
+ }
@@ -0,0 +1,35 @@
1
+ import { type GraphFreshnessRequest, type GraphImpactRequest, type GraphIndexRequest, type GraphIndexResponse, type GraphLocation, type GraphProvider, type GraphQueryOptions, type GraphQueryResponse, type GraphRelation, type GraphRelationRequest, type GraphSearchHit, type GraphSearchRequest, type GraphSymbol, type GraphSymbolRequest } from "@blokjs/shared";
2
+ /**
3
+ * Transport seam for Tetrix. The repository does not vendor a Tetrix client;
4
+ * an integration supplies this narrow transport without exposing its native
5
+ * response shapes to workflows.
6
+ */
7
+ export interface TetrixTransport {
8
+ search(request: GraphSearchRequest, options?: GraphQueryOptions): Promise<unknown>;
9
+ findSymbol(request: GraphSymbolRequest, options?: GraphQueryOptions): Promise<unknown>;
10
+ relations(request: GraphRelationRequest, options?: GraphQueryOptions): Promise<unknown>;
11
+ impact(request: GraphImpactRequest, options?: GraphQueryOptions): Promise<unknown>;
12
+ freshness(request: GraphFreshnessRequest, options?: GraphQueryOptions): Promise<unknown>;
13
+ index(request: GraphIndexRequest, options?: GraphQueryOptions): Promise<unknown>;
14
+ }
15
+ export interface TetrixGraphProviderOptions {
16
+ readonly providerVersion?: string;
17
+ readonly indexVersion?: string;
18
+ }
19
+ /** First-party seam for Tetrix; only normalized Blok graph contracts escape it. */
20
+ export declare class TetrixGraphProvider implements GraphProvider {
21
+ private readonly transport;
22
+ readonly id = "tetrix";
23
+ readonly version: string;
24
+ private readonly indexVersion;
25
+ constructor(transport: TetrixTransport, options?: TetrixGraphProviderOptions);
26
+ search(request: GraphSearchRequest, options?: GraphQueryOptions): Promise<GraphQueryResponse<GraphSearchHit>>;
27
+ findSymbol(request: GraphSymbolRequest, options?: GraphQueryOptions): Promise<GraphQueryResponse<GraphSymbol>>;
28
+ relations(request: GraphRelationRequest, options?: GraphQueryOptions): Promise<GraphQueryResponse<GraphRelation>>;
29
+ impact(request: GraphImpactRequest, options?: GraphQueryOptions): Promise<GraphQueryResponse<GraphSymbol>>;
30
+ freshness(request: GraphFreshnessRequest, options?: GraphQueryOptions): Promise<GraphQueryResponse<GraphLocation>>;
31
+ index(request: GraphIndexRequest, options?: GraphQueryOptions): Promise<GraphIndexResponse>;
32
+ private stampQuery;
33
+ private stampIndex;
34
+ private provenance;
35
+ }
@@ -0,0 +1,99 @@
1
+ import { GraphContractError, GraphLocationSchema, GraphRelationSchema, GraphSearchHitSchema, GraphSymbolSchema, parseGraphFreshnessRequest, parseGraphImpactRequest, parseGraphIndexRequest, parseGraphIndexResponse, parseGraphQueryResponse, parseGraphRelationRequest, parseGraphSearchRequest, parseGraphSymbolRequest, } from "@blokjs/shared";
2
+ import { GraphProviderError } from "./GraphProviderError.js";
3
+ function invalidResponse(error) {
4
+ return new GraphProviderError("internal", "GRAPH_INVALID_PROVIDER_RESPONSE", error instanceof Error ? error.message : "Tetrix returned an invalid graph response", { guidance: "inspect-provider" });
5
+ }
6
+ function call(operation, parse) {
7
+ return operation()
8
+ .then((value) => {
9
+ try {
10
+ return parse(value);
11
+ }
12
+ catch (error) {
13
+ if (error instanceof GraphProviderError)
14
+ throw error;
15
+ if (error instanceof GraphContractError)
16
+ throw invalidResponse(error);
17
+ throw invalidResponse(error);
18
+ }
19
+ })
20
+ .catch((error) => {
21
+ if (error instanceof GraphProviderError)
22
+ throw error;
23
+ throw new GraphProviderError("provider-unavailable", "GRAPH_TETRIX_UNAVAILABLE", String(error), {
24
+ retryable: true,
25
+ guidance: "retry",
26
+ });
27
+ });
28
+ }
29
+ function validateRequest(parser, signal) {
30
+ if (signal?.aborted)
31
+ throw GraphProviderError.cancelled();
32
+ try {
33
+ return parser();
34
+ }
35
+ catch (error) {
36
+ throw new GraphProviderError("invalid-query", "GRAPH_INVALID_REQUEST", error instanceof Error ? error.message : "Invalid graph request", { guidance: "narrow-query" });
37
+ }
38
+ }
39
+ /** First-party seam for Tetrix; only normalized Blok graph contracts escape it. */
40
+ export class TetrixGraphProvider {
41
+ transport;
42
+ id = "tetrix";
43
+ version;
44
+ indexVersion;
45
+ constructor(transport, options = {}) {
46
+ this.transport = transport;
47
+ this.version = options.providerVersion ?? "tetrix-1";
48
+ this.indexVersion = options.indexVersion ?? "tetrix-index-unknown";
49
+ }
50
+ search(request, options) {
51
+ const normalized = validateRequest(() => parseGraphSearchRequest(request), options?.signal);
52
+ return call(() => this.transport.search(normalized, options), (value) => this.stampQuery(parseGraphQueryResponse(GraphSearchHitSchema, value), normalized));
53
+ }
54
+ findSymbol(request, options) {
55
+ const normalized = validateRequest(() => parseGraphSymbolRequest(request), options?.signal);
56
+ return call(() => this.transport.findSymbol(normalized, options), (value) => this.stampQuery(parseGraphQueryResponse(GraphSymbolSchema, value), normalized));
57
+ }
58
+ relations(request, options) {
59
+ const normalized = validateRequest(() => parseGraphRelationRequest(request), options?.signal);
60
+ return call(() => this.transport.relations(normalized, options), (value) => this.stampQuery(parseGraphQueryResponse(GraphRelationSchema, value), normalized));
61
+ }
62
+ impact(request, options) {
63
+ const normalized = validateRequest(() => parseGraphImpactRequest(request), options?.signal);
64
+ return call(() => this.transport.impact(normalized, options), (value) => this.stampQuery(parseGraphQueryResponse(GraphSymbolSchema, value), normalized));
65
+ }
66
+ freshness(request, options) {
67
+ const normalized = validateRequest(() => parseGraphFreshnessRequest(request), options?.signal);
68
+ return call(() => this.transport.freshness(normalized, options), (value) => this.stampQuery(parseGraphQueryResponse(GraphLocationSchema, value), normalized));
69
+ }
70
+ index(request, options) {
71
+ const normalized = validateRequest(() => parseGraphIndexRequest(request), options?.signal);
72
+ return call(() => this.transport.index(normalized, options), (value) => this.stampIndex(parseGraphIndexResponse(value), normalized));
73
+ }
74
+ stampQuery(result, request) {
75
+ return {
76
+ ...result,
77
+ provenance: this.provenance(result.provenance, request.scope, result.freshness.indexedAt),
78
+ };
79
+ }
80
+ stampIndex(result, request) {
81
+ return {
82
+ ...result,
83
+ provenance: this.provenance(result.provenance, request.scope, result.freshness.indexedAt, request.indexVersion),
84
+ };
85
+ }
86
+ provenance(existing, scope, indexedAt, indexVersion = this.indexVersion) {
87
+ return {
88
+ ...(existing ?? {}),
89
+ source: "derived-index",
90
+ provider: this.id,
91
+ providerVersion: this.version,
92
+ indexVersion,
93
+ repository: existing?.repository ?? scope.repository,
94
+ ...(existing?.worktree || scope.worktree ? { worktree: existing?.worktree ?? scope.worktree } : {}),
95
+ ...(existing?.commit || scope.commit ? { commit: existing?.commit ?? scope.commit } : {}),
96
+ ...(existing?.indexedAt || indexedAt ? { indexedAt: existing?.indexedAt ?? indexedAt } : {}),
97
+ };
98
+ }
99
+ }
@@ -0,0 +1,11 @@
1
+ export { WORKSPACE_FILESYSTEM_CONTRACT_VERSION, WORKSPACE_FILESYSTEM_OPERATIONS, WORKSPACE_FILESYSTEM_MAX_PATH_LENGTH, WORKSPACE_FILESYSTEM_MAX_QUERY_LENGTH, WORKSPACE_FILESYSTEM_MAX_READ_BYTES, WORKSPACE_FILESYSTEM_MAX_WRITE_BYTES, WORKSPACE_FILESYSTEM_MAX_LIST_FILES, WORKSPACE_FILESYSTEM_MAX_SEARCH_FILES, WORKSPACE_FILESYSTEM_MAX_SEARCH_MATCHES, WORKSPACE_FILESYSTEM_MAX_SEARCH_BYTES, WORKSPACE_FILESYSTEM_MAX_LINES, WORKSPACE_FILESYSTEM_MAX_WATCH_EVENTS, WORKSPACE_FILESYSTEM_MAX_DURATION_MS, WORKSPACE_FILESYSTEM_MAX_WATCH_DEBOUNCE_MS, type WorkspaceFilesystemOperation, type WorkspaceRootInput, type WorkspaceRoot, type WorkspaceFilesystemLimits, type WorkspaceFilesystemPolicyRequest, type WorkspaceFilesystemPolicy, type WorkspaceFilesystemOptions, type WorkspacePathInput, type WorkspaceFileKind, type WorkspaceArtifact, type WorkspaceFileMetadata, type WorkspaceMetadataInput, type WorkspaceListInput, type WorkspaceListResult, type WorkspaceReadEncoding, type WorkspaceReadInput, type WorkspaceReadResult, type WorkspaceSearchInput, type WorkspaceSearchMatch, type WorkspaceSearchResult, type WorkspaceWriteInput, type WorkspaceWriteResult, type WorkspaceTextPatch, type WorkspacePatchInput, type WorkspaceWatchInput, type WorkspaceWatchEvent, type WorkspacePolicyDecision, } from "./contracts.js";
2
+ export { WORKSPACE_FILESYSTEM_ERROR_CODES, WorkspaceFilesystemError, type WorkspaceFilesystemErrorCode, } from "./errors.js";
3
+ export { WorkspaceFilesystemCapability, workspaceFilesystemManifest, workspaceFilesystemAuthority, workspaceRelativePath, } from "./WorkspaceFilesystemCapability.js";
4
+ export { BoundedGraphIndexer } from "./graph/BoundedGraphIndexer.js";
5
+ export type { BoundedGraphIndexerOptions, GraphIndexJobHandle, GraphIndexerEvent, GraphIndexerEnqueueOptions, } from "./graph/BoundedGraphIndexer.js";
6
+ export { FakeGraphProvider } from "./graph/FakeGraphProvider.js";
7
+ export type { FakeGraphProviderOptions } from "./graph/FakeGraphProvider.js";
8
+ export { GRAPH_INDEX_CAPABILITY_MANIFEST, GRAPH_QUERY_CAPABILITY_MANIFEST } from "./graph/GraphCapabilityManifests.js";
9
+ export { GraphProviderError } from "./graph/GraphProviderError.js";
10
+ export { TetrixGraphProvider } from "./graph/TetrixGraphProvider.js";
11
+ export type { TetrixGraphProviderOptions, TetrixTransport } from "./graph/TetrixGraphProvider.js";
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { WORKSPACE_FILESYSTEM_CONTRACT_VERSION, WORKSPACE_FILESYSTEM_OPERATIONS, WORKSPACE_FILESYSTEM_MAX_PATH_LENGTH, WORKSPACE_FILESYSTEM_MAX_QUERY_LENGTH, WORKSPACE_FILESYSTEM_MAX_READ_BYTES, WORKSPACE_FILESYSTEM_MAX_WRITE_BYTES, WORKSPACE_FILESYSTEM_MAX_LIST_FILES, WORKSPACE_FILESYSTEM_MAX_SEARCH_FILES, WORKSPACE_FILESYSTEM_MAX_SEARCH_MATCHES, WORKSPACE_FILESYSTEM_MAX_SEARCH_BYTES, WORKSPACE_FILESYSTEM_MAX_LINES, WORKSPACE_FILESYSTEM_MAX_WATCH_EVENTS, WORKSPACE_FILESYSTEM_MAX_DURATION_MS, WORKSPACE_FILESYSTEM_MAX_WATCH_DEBOUNCE_MS, } from "./contracts.js";
2
+ export { WORKSPACE_FILESYSTEM_ERROR_CODES, WorkspaceFilesystemError, } from "./errors.js";
3
+ export { WorkspaceFilesystemCapability, workspaceFilesystemManifest, workspaceFilesystemAuthority, workspaceRelativePath, } from "./WorkspaceFilesystemCapability.js";
4
+ export { BoundedGraphIndexer } from "./graph/BoundedGraphIndexer.js";
5
+ export { FakeGraphProvider } from "./graph/FakeGraphProvider.js";
6
+ export { GRAPH_INDEX_CAPABILITY_MANIFEST, GRAPH_QUERY_CAPABILITY_MANIFEST } from "./graph/GraphCapabilityManifests.js";
7
+ export { GraphProviderError } from "./graph/GraphProviderError.js";
8
+ export { TetrixGraphProvider } from "./graph/TetrixGraphProvider.js";
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@blokjs/capabilities",
3
+ "version": "2.2.0",
4
+ "description": "Trusted, bounded capability adapters for Blok harnesses.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": ["dist"],
9
+ "scripts": {
10
+ "build": "rm -rf dist && bun run tsc",
11
+ "build:dev": "tsc --watch",
12
+ "test": "vitest run",
13
+ "test:dev": "vitest",
14
+ "typecheck": "tsc --noEmit"
15
+ },
16
+ "keywords": ["blok", "capabilities", "filesystem", "harness"],
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "@blokjs/shared": "^2.1.0",
20
+ "uuid": "^11.1.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^22.15.21",
24
+ "typescript": "^5.8.3",
25
+ "vitest": "^4.0.18"
26
+ },
27
+ "private": false,
28
+ "publishConfig": {
29
+ "access": "public"
30
+ }
31
+ }