@gmickel/gno 2.13.0 → 2.14.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.
@@ -33,10 +33,16 @@ const POLICY_RESTRICTIVENESS: Readonly<Record<EgressPolicy, number>> = {
33
33
  };
34
34
 
35
35
  export class EgressProvenanceError extends Error {
36
- readonly code: "EMPTY_EGRESS_LINEAGE" | "INVALID_EGRESS_LINEAGE";
36
+ readonly code:
37
+ | "EMPTY_EGRESS_LINEAGE"
38
+ | "INVALID_EGRESS_LINEAGE"
39
+ | "EGRESS_SCOPE_LIMIT_EXCEEDED";
37
40
 
38
41
  constructor(
39
- code: "EMPTY_EGRESS_LINEAGE" | "INVALID_EGRESS_LINEAGE",
42
+ code:
43
+ | "EMPTY_EGRESS_LINEAGE"
44
+ | "INVALID_EGRESS_LINEAGE"
45
+ | "EGRESS_SCOPE_LIMIT_EXCEEDED",
40
46
  message: string
41
47
  ) {
42
48
  super(message);
@@ -71,13 +77,24 @@ const invalidLineageInput = (): EgressProvenanceError =>
71
77
  `Source policy lineage must be a readable dense array with at most ${MAX_EGRESS_LINEAGE_SOURCES} entries`
72
78
  );
73
79
 
80
+ const scopeLimitExceeded = (): EgressProvenanceError =>
81
+ new EgressProvenanceError(
82
+ "EGRESS_SCOPE_LIMIT_EXCEEDED",
83
+ `Collection policy scope exceeds ${MAX_EGRESS_LINEAGE_SOURCES} collections`
84
+ );
85
+
86
+ /**
87
+ * `maxSources` bounds a lineage. Configured sources a scope is resolved from
88
+ * are unbounded: only the resolved scope must fit (resolveEgressLineage).
89
+ */
74
90
  const snapshotInputs = (
75
- inputs: readonly EgressLineageInput[]
91
+ inputs: readonly EgressLineageInput[],
92
+ maxSources: number
76
93
  ): readonly EgressLineageInput[] => {
77
94
  try {
78
95
  if (!Array.isArray(inputs)) throw invalidLineageInput();
79
96
  const { length } = inputs;
80
- if (!Number.isSafeInteger(length) || length > MAX_EGRESS_LINEAGE_SOURCES) {
97
+ if (!Number.isSafeInteger(length) || length > maxSources) {
81
98
  throw invalidLineageInput();
82
99
  }
83
100
  if (length === 0) {
@@ -109,9 +126,10 @@ const snapshotInputs = (
109
126
  };
110
127
 
111
128
  const canonicalSources = (
112
- inputs: readonly EgressLineageInput[]
129
+ inputs: readonly EgressLineageInput[],
130
+ maxSources = MAX_EGRESS_LINEAGE_SOURCES
113
131
  ): readonly EgressLineageSource[] => {
114
- const snapshots = snapshotInputs(inputs);
132
+ const snapshots = snapshotInputs(inputs, maxSources);
115
133
  const byCollection = new Map<string, EgressLineageSource>();
116
134
  for (const input of snapshots) {
117
135
  const collectionInput = input.collection;
@@ -225,15 +243,19 @@ export const resolveEgressLineage = (
225
243
  inputs: readonly EgressLineageInput[],
226
244
  requestedNames?: readonly string[]
227
245
  ): EgressLineage => {
228
- const canonical = canonicalSources(inputs);
229
- if (requestedNames === undefined) return createEgressLineage(canonical);
246
+ const canonical = canonicalSources(inputs, Number.MAX_SAFE_INTEGER);
247
+ if (requestedNames === undefined) {
248
+ if (canonical.length > MAX_EGRESS_LINEAGE_SOURCES) {
249
+ throw scopeLimitExceeded();
250
+ }
251
+ return createEgressLineage(canonical);
252
+ }
230
253
  let requested: string[];
231
254
  try {
232
255
  if (!Array.isArray(requestedNames)) throw invalidLineageInput();
233
256
  const { length } = requestedNames;
234
- if (!Number.isSafeInteger(length) || length > MAX_EGRESS_LINEAGE_SOURCES) {
235
- throw invalidLineageInput();
236
- }
257
+ if (!Number.isSafeInteger(length)) throw invalidLineageInput();
258
+ if (length > MAX_EGRESS_LINEAGE_SOURCES) throw scopeLimitExceeded();
237
259
  const names: string[] = [];
238
260
  for (let index = 0; index < length; index += 1) {
239
261
  if (!(index in requestedNames)) throw invalidLineageInput();
@@ -71,31 +71,22 @@ export function applyProjectProfileDesiredState(
71
71
  const existing =
72
72
  existingIndex >= 0 ? config.collections[existingIndex] : undefined;
73
73
  const models = collectionModels(config, desired.collection.modelPreset);
74
+ // Start from the existing entry so settings the profile does not own
75
+ // (egress, workspace, availability, memory, audit, update command) survive.
74
76
  const nextCollection: Config["collections"][number] = {
77
+ ...existing,
75
78
  name: desired.collection.name,
76
79
  path: collectionRoot,
77
80
  pattern: projectProfileIncludePattern(desired.collection.include),
78
81
  include: [],
79
82
  exclude: desired.collection.exclude,
80
- ...(existing?.updateCmd ? { updateCmd: existing.updateCmd } : {}),
81
- ...(existing?.updateCmdTimeoutMs
82
- ? { updateCmdTimeoutMs: existing.updateCmdTimeoutMs }
83
- : {}),
84
83
  ...(desired.collection.languageHint
85
84
  ? { languageHint: desired.collection.languageHint }
86
- : existing?.languageHint
87
- ? { languageHint: existing.languageHint }
88
- : {}),
89
- ...(models
90
- ? { models }
91
- : existing?.models
92
- ? { models: existing.models }
93
- : {}),
85
+ : {}),
86
+ ...(models ? { models } : {}),
94
87
  ...(desired.collection.recordAdapters
95
88
  ? { recordAdapters: desired.collection.recordAdapters }
96
- : existing?.recordAdapters
97
- ? { recordAdapters: existing.recordAdapters }
98
- : {}),
89
+ : {}),
99
90
  };
100
91
  const collections = [...config.collections];
101
92
  if (existingIndex >= 0) collections[existingIndex] = nextCollection;
@@ -28,6 +28,10 @@ import {
28
28
  canonicalOperationalPath,
29
29
  getConfigWriteLockPath,
30
30
  } from "./config-write-lock";
31
+ import {
32
+ validateWorkspaceRootSetting,
33
+ workspaceRootSettingMessage,
34
+ } from "./link-workspace";
31
35
  import { compileProjectProfileYaml } from "./project-profile";
32
36
  import {
33
37
  applyProjectProfileDesiredState,
@@ -64,7 +68,8 @@ export type ProjectProfileApplyErrorCode =
64
68
  | "PROFILE_INVALID"
65
69
  | "RECEIPT_WRITE_FAILED"
66
70
  | "RUNTIME_PATH_OVERLAP"
67
- | "STORE_SYNC_FAILED";
71
+ | "STORE_SYNC_FAILED"
72
+ | "WORKSPACE_ROOT_CONFLICT";
68
73
 
69
74
  export interface ProjectProfileApplyError {
70
75
  code: ProjectProfileApplyErrorCode;
@@ -159,6 +164,14 @@ const mutationFailure = (
159
164
  "Repair the local index store and rerun apply; the saved config is resumable.",
160
165
  };
161
166
  }
167
+ if (code === "WORKSPACE_ROOT_CONFLICT") {
168
+ return {
169
+ code: "WORKSPACE_ROOT_CONFLICT",
170
+ message,
171
+ remediation:
172
+ "Point workspaceRoot at a directory that contains the profile root, or remove it, then rerun apply.",
173
+ };
174
+ }
162
175
  if (code === "LOAD_ERROR") {
163
176
  return {
164
177
  code: "CONFIG_LOAD_FAILED",
@@ -304,6 +317,24 @@ export async function applyProjectProfile(
304
317
  compiled.value.resolvedPaths.collectionRoot,
305
318
  profileBinding
306
319
  );
320
+ // A kept explicit workspaceRoot must still contain a moved root, or
321
+ // the saved config would fail to load.
322
+ const applied = nextConfig.collections.find(
323
+ ({ name }) => name === compiled.value.desiredState.collection.name
324
+ );
325
+ if (typeof applied?.workspaceRoot === "string") {
326
+ const invalid = validateWorkspaceRootSetting(
327
+ applied.path,
328
+ applied.workspaceRoot
329
+ );
330
+ if (invalid) {
331
+ return {
332
+ ok: false,
333
+ code: "WORKSPACE_ROOT_CONFLICT",
334
+ error: workspaceRootSettingMessage(applied.name, invalid),
335
+ };
336
+ }
337
+ }
307
338
  projection = {
308
339
  collectionName: compiled.value.desiredState.collection.name,
309
340
  contexts: compiled.value.desiredState.contexts.map((context) => ({
@@ -250,13 +250,41 @@ export async function typedMetadataWarnings(
250
250
  excludeSuperseded: options.memoryFilter?.excludeSuperseded,
251
251
  });
252
252
  if (!result.ok) return unknown;
253
- const { pending, invalid } = result.value;
253
+ return metadataCoverageWarnings(result.value, "query");
254
+ }
255
+
256
+ /** The coverage warning for documents a typed metadata filter had to skip. */
257
+ export function metadataCoverageWarnings(
258
+ coverage: { pending: number; invalid: number },
259
+ scope: "query" | "listing"
260
+ ): { code: string; message: string }[] | undefined {
261
+ const { pending, invalid } = coverage;
254
262
  return pending || invalid
255
263
  ? [
256
264
  {
257
265
  code: "METADATA_COVERAGE_INCOMPLETE",
258
- message: `Typed metadata coverage is incomplete: ${pending} documents need re-ingestion and ${invalid} have invalid metadata within the query scope.`,
266
+ message: `Typed metadata coverage is incomplete: ${pending} documents need re-ingestion and ${invalid} have invalid metadata within the ${scope} scope.`,
259
267
  },
260
268
  ]
261
269
  : undefined;
262
270
  }
271
+
272
+ /**
273
+ * Keep the documents a typed metadata predicate matches, counting the ones it
274
+ * could not judge (pending re-ingestion or invalid metadata) for the warning.
275
+ */
276
+ export function filterDocumentsByMetadata<T extends DocumentRow>(
277
+ docs: readonly T[],
278
+ filter: MetadataPredicate
279
+ ): { matches: T[]; pending: number; invalid: number } {
280
+ const matches: T[] = [];
281
+ let pending = 0;
282
+ let invalid = 0;
283
+ for (const doc of docs) {
284
+ const reason = typedMetadataFilterReason(doc, filter);
285
+ if (reason === undefined) matches.push(doc);
286
+ else if (reason === "metadata_backfill") pending += 1;
287
+ else if (reason === "metadata_invalid") invalid += 1;
288
+ }
289
+ return { matches, pending, invalid };
290
+ }
package/src/sdk/client.ts CHANGED
@@ -1524,7 +1524,12 @@ class GnoClientImpl implements GnoClient {
1524
1524
 
1525
1525
  async list(options: GnoListOptions = {}) {
1526
1526
  this.assertOpen();
1527
- const result = await listDocuments(this.store, options);
1527
+ const result = await listDocuments(
1528
+ this.store,
1529
+ options.filter === undefined
1530
+ ? options
1531
+ : { ...options, filter: normalizeSdkMetadataFilter(options.filter) }
1532
+ );
1528
1533
  return {
1529
1534
  ...result,
1530
1535
  documents: result.documents.map((doc) => ({
@@ -30,6 +30,10 @@ import {
30
30
  evidenceFromExactDocument,
31
31
  RetrievalTraceSession,
32
32
  } from "../core/retrieval-trace-session";
33
+ import {
34
+ filterDocumentsByMetadata,
35
+ metadataCoverageWarnings,
36
+ } from "../pipeline/filters";
33
37
  import { supersedingUri } from "../pipeline/result-context";
34
38
  import { sdkError } from "./errors";
35
39
 
@@ -388,11 +392,19 @@ export async function listDocuments(
388
392
  });
389
393
  }
390
394
 
391
- const allActive = docsResult.value
395
+ const inScope = docsResult.value
392
396
  .filter((d) => d.active)
393
397
  .filter((d) =>
394
398
  !scope?.startsWith("gno://") ? true : d.uri.startsWith(scope)
395
- )
399
+ );
400
+ const filtered = options.filter
401
+ ? filterDocumentsByMetadata(inScope, options.filter)
402
+ : undefined;
403
+ const warnings = filtered
404
+ ? metadataCoverageWarnings(filtered, "listing")
405
+ : undefined;
406
+
407
+ const allActive = (filtered?.matches ?? inScope)
396
408
  .map((d) => ({
397
409
  docid: d.docid,
398
410
  uri: d.uri,
@@ -415,6 +427,7 @@ export async function listDocuments(
415
427
  total: allActive.length,
416
428
  returned: paged.length,
417
429
  offset,
430
+ ...(warnings ? { warnings } : {}),
418
431
  },
419
432
  };
420
433
  }
package/src/sdk/types.ts CHANGED
@@ -89,6 +89,7 @@ import type {
89
89
  SectionTargetResolveResult,
90
90
  SectionTargetV1,
91
91
  } from "../core/sections";
92
+ import type { MetadataPredicate } from "../core/typed-metadata";
92
93
  import type { SyncResult } from "../ingestion";
93
94
  import type { CollectionRootError } from "../ingestion/source-availability/root";
94
95
  import type { DownloadPolicy } from "../llm/policy";
@@ -223,6 +224,8 @@ export interface GnoListOptions {
223
224
  scope?: string;
224
225
  limit?: number;
225
226
  offset?: number;
227
+ /** Typed metadata predicate, as for search `filter`. */
228
+ filter?: MetadataPredicate;
226
229
  }
227
230
 
228
231
  export interface GnoUpdateOptions {
@@ -1 +0,0 @@
1
- 477bb4440a241369687ec7c27d3720e4ce46b8f7139dfec842c5eee10d09e0ea gno-browser-clipper-v2.13.0.zip