@saasontools/strauss-kb 0.1.14 → 0.1.15

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/dist/index.d.cts CHANGED
@@ -187,12 +187,14 @@ type KbRecord = {
187
187
  body: string;
188
188
  };
189
189
 
190
+ /** Where bare mirrors live. Overridable so a test never writes to `$HOME`. */
191
+ declare function repoCacheDir(override?: string): string;
192
+
190
193
  /**
191
- * Resolves symbolic anchors to text and detects drift against stored hashes.
194
+ * The vocabulary anchor resolution is reported in.
192
195
  *
193
- * Resolvers are pure source string in, range out; only the file readers touch
194
- * disk. Any shape the lexer cannot handle confidently returns `null` and is
195
- * reported `unresolved` rather than guessed at.
196
+ * Every failure is a finding with a reason, never a throw: a record pointing
197
+ * at code that moved is information, not a broken run.
196
198
  */
197
199
  type ResolvedSymbol = {
198
200
  text: string;
@@ -206,36 +208,22 @@ interface AnchorResolver {
206
208
  }
207
209
  /** Why an anchor could not be compared. Never an error — always a finding. */
208
210
  type AnchorUnresolvedReason = "file-missing" | "symbol-not-found" | "outside-repo" | "file-too-large" | "file-unreadable"
209
- /**
210
- * The anchor names a repository this root is not. Expected rather than
211
- * wrong a base describing several repositories resolves against one tree
212
- * at a time — so it is never a drift finding and never reaches a reader as
213
- * a warning. Multi-root resolution is SAA-709.
214
- */
215
- | "foreign-repo";
216
- /**
217
- * v1 heuristic resolver. A dotted symbol like `OrderService.cancel` matches on
218
- * its last segment, with the parent used to scope the search: a candidate
219
- * counts only if the parent name appears in the fifty lines above it, when any
220
- * candidate satisfies that at all.
221
- *
222
- * Deterministic, and ambiguity is not resolved by guessing two lines of
223
- * equally good shape mean the resolver cannot tell which one the record meant,
224
- * and it says so by returning `null`.
225
- */
226
- declare const regexResolver: AnchorResolver;
227
- /** CRLF normalized to LF before hashing, so checkout style cannot read as drift. */
228
- declare function hashAnchorText(text: string): string;
229
- /**
230
- * An anchor without a symbol is about the whole file; with one, the resolver
231
- * decides. Source newlines are normalized first so line counts and hashes
232
- * agree with `hashAnchorText`.
233
- *
234
- * A file's last line is the last line with content: a trailing newline is a
235
- * terminator, not an empty line, and counting it would have made every
236
- * whole-file anchor's `lines` one larger than the file.
237
- */
238
- declare function resolveAnchor(source: string, anchor: KbAnchor, resolver?: AnchorResolver): ResolvedSymbol | null;
211
+ /** The remote could not be fetched, or `--offline` found nothing cached. */
212
+ | "remote-unreachable"
213
+ /** The anchor's `ref` is not on the remote any more. */
214
+ | "ref-not-found" | "repo-unauthorized"
215
+ /** No default branch, so there is no "current" to compare against. */
216
+ | "default-branch-unknown"
217
+ /** The anchor's `ref` is not a name git may safely be handed. */
218
+ | "ref-invalid"
219
+ /** The anchor's `repo` is not a remote we will fetch from. */
220
+ | "repo-invalid";
221
+ /**
222
+ * How a ref-pinned foreign anchor stands. `drifted-on-default` is the one a
223
+ * working-tree anchor has no equivalent of: the evidence is still true at the
224
+ * commit it was taken from, and the code has moved since.
225
+ */
226
+ type RemoteAnchorState = "matches-ref" | "drifted-from-ref" | "drifted-on-default";
239
227
  type KbAnchorDriftEntry = {
240
228
  file: string;
241
229
  symbol?: string;
@@ -245,17 +233,10 @@ type KbAnchorDriftEntry = {
245
233
  /** `null` when the anchor recorded no `lines` — size unknown, not zero. */
246
234
  diffSize: number | null;
247
235
  reason?: AnchorUnresolvedReason;
236
+ /** Set only when the anchor was resolved against another repository. */
237
+ repo?: string;
238
+ remoteState?: RemoteAnchorState;
248
239
  };
249
- /**
250
- * An anchor's `file` must stay inside the repository root — a record points
251
- * at code, not at arbitrary files on the machine reading it. Bundles are
252
- * data, so a traversal or absolute path here is untrusted input, not a bug
253
- * in the caller. Returns the resolved path, or `null` when it escapes.
254
- *
255
- * Lexical only, and therefore not the whole containment check: see
256
- * `readAnchorFile`, which re-tests the real path after following symlinks.
257
- */
258
- declare function anchorFilePath(repoRoot: string, file: string): string | null;
259
240
  type AnchorRead = {
260
241
  ok: true;
261
242
  source: string;
@@ -264,26 +245,109 @@ type AnchorRead = {
264
245
  reason: AnchorUnresolvedReason;
265
246
  };
266
247
  type AnchorFileReader = (file: string) => Promise<AnchorRead>;
248
+
267
249
  /**
268
- * Re-resolves every hash-carrying anchor and compares against the stored hash.
269
- *
270
- * Anchors without a `hash` are skipped; one naming another repository is
271
- * reported `foreign-repo` and never read, and `origin` is asked for once per
272
- * run, only when some anchor declares a `repo`. A missing file or unresolvable
273
- * symbol is a finding (`unresolved`), never a throw. Each distinct file is read
274
- * once per run; all checked entries are returned per record, callers filter.
250
+ * A foreign anchor's file at one rev. `ref` absent means the remote's default
251
+ * branch — the "current" side of a ref-pinned comparison.
252
+ */
253
+ type RemoteWant = {
254
+ repo: string;
255
+ ref?: string;
256
+ file: string;
257
+ };
258
+ type RemoteRead = {
259
+ ok: true;
260
+ source: string;
261
+ } | {
262
+ ok: false;
263
+ reason: AnchorUnresolvedReason;
264
+ };
265
+ type RemoteOptions = {
266
+ /** Cache only: no `fetch`, no `ls-remote`. */
267
+ offline?: boolean;
268
+ cacheDir?: string;
269
+ fetchTimeoutMs?: number;
270
+ /** Repositories worked on at once; fetches within one repo stay serial. */
271
+ concurrency?: number;
272
+ };
273
+
274
+ /**
275
+ * Reads each wanted (repo, rev, file) out of a bare cache under
276
+ * `~/.strauss/repo-cache`, fetching once per (repo, rev) and never per anchor.
275
277
  *
276
- * Three phases: collect the checkable anchors, read their distinct files with
277
- * a bounded pool, then resolve and hash in record order — so the output does
278
- * not depend on which read finished first.
278
+ * Every failure lands as an `unresolved` reason on the wants it affects; no
279
+ * path is read from disk, so containment does not apply here.
279
280
  */
280
- declare function detectAnchorDrift(records: KbRecord[], options?: {
281
+ declare function readRemoteAnchors(wants: readonly RemoteWant[], options?: RemoteOptions): Promise<Map<string, RemoteRead>>;
282
+
283
+ type AnchorDriftOptions = {
281
284
  repoRoot?: string;
282
285
  resolver?: AnchorResolver;
283
286
  concurrency?: number;
284
287
  /** Test seam: replaces the disk reader. */
285
288
  reader?: AnchorFileReader;
286
- }): Promise<Map<string, KbAnchorDriftEntry[]>>;
289
+ /** Remote resolution of foreign anchors; `offline` keeps a run off the wire. */
290
+ remote?: RemoteOptions;
291
+ /** Test seam: replaces the remote blob reader. */
292
+ readRemote?: typeof readRemoteAnchors;
293
+ };
294
+ /**
295
+ * Re-resolves every hash-carrying anchor and compares against the stored hash.
296
+ *
297
+ * An anchor naming another repository is read from that repository's remote
298
+ * through a bare cache; everything else is read from the working tree. A
299
+ * missing file, an unreachable remote, or an unresolvable symbol is a finding
300
+ * (`unresolved`), never a throw.
301
+ *
302
+ * Four phases: collect the checkable anchors, read the working tree's distinct
303
+ * files and the remotes' distinct (repo, rev, file) blobs, then resolve and
304
+ * hash in record order — so the output never depends on which read finished
305
+ * first.
306
+ */
307
+ declare function detectAnchorDrift(records: KbRecord[], options?: AnchorDriftOptions): Promise<Map<string, KbAnchorDriftEntry[]>>;
308
+
309
+ /**
310
+ * An anchor's `file` must stay inside the repository root — a record points
311
+ * at code, not at arbitrary files on the machine reading it. Bundles are
312
+ * data, so a traversal or absolute path here is untrusted input, not a bug
313
+ * in the caller. Returns the resolved path, or `null` when it escapes.
314
+ *
315
+ * Lexical only, and therefore not the whole containment check: see
316
+ * `readAnchorFile`, which re-tests the real path after following symlinks.
317
+ */
318
+ declare function anchorFilePath(repoRoot: string, file: string): string | null;
319
+
320
+ /**
321
+ * Does this spelling say where the repository lives?
322
+ *
323
+ * Only a full URL can be fetched from. A short form still matches this root's
324
+ * own origin, which is why `validate` warns rather than rejects.
325
+ */
326
+ declare function isCanonicalRepoUrl(value: string): boolean;
327
+
328
+ /**
329
+ * v1 heuristic resolver. A dotted symbol like `OrderService.cancel` matches on
330
+ * its last segment, with the parent used to scope the search: a candidate
331
+ * counts only if the parent name appears in the fifty lines above it, when any
332
+ * candidate satisfies that at all.
333
+ *
334
+ * Deterministic, and ambiguity is not resolved by guessing — two lines of
335
+ * equally good shape mean the resolver cannot tell which one the record meant,
336
+ * and it says so by returning `null`.
337
+ */
338
+ declare const regexResolver: AnchorResolver;
339
+ /** CRLF normalized to LF before hashing, so checkout style cannot read as drift. */
340
+ declare function hashAnchorText(text: string): string;
341
+ /**
342
+ * An anchor without a symbol is about the whole file; with one, the resolver
343
+ * decides. Source newlines are normalized first so line counts and hashes
344
+ * agree with `hashAnchorText`.
345
+ *
346
+ * A file's last line is the last line with content: a trailing newline is a
347
+ * terminator, not an empty line, and counting it would have made every
348
+ * whole-file anchor's `lines` one larger than the file.
349
+ */
350
+ declare function resolveAnchor(source: string, anchor: KbAnchor, resolver?: AnchorResolver): ResolvedSymbol | null;
287
351
 
288
352
  /**
289
353
  * Why a matched record must not be read as a plain answer.
@@ -334,13 +398,23 @@ type KbWarning =
334
398
  * the record may describe code that no longer exists in that form. */
335
399
  | {
336
400
  kind: "drifted";
337
- anchors: {
338
- file: string;
339
- symbol?: string;
340
- /** `null` when the anchor recorded no line count size unknown. */
341
- diffSize: number | null;
342
- reason?: string;
343
- }[];
401
+ anchors: KbWarningAnchor[];
402
+ }
403
+ /** A foreign anchor nothing could check: the remote was unreachable, refused,
404
+ * or the run was offline. Neither drift nor a clean match. */
405
+ | {
406
+ kind: "unchecked";
407
+ anchors: KbWarningAnchor[];
408
+ };
409
+ type KbWarningAnchor = {
410
+ file: string;
411
+ symbol?: string;
412
+ /** `null` when the anchor recorded no line count — size unknown. */
413
+ diffSize: number | null;
414
+ reason?: string;
415
+ /** Set only for an anchor resolved against another repository. */
416
+ repo?: string;
417
+ remoteState?: string;
344
418
  };
345
419
  type KbStanding = "current" | "superseded" | "rejected" | "unsettled" | "open";
346
420
  type KbAdjudicated = {
@@ -858,7 +932,8 @@ declare class KbStore {
858
932
  * at the repo root, and the MCP server's cwd is the workspace.
859
933
  *
860
934
  * Public because `doctor` needs the same map with the same degradation: a
861
- * sweep that failed to read the tree should report no drift, not fail.
935
+ * sweep that failed to read the tree should report no drift, not fail — and
936
+ * `offline: false` there, because a sweep is worth a fetch.
862
937
  *
863
938
  * When no root was given and not one anchored file was found, the finding is
864
939
  * discarded. A base read from somewhere other than the tree it describes
@@ -870,7 +945,9 @@ declare class KbStore {
870
945
  * plausible, and the misses become findings again; an explicit `repoRoot` is
871
946
  * taken at its word either way.
872
947
  */
873
- detectDrift(records: KbRecord[], repoRoot?: string): Promise<Map<string, KbAnchorDriftEntry[]> | undefined>;
948
+ detectDrift(records: KbRecord[], repoRoot?: string, options?: {
949
+ offline?: boolean;
950
+ }): Promise<Map<string, KbAnchorDriftEntry[]> | undefined>;
874
951
  /**
875
952
  * The whole base, adjudicated, when it is small enough to hand over.
876
953
  *
@@ -1727,7 +1804,7 @@ declare function validateBundle(records: KbRecord[]): KbValidationProblem[];
1727
1804
  declare const DEFAULT_EXPIRING_DAYS = 30;
1728
1805
  declare const DEFAULT_UNVERIFIED_DAYS = 90;
1729
1806
  declare const DEFAULT_AGING_DAYS = 90;
1730
- declare const KB_DOCTOR_CHECKS: readonly ["expired", "expiring", "unverified", "aging", "orphaned", "broken-supersession", "superseded-but-cited", "drifted"];
1807
+ declare const KB_DOCTOR_CHECKS: readonly ["expired", "expiring", "unverified", "aging", "orphaned", "broken-supersession", "superseded-but-cited", "drifted", "unchecked"];
1731
1808
  type KbDoctorCheck = (typeof KB_DOCTOR_CHECKS)[number];
1732
1809
  type KbDoctorFinding = {
1733
1810
  conceptId: string;
@@ -2071,4 +2148,4 @@ declare function parseMarkdownWithFrontmatter<S extends z.ZodType>(text: string,
2071
2148
  frontmatter: ReturnType<S["safeParse"]>;
2072
2149
  };
2073
2150
 
2074
- export { type AnchorResolver, BaseError, CONTEXT_BEGIN, CONTEXT_END, CONTEXT_PROFILES, type ComposeInput, type ComposeLink, type ComposedRecord, DECISION_TYPE, DEFAULT_AGING_DAYS, DEFAULT_EXPIRING_DAYS, DEFAULT_LOAD_BUDGET, DEFAULT_PACK_HOPS, DEFAULT_PACK_MAX_NODES, DEFAULT_TYPED_LINK_RELS, DEFAULT_UNVERIFIED_DAYS, type DecisionInput, type DiffFile, type DiffHunk, type DiffMatch, type ErrorDetails, type ErrorProps, ErrorTypes, Fault, INDEX_FILE, KB_CAUSAL_LINK_RELS, KB_COMMANDS, KB_COMMANDS_BY_NAME, KB_CONCEPT_ID_PATTERN, KB_CONFIDENCES, KB_DIR, KB_DOCTOR_CHECKS, KB_EDGE_KINDS, KB_LINK_RELS, KB_MATERIALITIES, KB_RECORD_STATUSES, KB_RECORD_TYPES, KB_SLUG_PATTERN, type KbActorStamp, type KbAdjudicated, type KbAnchor, type KbAnchorDriftEntry, type KbBacklink, type KbBacklinksResult, KbBaseFrozenError, type KbCatalogEntry, type KbCatalogResult, type KbCommand, type KbCommandContext, type KbContextBudgets, type KbContextOptions, type KbContextResult, type KbDoctorCheck, type KbDoctorFinding, type KbDoctorGroup, type KbDoctorOptions, type KbDoctorReport, type KbDoctorThresholds, type KbEdgeKind, type KbImpactOptions, type KbImpactResult, type KbImpactedRecord, type KbInboundEdge, KbInvalidConceptIdError, type KbLink, type KbLinkEdge, type KbLinkRel, type KbLinkRelSpec, type KbLoadResult, type KbLogEntry, type KbLogReadResult, type KbLogger, type KbMergedPin, type KbMergedPins, KbMissingFlagValueError, type KbNeighbour, KbPackBudgetExceededError, type KbPackOptions, type KbPackResult, type KbPackedRecord, type KbPin, type KbPinLayer, type KbPinOptions, type KbPinResult, type KbPinStatus, KbPinsMalformedError, type KbPinsManifest, type KbRecord, KbRecordAlreadyExistsError, type KbRecordFrontmatter, KbRecordNotFoundError, type KbRecordStatus, type KbRecordType, type KbRecordTypeSpec, type KbSearchLogger, KbSelfVerificationError, type KbSource, type KbStanding, KbStore, type KbSupersededStub, type KbSyncResult, type KbTraceEdge, type KbTraceOptions, type KbTraceStep, KbUnknownLinkRelError, type KbValidationProblem, type KbValidationSeverity, type KbVerifiedEvent, type KbWarning, KbWriteConflictError, type KbWriteInput, LINK_RELS, LOG_FILE, type MatchOptions, NO_DECISION_SLUG, PINS_FILE, PINS_LOCAL_FILE, PIN_LAYERS, type QmdModule, RECORD_TYPES, type ResolvedSymbol, SEARCH_INDEX_FILE, type SearchHit, type SearchOptions, type SymbolRange, TRACE_EDGES, adjudicate, anchorFilePath, assertBaseNotFrozen, backlinks, buildContext, catalog, composeDecisionRecord, composeInputSchema, composeLinkSchema, composeNoDecisionRecord, composeRecord, contextProfileBudgets, createKbMcpServer, decisionInputSchema, detectAnchorDrift, doctor, edgeNeighbours, hashAnchorText, impact, inboundIndex, indexIsStale, isKbLinkRel, isKbRecordType, isNoDecisionRecord, kbActorStampSchema, kbAnchorSchema, kbConceptIdSchema, kbJsonSchemas, kbLinkSchema, kbLogEntrySchema, kbRecordFrontmatterSchema, kbSourceSchema, kbVerifiedEventSchema, listPins, loadQmd, matchToDiff, mergedContextBudgets, neighbours, pack, parseLog, parseMarkdownWithFrontmatter, pinBase, readMergedPins, readPinsLayer, regexResolver, renderCatalogLine, renderIndex, renderIndexLine, renderLogEntry, resolveAnchor, resolveHeads, resolveHits, resolvePinPath, runKbCli, runKbMcpServer, searchBase, selectDecisions, splitMarkdownFrontmatter, stringifyMarkdownWithFrontmatter, syncInstructions, toHookJson, trace, unpinBase, validateBundle };
2151
+ export { type AnchorResolver, BaseError, CONTEXT_BEGIN, CONTEXT_END, CONTEXT_PROFILES, type ComposeInput, type ComposeLink, type ComposedRecord, DECISION_TYPE, DEFAULT_AGING_DAYS, DEFAULT_EXPIRING_DAYS, DEFAULT_LOAD_BUDGET, DEFAULT_PACK_HOPS, DEFAULT_PACK_MAX_NODES, DEFAULT_TYPED_LINK_RELS, DEFAULT_UNVERIFIED_DAYS, type DecisionInput, type DiffFile, type DiffHunk, type DiffMatch, type ErrorDetails, type ErrorProps, ErrorTypes, Fault, INDEX_FILE, KB_CAUSAL_LINK_RELS, KB_COMMANDS, KB_COMMANDS_BY_NAME, KB_CONCEPT_ID_PATTERN, KB_CONFIDENCES, KB_DIR, KB_DOCTOR_CHECKS, KB_EDGE_KINDS, KB_LINK_RELS, KB_MATERIALITIES, KB_RECORD_STATUSES, KB_RECORD_TYPES, KB_SLUG_PATTERN, type KbActorStamp, type KbAdjudicated, type KbAnchor, type KbAnchorDriftEntry, type KbBacklink, type KbBacklinksResult, KbBaseFrozenError, type KbCatalogEntry, type KbCatalogResult, type KbCommand, type KbCommandContext, type KbContextBudgets, type KbContextOptions, type KbContextResult, type KbDoctorCheck, type KbDoctorFinding, type KbDoctorGroup, type KbDoctorOptions, type KbDoctorReport, type KbDoctorThresholds, type KbEdgeKind, type KbImpactOptions, type KbImpactResult, type KbImpactedRecord, type KbInboundEdge, KbInvalidConceptIdError, type KbLink, type KbLinkEdge, type KbLinkRel, type KbLinkRelSpec, type KbLoadResult, type KbLogEntry, type KbLogReadResult, type KbLogger, type KbMergedPin, type KbMergedPins, KbMissingFlagValueError, type KbNeighbour, KbPackBudgetExceededError, type KbPackOptions, type KbPackResult, type KbPackedRecord, type KbPin, type KbPinLayer, type KbPinOptions, type KbPinResult, type KbPinStatus, KbPinsMalformedError, type KbPinsManifest, type KbRecord, KbRecordAlreadyExistsError, type KbRecordFrontmatter, KbRecordNotFoundError, type KbRecordStatus, type KbRecordType, type KbRecordTypeSpec, type KbSearchLogger, KbSelfVerificationError, type KbSource, type KbStanding, KbStore, type KbSupersededStub, type KbSyncResult, type KbTraceEdge, type KbTraceOptions, type KbTraceStep, KbUnknownLinkRelError, type KbValidationProblem, type KbValidationSeverity, type KbVerifiedEvent, type KbWarning, KbWriteConflictError, type KbWriteInput, LINK_RELS, LOG_FILE, type MatchOptions, NO_DECISION_SLUG, PINS_FILE, PINS_LOCAL_FILE, PIN_LAYERS, type QmdModule, RECORD_TYPES, type RemoteAnchorState, type RemoteOptions, type RemoteRead, type ResolvedSymbol, SEARCH_INDEX_FILE, type SearchHit, type SearchOptions, type SymbolRange, TRACE_EDGES, adjudicate, anchorFilePath, assertBaseNotFrozen, backlinks, buildContext, catalog, composeDecisionRecord, composeInputSchema, composeLinkSchema, composeNoDecisionRecord, composeRecord, contextProfileBudgets, createKbMcpServer, decisionInputSchema, detectAnchorDrift, doctor, edgeNeighbours, hashAnchorText, impact, inboundIndex, indexIsStale, isCanonicalRepoUrl, isKbLinkRel, isKbRecordType, isNoDecisionRecord, kbActorStampSchema, kbAnchorSchema, kbConceptIdSchema, kbJsonSchemas, kbLinkSchema, kbLogEntrySchema, kbRecordFrontmatterSchema, kbSourceSchema, kbVerifiedEventSchema, listPins, loadQmd, matchToDiff, mergedContextBudgets, neighbours, pack, parseLog, parseMarkdownWithFrontmatter, pinBase, readMergedPins, readPinsLayer, readRemoteAnchors, regexResolver, renderCatalogLine, renderIndex, renderIndexLine, renderLogEntry, repoCacheDir, resolveAnchor, resolveHeads, resolveHits, resolvePinPath, runKbCli, runKbMcpServer, searchBase, selectDecisions, splitMarkdownFrontmatter, stringifyMarkdownWithFrontmatter, syncInstructions, toHookJson, trace, unpinBase, validateBundle };
package/dist/index.d.ts CHANGED
@@ -187,12 +187,14 @@ type KbRecord = {
187
187
  body: string;
188
188
  };
189
189
 
190
+ /** Where bare mirrors live. Overridable so a test never writes to `$HOME`. */
191
+ declare function repoCacheDir(override?: string): string;
192
+
190
193
  /**
191
- * Resolves symbolic anchors to text and detects drift against stored hashes.
194
+ * The vocabulary anchor resolution is reported in.
192
195
  *
193
- * Resolvers are pure source string in, range out; only the file readers touch
194
- * disk. Any shape the lexer cannot handle confidently returns `null` and is
195
- * reported `unresolved` rather than guessed at.
196
+ * Every failure is a finding with a reason, never a throw: a record pointing
197
+ * at code that moved is information, not a broken run.
196
198
  */
197
199
  type ResolvedSymbol = {
198
200
  text: string;
@@ -206,36 +208,22 @@ interface AnchorResolver {
206
208
  }
207
209
  /** Why an anchor could not be compared. Never an error — always a finding. */
208
210
  type AnchorUnresolvedReason = "file-missing" | "symbol-not-found" | "outside-repo" | "file-too-large" | "file-unreadable"
209
- /**
210
- * The anchor names a repository this root is not. Expected rather than
211
- * wrong a base describing several repositories resolves against one tree
212
- * at a time — so it is never a drift finding and never reaches a reader as
213
- * a warning. Multi-root resolution is SAA-709.
214
- */
215
- | "foreign-repo";
216
- /**
217
- * v1 heuristic resolver. A dotted symbol like `OrderService.cancel` matches on
218
- * its last segment, with the parent used to scope the search: a candidate
219
- * counts only if the parent name appears in the fifty lines above it, when any
220
- * candidate satisfies that at all.
221
- *
222
- * Deterministic, and ambiguity is not resolved by guessing two lines of
223
- * equally good shape mean the resolver cannot tell which one the record meant,
224
- * and it says so by returning `null`.
225
- */
226
- declare const regexResolver: AnchorResolver;
227
- /** CRLF normalized to LF before hashing, so checkout style cannot read as drift. */
228
- declare function hashAnchorText(text: string): string;
229
- /**
230
- * An anchor without a symbol is about the whole file; with one, the resolver
231
- * decides. Source newlines are normalized first so line counts and hashes
232
- * agree with `hashAnchorText`.
233
- *
234
- * A file's last line is the last line with content: a trailing newline is a
235
- * terminator, not an empty line, and counting it would have made every
236
- * whole-file anchor's `lines` one larger than the file.
237
- */
238
- declare function resolveAnchor(source: string, anchor: KbAnchor, resolver?: AnchorResolver): ResolvedSymbol | null;
211
+ /** The remote could not be fetched, or `--offline` found nothing cached. */
212
+ | "remote-unreachable"
213
+ /** The anchor's `ref` is not on the remote any more. */
214
+ | "ref-not-found" | "repo-unauthorized"
215
+ /** No default branch, so there is no "current" to compare against. */
216
+ | "default-branch-unknown"
217
+ /** The anchor's `ref` is not a name git may safely be handed. */
218
+ | "ref-invalid"
219
+ /** The anchor's `repo` is not a remote we will fetch from. */
220
+ | "repo-invalid";
221
+ /**
222
+ * How a ref-pinned foreign anchor stands. `drifted-on-default` is the one a
223
+ * working-tree anchor has no equivalent of: the evidence is still true at the
224
+ * commit it was taken from, and the code has moved since.
225
+ */
226
+ type RemoteAnchorState = "matches-ref" | "drifted-from-ref" | "drifted-on-default";
239
227
  type KbAnchorDriftEntry = {
240
228
  file: string;
241
229
  symbol?: string;
@@ -245,17 +233,10 @@ type KbAnchorDriftEntry = {
245
233
  /** `null` when the anchor recorded no `lines` — size unknown, not zero. */
246
234
  diffSize: number | null;
247
235
  reason?: AnchorUnresolvedReason;
236
+ /** Set only when the anchor was resolved against another repository. */
237
+ repo?: string;
238
+ remoteState?: RemoteAnchorState;
248
239
  };
249
- /**
250
- * An anchor's `file` must stay inside the repository root — a record points
251
- * at code, not at arbitrary files on the machine reading it. Bundles are
252
- * data, so a traversal or absolute path here is untrusted input, not a bug
253
- * in the caller. Returns the resolved path, or `null` when it escapes.
254
- *
255
- * Lexical only, and therefore not the whole containment check: see
256
- * `readAnchorFile`, which re-tests the real path after following symlinks.
257
- */
258
- declare function anchorFilePath(repoRoot: string, file: string): string | null;
259
240
  type AnchorRead = {
260
241
  ok: true;
261
242
  source: string;
@@ -264,26 +245,109 @@ type AnchorRead = {
264
245
  reason: AnchorUnresolvedReason;
265
246
  };
266
247
  type AnchorFileReader = (file: string) => Promise<AnchorRead>;
248
+
267
249
  /**
268
- * Re-resolves every hash-carrying anchor and compares against the stored hash.
269
- *
270
- * Anchors without a `hash` are skipped; one naming another repository is
271
- * reported `foreign-repo` and never read, and `origin` is asked for once per
272
- * run, only when some anchor declares a `repo`. A missing file or unresolvable
273
- * symbol is a finding (`unresolved`), never a throw. Each distinct file is read
274
- * once per run; all checked entries are returned per record, callers filter.
250
+ * A foreign anchor's file at one rev. `ref` absent means the remote's default
251
+ * branch — the "current" side of a ref-pinned comparison.
252
+ */
253
+ type RemoteWant = {
254
+ repo: string;
255
+ ref?: string;
256
+ file: string;
257
+ };
258
+ type RemoteRead = {
259
+ ok: true;
260
+ source: string;
261
+ } | {
262
+ ok: false;
263
+ reason: AnchorUnresolvedReason;
264
+ };
265
+ type RemoteOptions = {
266
+ /** Cache only: no `fetch`, no `ls-remote`. */
267
+ offline?: boolean;
268
+ cacheDir?: string;
269
+ fetchTimeoutMs?: number;
270
+ /** Repositories worked on at once; fetches within one repo stay serial. */
271
+ concurrency?: number;
272
+ };
273
+
274
+ /**
275
+ * Reads each wanted (repo, rev, file) out of a bare cache under
276
+ * `~/.strauss/repo-cache`, fetching once per (repo, rev) and never per anchor.
275
277
  *
276
- * Three phases: collect the checkable anchors, read their distinct files with
277
- * a bounded pool, then resolve and hash in record order — so the output does
278
- * not depend on which read finished first.
278
+ * Every failure lands as an `unresolved` reason on the wants it affects; no
279
+ * path is read from disk, so containment does not apply here.
279
280
  */
280
- declare function detectAnchorDrift(records: KbRecord[], options?: {
281
+ declare function readRemoteAnchors(wants: readonly RemoteWant[], options?: RemoteOptions): Promise<Map<string, RemoteRead>>;
282
+
283
+ type AnchorDriftOptions = {
281
284
  repoRoot?: string;
282
285
  resolver?: AnchorResolver;
283
286
  concurrency?: number;
284
287
  /** Test seam: replaces the disk reader. */
285
288
  reader?: AnchorFileReader;
286
- }): Promise<Map<string, KbAnchorDriftEntry[]>>;
289
+ /** Remote resolution of foreign anchors; `offline` keeps a run off the wire. */
290
+ remote?: RemoteOptions;
291
+ /** Test seam: replaces the remote blob reader. */
292
+ readRemote?: typeof readRemoteAnchors;
293
+ };
294
+ /**
295
+ * Re-resolves every hash-carrying anchor and compares against the stored hash.
296
+ *
297
+ * An anchor naming another repository is read from that repository's remote
298
+ * through a bare cache; everything else is read from the working tree. A
299
+ * missing file, an unreachable remote, or an unresolvable symbol is a finding
300
+ * (`unresolved`), never a throw.
301
+ *
302
+ * Four phases: collect the checkable anchors, read the working tree's distinct
303
+ * files and the remotes' distinct (repo, rev, file) blobs, then resolve and
304
+ * hash in record order — so the output never depends on which read finished
305
+ * first.
306
+ */
307
+ declare function detectAnchorDrift(records: KbRecord[], options?: AnchorDriftOptions): Promise<Map<string, KbAnchorDriftEntry[]>>;
308
+
309
+ /**
310
+ * An anchor's `file` must stay inside the repository root — a record points
311
+ * at code, not at arbitrary files on the machine reading it. Bundles are
312
+ * data, so a traversal or absolute path here is untrusted input, not a bug
313
+ * in the caller. Returns the resolved path, or `null` when it escapes.
314
+ *
315
+ * Lexical only, and therefore not the whole containment check: see
316
+ * `readAnchorFile`, which re-tests the real path after following symlinks.
317
+ */
318
+ declare function anchorFilePath(repoRoot: string, file: string): string | null;
319
+
320
+ /**
321
+ * Does this spelling say where the repository lives?
322
+ *
323
+ * Only a full URL can be fetched from. A short form still matches this root's
324
+ * own origin, which is why `validate` warns rather than rejects.
325
+ */
326
+ declare function isCanonicalRepoUrl(value: string): boolean;
327
+
328
+ /**
329
+ * v1 heuristic resolver. A dotted symbol like `OrderService.cancel` matches on
330
+ * its last segment, with the parent used to scope the search: a candidate
331
+ * counts only if the parent name appears in the fifty lines above it, when any
332
+ * candidate satisfies that at all.
333
+ *
334
+ * Deterministic, and ambiguity is not resolved by guessing — two lines of
335
+ * equally good shape mean the resolver cannot tell which one the record meant,
336
+ * and it says so by returning `null`.
337
+ */
338
+ declare const regexResolver: AnchorResolver;
339
+ /** CRLF normalized to LF before hashing, so checkout style cannot read as drift. */
340
+ declare function hashAnchorText(text: string): string;
341
+ /**
342
+ * An anchor without a symbol is about the whole file; with one, the resolver
343
+ * decides. Source newlines are normalized first so line counts and hashes
344
+ * agree with `hashAnchorText`.
345
+ *
346
+ * A file's last line is the last line with content: a trailing newline is a
347
+ * terminator, not an empty line, and counting it would have made every
348
+ * whole-file anchor's `lines` one larger than the file.
349
+ */
350
+ declare function resolveAnchor(source: string, anchor: KbAnchor, resolver?: AnchorResolver): ResolvedSymbol | null;
287
351
 
288
352
  /**
289
353
  * Why a matched record must not be read as a plain answer.
@@ -334,13 +398,23 @@ type KbWarning =
334
398
  * the record may describe code that no longer exists in that form. */
335
399
  | {
336
400
  kind: "drifted";
337
- anchors: {
338
- file: string;
339
- symbol?: string;
340
- /** `null` when the anchor recorded no line count size unknown. */
341
- diffSize: number | null;
342
- reason?: string;
343
- }[];
401
+ anchors: KbWarningAnchor[];
402
+ }
403
+ /** A foreign anchor nothing could check: the remote was unreachable, refused,
404
+ * or the run was offline. Neither drift nor a clean match. */
405
+ | {
406
+ kind: "unchecked";
407
+ anchors: KbWarningAnchor[];
408
+ };
409
+ type KbWarningAnchor = {
410
+ file: string;
411
+ symbol?: string;
412
+ /** `null` when the anchor recorded no line count — size unknown. */
413
+ diffSize: number | null;
414
+ reason?: string;
415
+ /** Set only for an anchor resolved against another repository. */
416
+ repo?: string;
417
+ remoteState?: string;
344
418
  };
345
419
  type KbStanding = "current" | "superseded" | "rejected" | "unsettled" | "open";
346
420
  type KbAdjudicated = {
@@ -858,7 +932,8 @@ declare class KbStore {
858
932
  * at the repo root, and the MCP server's cwd is the workspace.
859
933
  *
860
934
  * Public because `doctor` needs the same map with the same degradation: a
861
- * sweep that failed to read the tree should report no drift, not fail.
935
+ * sweep that failed to read the tree should report no drift, not fail — and
936
+ * `offline: false` there, because a sweep is worth a fetch.
862
937
  *
863
938
  * When no root was given and not one anchored file was found, the finding is
864
939
  * discarded. A base read from somewhere other than the tree it describes
@@ -870,7 +945,9 @@ declare class KbStore {
870
945
  * plausible, and the misses become findings again; an explicit `repoRoot` is
871
946
  * taken at its word either way.
872
947
  */
873
- detectDrift(records: KbRecord[], repoRoot?: string): Promise<Map<string, KbAnchorDriftEntry[]> | undefined>;
948
+ detectDrift(records: KbRecord[], repoRoot?: string, options?: {
949
+ offline?: boolean;
950
+ }): Promise<Map<string, KbAnchorDriftEntry[]> | undefined>;
874
951
  /**
875
952
  * The whole base, adjudicated, when it is small enough to hand over.
876
953
  *
@@ -1727,7 +1804,7 @@ declare function validateBundle(records: KbRecord[]): KbValidationProblem[];
1727
1804
  declare const DEFAULT_EXPIRING_DAYS = 30;
1728
1805
  declare const DEFAULT_UNVERIFIED_DAYS = 90;
1729
1806
  declare const DEFAULT_AGING_DAYS = 90;
1730
- declare const KB_DOCTOR_CHECKS: readonly ["expired", "expiring", "unverified", "aging", "orphaned", "broken-supersession", "superseded-but-cited", "drifted"];
1807
+ declare const KB_DOCTOR_CHECKS: readonly ["expired", "expiring", "unverified", "aging", "orphaned", "broken-supersession", "superseded-but-cited", "drifted", "unchecked"];
1731
1808
  type KbDoctorCheck = (typeof KB_DOCTOR_CHECKS)[number];
1732
1809
  type KbDoctorFinding = {
1733
1810
  conceptId: string;
@@ -2071,4 +2148,4 @@ declare function parseMarkdownWithFrontmatter<S extends z.ZodType>(text: string,
2071
2148
  frontmatter: ReturnType<S["safeParse"]>;
2072
2149
  };
2073
2150
 
2074
- export { type AnchorResolver, BaseError, CONTEXT_BEGIN, CONTEXT_END, CONTEXT_PROFILES, type ComposeInput, type ComposeLink, type ComposedRecord, DECISION_TYPE, DEFAULT_AGING_DAYS, DEFAULT_EXPIRING_DAYS, DEFAULT_LOAD_BUDGET, DEFAULT_PACK_HOPS, DEFAULT_PACK_MAX_NODES, DEFAULT_TYPED_LINK_RELS, DEFAULT_UNVERIFIED_DAYS, type DecisionInput, type DiffFile, type DiffHunk, type DiffMatch, type ErrorDetails, type ErrorProps, ErrorTypes, Fault, INDEX_FILE, KB_CAUSAL_LINK_RELS, KB_COMMANDS, KB_COMMANDS_BY_NAME, KB_CONCEPT_ID_PATTERN, KB_CONFIDENCES, KB_DIR, KB_DOCTOR_CHECKS, KB_EDGE_KINDS, KB_LINK_RELS, KB_MATERIALITIES, KB_RECORD_STATUSES, KB_RECORD_TYPES, KB_SLUG_PATTERN, type KbActorStamp, type KbAdjudicated, type KbAnchor, type KbAnchorDriftEntry, type KbBacklink, type KbBacklinksResult, KbBaseFrozenError, type KbCatalogEntry, type KbCatalogResult, type KbCommand, type KbCommandContext, type KbContextBudgets, type KbContextOptions, type KbContextResult, type KbDoctorCheck, type KbDoctorFinding, type KbDoctorGroup, type KbDoctorOptions, type KbDoctorReport, type KbDoctorThresholds, type KbEdgeKind, type KbImpactOptions, type KbImpactResult, type KbImpactedRecord, type KbInboundEdge, KbInvalidConceptIdError, type KbLink, type KbLinkEdge, type KbLinkRel, type KbLinkRelSpec, type KbLoadResult, type KbLogEntry, type KbLogReadResult, type KbLogger, type KbMergedPin, type KbMergedPins, KbMissingFlagValueError, type KbNeighbour, KbPackBudgetExceededError, type KbPackOptions, type KbPackResult, type KbPackedRecord, type KbPin, type KbPinLayer, type KbPinOptions, type KbPinResult, type KbPinStatus, KbPinsMalformedError, type KbPinsManifest, type KbRecord, KbRecordAlreadyExistsError, type KbRecordFrontmatter, KbRecordNotFoundError, type KbRecordStatus, type KbRecordType, type KbRecordTypeSpec, type KbSearchLogger, KbSelfVerificationError, type KbSource, type KbStanding, KbStore, type KbSupersededStub, type KbSyncResult, type KbTraceEdge, type KbTraceOptions, type KbTraceStep, KbUnknownLinkRelError, type KbValidationProblem, type KbValidationSeverity, type KbVerifiedEvent, type KbWarning, KbWriteConflictError, type KbWriteInput, LINK_RELS, LOG_FILE, type MatchOptions, NO_DECISION_SLUG, PINS_FILE, PINS_LOCAL_FILE, PIN_LAYERS, type QmdModule, RECORD_TYPES, type ResolvedSymbol, SEARCH_INDEX_FILE, type SearchHit, type SearchOptions, type SymbolRange, TRACE_EDGES, adjudicate, anchorFilePath, assertBaseNotFrozen, backlinks, buildContext, catalog, composeDecisionRecord, composeInputSchema, composeLinkSchema, composeNoDecisionRecord, composeRecord, contextProfileBudgets, createKbMcpServer, decisionInputSchema, detectAnchorDrift, doctor, edgeNeighbours, hashAnchorText, impact, inboundIndex, indexIsStale, isKbLinkRel, isKbRecordType, isNoDecisionRecord, kbActorStampSchema, kbAnchorSchema, kbConceptIdSchema, kbJsonSchemas, kbLinkSchema, kbLogEntrySchema, kbRecordFrontmatterSchema, kbSourceSchema, kbVerifiedEventSchema, listPins, loadQmd, matchToDiff, mergedContextBudgets, neighbours, pack, parseLog, parseMarkdownWithFrontmatter, pinBase, readMergedPins, readPinsLayer, regexResolver, renderCatalogLine, renderIndex, renderIndexLine, renderLogEntry, resolveAnchor, resolveHeads, resolveHits, resolvePinPath, runKbCli, runKbMcpServer, searchBase, selectDecisions, splitMarkdownFrontmatter, stringifyMarkdownWithFrontmatter, syncInstructions, toHookJson, trace, unpinBase, validateBundle };
2151
+ export { type AnchorResolver, BaseError, CONTEXT_BEGIN, CONTEXT_END, CONTEXT_PROFILES, type ComposeInput, type ComposeLink, type ComposedRecord, DECISION_TYPE, DEFAULT_AGING_DAYS, DEFAULT_EXPIRING_DAYS, DEFAULT_LOAD_BUDGET, DEFAULT_PACK_HOPS, DEFAULT_PACK_MAX_NODES, DEFAULT_TYPED_LINK_RELS, DEFAULT_UNVERIFIED_DAYS, type DecisionInput, type DiffFile, type DiffHunk, type DiffMatch, type ErrorDetails, type ErrorProps, ErrorTypes, Fault, INDEX_FILE, KB_CAUSAL_LINK_RELS, KB_COMMANDS, KB_COMMANDS_BY_NAME, KB_CONCEPT_ID_PATTERN, KB_CONFIDENCES, KB_DIR, KB_DOCTOR_CHECKS, KB_EDGE_KINDS, KB_LINK_RELS, KB_MATERIALITIES, KB_RECORD_STATUSES, KB_RECORD_TYPES, KB_SLUG_PATTERN, type KbActorStamp, type KbAdjudicated, type KbAnchor, type KbAnchorDriftEntry, type KbBacklink, type KbBacklinksResult, KbBaseFrozenError, type KbCatalogEntry, type KbCatalogResult, type KbCommand, type KbCommandContext, type KbContextBudgets, type KbContextOptions, type KbContextResult, type KbDoctorCheck, type KbDoctorFinding, type KbDoctorGroup, type KbDoctorOptions, type KbDoctorReport, type KbDoctorThresholds, type KbEdgeKind, type KbImpactOptions, type KbImpactResult, type KbImpactedRecord, type KbInboundEdge, KbInvalidConceptIdError, type KbLink, type KbLinkEdge, type KbLinkRel, type KbLinkRelSpec, type KbLoadResult, type KbLogEntry, type KbLogReadResult, type KbLogger, type KbMergedPin, type KbMergedPins, KbMissingFlagValueError, type KbNeighbour, KbPackBudgetExceededError, type KbPackOptions, type KbPackResult, type KbPackedRecord, type KbPin, type KbPinLayer, type KbPinOptions, type KbPinResult, type KbPinStatus, KbPinsMalformedError, type KbPinsManifest, type KbRecord, KbRecordAlreadyExistsError, type KbRecordFrontmatter, KbRecordNotFoundError, type KbRecordStatus, type KbRecordType, type KbRecordTypeSpec, type KbSearchLogger, KbSelfVerificationError, type KbSource, type KbStanding, KbStore, type KbSupersededStub, type KbSyncResult, type KbTraceEdge, type KbTraceOptions, type KbTraceStep, KbUnknownLinkRelError, type KbValidationProblem, type KbValidationSeverity, type KbVerifiedEvent, type KbWarning, KbWriteConflictError, type KbWriteInput, LINK_RELS, LOG_FILE, type MatchOptions, NO_DECISION_SLUG, PINS_FILE, PINS_LOCAL_FILE, PIN_LAYERS, type QmdModule, RECORD_TYPES, type RemoteAnchorState, type RemoteOptions, type RemoteRead, type ResolvedSymbol, SEARCH_INDEX_FILE, type SearchHit, type SearchOptions, type SymbolRange, TRACE_EDGES, adjudicate, anchorFilePath, assertBaseNotFrozen, backlinks, buildContext, catalog, composeDecisionRecord, composeInputSchema, composeLinkSchema, composeNoDecisionRecord, composeRecord, contextProfileBudgets, createKbMcpServer, decisionInputSchema, detectAnchorDrift, doctor, edgeNeighbours, hashAnchorText, impact, inboundIndex, indexIsStale, isCanonicalRepoUrl, isKbLinkRel, isKbRecordType, isNoDecisionRecord, kbActorStampSchema, kbAnchorSchema, kbConceptIdSchema, kbJsonSchemas, kbLinkSchema, kbLogEntrySchema, kbRecordFrontmatterSchema, kbSourceSchema, kbVerifiedEventSchema, listPins, loadQmd, matchToDiff, mergedContextBudgets, neighbours, pack, parseLog, parseMarkdownWithFrontmatter, pinBase, readMergedPins, readPinsLayer, readRemoteAnchors, regexResolver, renderCatalogLine, renderIndex, renderIndexLine, renderLogEntry, repoCacheDir, resolveAnchor, resolveHeads, resolveHits, resolvePinPath, runKbCli, runKbMcpServer, searchBase, selectDecisions, splitMarkdownFrontmatter, stringifyMarkdownWithFrontmatter, syncInstructions, toHookJson, trace, unpinBase, validateBundle };
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  runKbCli
3
- } from "./chunk-MBXNCZ4V.js";
3
+ } from "./chunk-MK7GU4DX.js";
4
4
  import {
5
5
  createKbMcpServer,
6
6
  runKbMcpServer
7
- } from "./chunk-PYA5E7FL.js";
7
+ } from "./chunk-LACCRB2Y.js";
8
8
  import {
9
9
  BaseError,
10
10
  CONTEXT_BEGIN,
@@ -74,6 +74,7 @@ import {
74
74
  impact,
75
75
  inboundIndex,
76
76
  indexIsStale,
77
+ isCanonicalRepoUrl,
77
78
  isKbLinkRel,
78
79
  isKbRecordType,
79
80
  isNoDecisionRecord,
@@ -96,11 +97,13 @@ import {
96
97
  pinBase,
97
98
  readMergedPins,
98
99
  readPinsLayer,
100
+ readRemoteAnchors,
99
101
  regexResolver,
100
102
  renderCatalogLine,
101
103
  renderIndex,
102
104
  renderIndexLine,
103
105
  renderLogEntry,
106
+ repoCacheDir,
104
107
  resolveAnchor,
105
108
  resolveHeads,
106
109
  resolveHits,
@@ -114,7 +117,7 @@ import {
114
117
  trace,
115
118
  unpinBase,
116
119
  validateBundle
117
- } from "./chunk-43KALLFU.js";
120
+ } from "./chunk-KNIUBCZY.js";
118
121
 
119
122
  // src/match-diff.ts
120
123
  function matchToDiff(files, records, options = {}) {
@@ -265,6 +268,7 @@ export {
265
268
  impact,
266
269
  inboundIndex,
267
270
  indexIsStale,
271
+ isCanonicalRepoUrl,
268
272
  isKbLinkRel,
269
273
  isKbRecordType,
270
274
  isNoDecisionRecord,
@@ -288,11 +292,13 @@ export {
288
292
  pinBase,
289
293
  readMergedPins,
290
294
  readPinsLayer,
295
+ readRemoteAnchors,
291
296
  regexResolver,
292
297
  renderCatalogLine,
293
298
  renderIndex,
294
299
  renderIndexLine,
295
300
  renderLogEntry,
301
+ repoCacheDir,
296
302
  resolveAnchor,
297
303
  resolveHeads,
298
304
  resolveHits,