@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.
- package/README.md +1 -1
- package/assets/skill/SKILL.md +35 -4
- package/assets/skill/cli-reference.md +39 -1
- package/assets/skill/examples.md +30 -4
- package/assets/skill/mcp-reference.md +14 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.13.0.zip → gno-browser-clipper-v2.14.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.14.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +73 -11
- package/spec/mcp.md +15 -2
- package/spec/output-schemas/collection-list.schema.json +28 -0
- package/spec/output-schemas/ls.schema.json +65 -0
- package/src/cli/commands/ls.ts +20 -3
- package/src/cli/commands/profile-apply.ts +2 -1
- package/src/cli/program.ts +11 -0
- package/src/config/loader.ts +11 -1
- package/src/config/types.ts +90 -1
- package/src/core/audit-collection-rules.ts +331 -0
- package/src/core/audit-links.ts +3 -1
- package/src/core/audit-workspace.ts +69 -5
- package/src/core/collection-egress-policy-service.ts +4 -1
- package/src/core/egress-enforcement.ts +20 -4
- package/src/core/egress-policy.ts +4 -0
- package/src/core/egress-provenance.ts +33 -11
- package/src/core/project-profile-apply-state.ts +6 -15
- package/src/core/project-profile-apply.ts +32 -1
- package/src/pipeline/filters.ts +30 -2
- package/src/sdk/client.ts +6 -1
- package/src/sdk/documents.ts +15 -2
- package/src/sdk/types.ts +3 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.13.0.zip.sha256 +0 -1
|
@@ -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:
|
|
36
|
+
readonly code:
|
|
37
|
+
| "EMPTY_EGRESS_LINEAGE"
|
|
38
|
+
| "INVALID_EGRESS_LINEAGE"
|
|
39
|
+
| "EGRESS_SCOPE_LIMIT_EXCEEDED";
|
|
37
40
|
|
|
38
41
|
constructor(
|
|
39
|
-
code:
|
|
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 >
|
|
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)
|
|
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)
|
|
235
|
-
|
|
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
|
-
:
|
|
87
|
-
|
|
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
|
-
:
|
|
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) => ({
|
package/src/pipeline/filters.ts
CHANGED
|
@@ -250,13 +250,41 @@ export async function typedMetadataWarnings(
|
|
|
250
250
|
excludeSuperseded: options.memoryFilter?.excludeSuperseded,
|
|
251
251
|
});
|
|
252
252
|
if (!result.ok) return unknown;
|
|
253
|
-
|
|
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
|
|
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(
|
|
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) => ({
|
package/src/sdk/documents.ts
CHANGED
|
@@ -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
|
|
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
|