@gmickel/gno 1.25.1 → 1.26.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.
Files changed (56) hide show
  1. package/README.md +10 -4
  2. package/assets/skill/SKILL.md +29 -17
  3. package/browser-extension/artifacts/{gno-browser-clipper-v1.25.1.zip → gno-browser-clipper-v1.26.0.zip} +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.26.0.zip.sha256 +1 -0
  5. package/browser-extension/dist/manifest.json +1 -1
  6. package/package.json +1 -1
  7. package/spec/cli.md +146 -4
  8. package/spec/db/schema.sql +1 -1
  9. package/spec/output-schemas/project-profile-apply.schema.json +209 -0
  10. package/spec/output-schemas/project-profile-command.schema.json +160 -0
  11. package/spec/output-schemas/query-diagnose.schema.json +7 -1
  12. package/spec/output-schemas/setup-profile-result.schema.json +87 -0
  13. package/spec/project-profile.schema.json +301 -0
  14. package/src/cli/commands/collection/add.ts +39 -45
  15. package/src/cli/commands/collection/remove.ts +28 -28
  16. package/src/cli/commands/collection/rename.ts +55 -73
  17. package/src/cli/commands/context/add.ts +37 -26
  18. package/src/cli/commands/context/rm.ts +47 -20
  19. package/src/cli/commands/init.ts +55 -125
  20. package/src/cli/commands/models/use.ts +43 -38
  21. package/src/cli/commands/profile-apply.ts +334 -0
  22. package/src/cli/commands/profile.ts +409 -0
  23. package/src/cli/commands/setup-activation.ts +205 -54
  24. package/src/cli/commands/setup-profile.ts +223 -0
  25. package/src/cli/commands/setup.ts +3 -0
  26. package/src/cli/program.ts +110 -9
  27. package/src/config/index.ts +3 -0
  28. package/src/config/project-profile.ts +367 -0
  29. package/src/config/saver.ts +16 -7
  30. package/src/config/types.ts +42 -0
  31. package/src/core/config-mutation.ts +138 -76
  32. package/src/core/config-write-lock.ts +89 -0
  33. package/src/core/context-identity.ts +16 -0
  34. package/src/core/context-resolver.ts +2 -12
  35. package/src/core/folder-setup-planning.ts +6 -21
  36. package/src/core/folder-setup.ts +30 -2
  37. package/src/core/path-rules.ts +53 -0
  38. package/src/core/project-affinity-surface.ts +102 -7
  39. package/src/core/project-profile-apply-state.ts +268 -0
  40. package/src/core/project-profile-apply-validation.ts +95 -0
  41. package/src/core/project-profile-apply.ts +408 -0
  42. package/src/core/project-profile-canonical.ts +71 -0
  43. package/src/core/project-profile-diff.ts +302 -0
  44. package/src/core/project-profile-discovery.ts +519 -0
  45. package/src/core/project-profile-file.ts +37 -0
  46. package/src/core/project-profile-parser.ts +98 -0
  47. package/src/core/project-profile.ts +490 -0
  48. package/src/ingestion/walker.ts +85 -44
  49. package/src/llm/cache.ts +21 -0
  50. package/src/serve/config-sync.ts +2 -2
  51. package/src/serve/resident-runtime.ts +1 -0
  52. package/src/serve/routes/api.ts +1 -0
  53. package/src/store/migrations/021-multi-context-identity.ts +37 -0
  54. package/src/store/migrations/index.ts +2 -0
  55. package/src/store/sqlite/adapter.ts +83 -0
  56. package/browser-extension/artifacts/gno-browser-clipper-v1.25.1.zip.sha256 +0 -1
@@ -10,7 +10,10 @@
10
10
  import type { Config, ProjectAffinityInput } from "../config/types";
11
11
  import type { ProjectAffinityScoringInput } from "../pipeline/project-affinity";
12
12
 
13
+ import { PROJECT_AFFINITY_MAX_CONTRIBUTION } from "../config/types";
13
14
  import { resolveProjectAffinity } from "./project-affinity";
15
+ import { compileProjectProfileYaml } from "./project-profile";
16
+ import { discoverProjectProfile } from "./project-profile-discovery";
14
17
 
15
18
  export const MAX_PROJECT_AFFINITY_INPUTS = 16;
16
19
 
@@ -19,6 +22,12 @@ export interface CliProjectAffinityRequest {
19
22
  projectRoots?: string[];
20
23
  }
21
24
 
25
+ export interface ProjectProfileAffinityDefaults {
26
+ contribution: number;
27
+ enabled: boolean;
28
+ profileRoot: string;
29
+ }
30
+
22
31
  export class ProjectAffinityInputError extends Error {
23
32
  constructor(message: string) {
24
33
  super(message);
@@ -66,12 +75,54 @@ const scoringInput = async (
66
75
  }),
67
76
  });
68
77
 
78
+ /**
79
+ * Resolve only the nearest trusted local profile's compiled affinity defaults.
80
+ * Invalid/missing profiles are a fallback signal, not a retrieval failure.
81
+ * Profile content never supplies identity: discovery's canonical profile root
82
+ * is the sole project root.
83
+ */
84
+ export const resolveProjectProfileAffinityDefaults = async (
85
+ cwd: string,
86
+ config: Config
87
+ ): Promise<ProjectProfileAffinityDefaults | null> => {
88
+ const discovery = await discoverProjectProfile({
89
+ channel: "local",
90
+ cwd,
91
+ });
92
+ if (
93
+ discovery.summary.status !== "found" ||
94
+ !discovery.profilePath ||
95
+ !discovery.profileRoot
96
+ ) {
97
+ return null;
98
+ }
99
+
100
+ try {
101
+ const profileYaml = await Bun.file(discovery.profilePath).text();
102
+ const compiled = await compileProjectProfileYaml(profileYaml, {
103
+ profileRoot: discovery.profileRoot,
104
+ config,
105
+ });
106
+ if (!compiled.ok) return null;
107
+ return {
108
+ ...compiled.value.desiredState.affinityDefaults,
109
+ profileRoot: discovery.profileRoot,
110
+ };
111
+ } catch {
112
+ return null;
113
+ }
114
+ };
115
+
69
116
  export const resolveCliProjectAffinity = async (
70
117
  config: Config,
71
118
  options: {
72
119
  cwd: string;
73
120
  disabled?: boolean;
74
121
  projectRoots?: readonly string[];
122
+ resolveProfileDefaults?: (
123
+ cwd: string,
124
+ config: Config
125
+ ) => Promise<ProjectProfileAffinityDefaults | null>;
75
126
  }
76
127
  ): Promise<ProjectAffinityScoringInput | undefined> => {
77
128
  const projectRoots = normalizeProjectAffinityValues(
@@ -83,16 +134,60 @@ export const resolveCliProjectAffinity = async (
83
134
  "--no-project-affinity cannot be combined with --project-root"
84
135
  );
85
136
  }
86
- if (options.disabled || config.projectAffinity?.enabled === false) return;
137
+ if (options.disabled) return;
87
138
 
88
- const roots =
89
- projectRoots.length > 0
90
- ? projectRoots.map((path) => ({
139
+ if (projectRoots.length > 0) {
140
+ return scoringInput(
141
+ {
142
+ roots: projectRoots.map((path) => ({
91
143
  path,
92
144
  source: "cli_explicit" as const,
93
- }))
94
- : [{ path: options.cwd, source: "cli_cwd" as const }];
95
- return scoringInput({ roots }, config, "local");
145
+ })),
146
+ },
147
+ {
148
+ ...config,
149
+ projectAffinity: {
150
+ enabled: true,
151
+ contribution:
152
+ config.projectAffinity?.contribution ??
153
+ PROJECT_AFFINITY_MAX_CONTRIBUTION,
154
+ },
155
+ },
156
+ "local"
157
+ );
158
+ }
159
+
160
+ const profileDefaults = await (
161
+ options.resolveProfileDefaults ?? resolveProjectProfileAffinityDefaults
162
+ )(options.cwd, config);
163
+ if (profileDefaults) {
164
+ if (!profileDefaults.enabled) return;
165
+ return scoringInput(
166
+ {
167
+ roots: [
168
+ {
169
+ path: profileDefaults.profileRoot,
170
+ source: "project_profile",
171
+ },
172
+ ],
173
+ },
174
+ {
175
+ ...config,
176
+ projectAffinity: {
177
+ enabled: profileDefaults.enabled,
178
+ contribution: profileDefaults.contribution,
179
+ },
180
+ },
181
+ "local"
182
+ );
183
+ }
184
+
185
+ if (config.projectAffinity?.enabled === false) return;
186
+ return scoringInput(
187
+ { roots: [{ path: options.cwd, source: "cli_cwd" }] },
188
+ config,
189
+ "local"
190
+ );
96
191
  };
97
192
 
98
193
  export const resolveRemoteProjectAffinity = async (
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Pure create/update-only config projection for project profile apply.
3
+ *
4
+ * @module src/core/project-profile-apply-state
5
+ */
6
+
7
+ import type { Config, ProjectProfileBinding } from "../config/types";
8
+ import type { ProjectProfileDesiredState } from "./project-profile";
9
+ import type { ProjectProfileDiff } from "./project-profile-diff";
10
+
11
+ import { getPreset } from "../llm/registry";
12
+ import { normalizePersistedContextText } from "./context-identity";
13
+ import {
14
+ canonicalProjectProfileJson,
15
+ projectProfileIncludePattern,
16
+ } from "./project-profile";
17
+
18
+ export type ProjectProfileApplyDisposition =
19
+ | "created"
20
+ | "reused"
21
+ | "updated"
22
+ | "skipped";
23
+
24
+ export interface ProjectProfileApplyResource {
25
+ kind:
26
+ | "capability"
27
+ | "collection"
28
+ | "content_type"
29
+ | "contexts"
30
+ | "profile_binding"
31
+ | "project_affinity"
32
+ | "stale_mapping";
33
+ id: string;
34
+ disposition: ProjectProfileApplyDisposition;
35
+ pendingIndexing: boolean;
36
+ }
37
+
38
+ const compareCodeUnits = (left: string, right: string): number =>
39
+ left < right ? -1 : left > right ? 1 : 0;
40
+
41
+ export const canonicalProfileStateEqual = (
42
+ left: unknown,
43
+ right: unknown
44
+ ): boolean =>
45
+ canonicalProjectProfileJson(left) === canonicalProjectProfileJson(right);
46
+
47
+ const collectionModels = (
48
+ config: Config,
49
+ presetId: string | undefined
50
+ ): Config["collections"][number]["models"] | undefined => {
51
+ if (!presetId) return undefined;
52
+ const preset = getPreset(config, presetId);
53
+ if (!preset) return undefined;
54
+ return {
55
+ embed: preset.embed,
56
+ rerank: preset.rerank,
57
+ ...(preset.expand ? { expand: preset.expand } : {}),
58
+ gen: preset.gen,
59
+ };
60
+ };
61
+
62
+ export function applyProjectProfileDesiredState(
63
+ config: Config,
64
+ desired: ProjectProfileDesiredState,
65
+ collectionRoot: string,
66
+ profileBinding: ProjectProfileBinding
67
+ ): Config {
68
+ const existingIndex = config.collections.findIndex(
69
+ (collection) => collection.name === desired.collection.name
70
+ );
71
+ const existing =
72
+ existingIndex >= 0 ? config.collections[existingIndex] : undefined;
73
+ const models = collectionModels(config, desired.collection.modelPreset);
74
+ const nextCollection: Config["collections"][number] = {
75
+ name: desired.collection.name,
76
+ path: collectionRoot,
77
+ pattern: projectProfileIncludePattern(desired.collection.include),
78
+ include: [],
79
+ exclude: desired.collection.exclude,
80
+ ...(existing?.updateCmd ? { updateCmd: existing.updateCmd } : {}),
81
+ ...(desired.collection.languageHint
82
+ ? { languageHint: desired.collection.languageHint }
83
+ : existing?.languageHint
84
+ ? { languageHint: existing.languageHint }
85
+ : {}),
86
+ ...(models
87
+ ? { models }
88
+ : existing?.models
89
+ ? { models: existing.models }
90
+ : {}),
91
+ };
92
+ const collections = [...config.collections];
93
+ if (existingIndex >= 0) collections[existingIndex] = nextCollection;
94
+ else collections.push(nextCollection);
95
+
96
+ const contexts = [...config.contexts];
97
+ const configuredContexts = new Set(
98
+ contexts
99
+ .filter(
100
+ (context) =>
101
+ context.scopeType === "collection" &&
102
+ context.scopeKey === `${desired.collection.name}:`
103
+ )
104
+ .map((context) => normalizePersistedContextText(context.text))
105
+ );
106
+ for (const context of desired.contexts) {
107
+ const normalizedText = normalizePersistedContextText(context.text);
108
+ if (!configuredContexts.has(normalizedText)) {
109
+ contexts.push({
110
+ scopeType: context.scopeType,
111
+ scopeKey: context.scopeKey,
112
+ text: normalizedText,
113
+ });
114
+ configuredContexts.add(normalizedText);
115
+ }
116
+ }
117
+
118
+ const contentTypes = [...(config.contentTypes ?? [])];
119
+ for (const desiredRule of desired.contentTypes) {
120
+ const index = contentTypes.findIndex((rule) => rule.id === desiredRule.id);
121
+ if (index >= 0) contentTypes[index] = desiredRule;
122
+ else contentTypes.push(desiredRule);
123
+ }
124
+
125
+ const projectProfileBindings = [...(config.projectProfileBindings ?? [])];
126
+ const bindingIndex = projectProfileBindings.findIndex(
127
+ (binding) => binding.path === profileBinding.path
128
+ );
129
+ if (bindingIndex >= 0) projectProfileBindings[bindingIndex] = profileBinding;
130
+ else projectProfileBindings.push(profileBinding);
131
+ projectProfileBindings.sort((left, right) =>
132
+ compareCodeUnits(left.path, right.path)
133
+ );
134
+
135
+ return {
136
+ ...config,
137
+ collections,
138
+ contexts,
139
+ contentTypes,
140
+ projectProfileBindings,
141
+ };
142
+ }
143
+
144
+ export function buildProjectProfileResources(
145
+ before: Config,
146
+ after: Config,
147
+ diff: ProjectProfileDiff,
148
+ desired: ProjectProfileDesiredState,
149
+ profileBinding: ProjectProfileBinding
150
+ ): ProjectProfileApplyResource[] {
151
+ const resources: ProjectProfileApplyResource[] = [];
152
+ const beforeCollection = before.collections.find(
153
+ (collection) => collection.name === desired.collection.name
154
+ );
155
+ const afterCollection = after.collections.find(
156
+ (collection) => collection.name === desired.collection.name
157
+ );
158
+ resources.push({
159
+ kind: "collection",
160
+ id: desired.collection.name,
161
+ disposition: beforeCollection
162
+ ? canonicalProfileStateEqual(beforeCollection, afterCollection)
163
+ ? "reused"
164
+ : "updated"
165
+ : "created",
166
+ pendingIndexing: !canonicalProfileStateEqual(
167
+ beforeCollection,
168
+ afterCollection
169
+ ),
170
+ });
171
+
172
+ const beforeContextTexts = new Set(
173
+ before.contexts
174
+ .filter(
175
+ (context) =>
176
+ context.scopeType === "collection" &&
177
+ context.scopeKey === `${desired.collection.name}:`
178
+ )
179
+ .map((context) => normalizePersistedContextText(context.text))
180
+ );
181
+ const missingContexts = desired.contexts.filter(
182
+ (context) =>
183
+ !beforeContextTexts.has(normalizePersistedContextText(context.text))
184
+ );
185
+ resources.push({
186
+ kind: "contexts",
187
+ id: `${desired.collection.name}:`,
188
+ disposition:
189
+ desired.contexts.length === 0
190
+ ? "skipped"
191
+ : missingContexts.length === 0
192
+ ? "reused"
193
+ : beforeContextTexts.size === 0
194
+ ? "created"
195
+ : "updated",
196
+ pendingIndexing: false,
197
+ });
198
+
199
+ const beforeContentTypes = new Map(
200
+ (before.contentTypes ?? []).map((rule) => [rule.id, rule])
201
+ );
202
+ for (const rule of desired.contentTypes) {
203
+ const previous = beforeContentTypes.get(rule.id);
204
+ const disposition = previous
205
+ ? canonicalProfileStateEqual(previous, rule)
206
+ ? "reused"
207
+ : "updated"
208
+ : "created";
209
+ resources.push({
210
+ kind: "content_type",
211
+ id: rule.id,
212
+ disposition,
213
+ pendingIndexing: disposition !== "reused",
214
+ });
215
+ }
216
+ if (desired.contentTypes.length === 0) {
217
+ resources.push({
218
+ kind: "content_type",
219
+ id: "(none declared)",
220
+ disposition: "skipped",
221
+ pendingIndexing: false,
222
+ });
223
+ }
224
+
225
+ resources.push({
226
+ kind: "profile_binding",
227
+ id: profileBinding.collection,
228
+ disposition: (() => {
229
+ const previous = before.projectProfileBindings?.find(
230
+ (binding) => binding.path === profileBinding.path
231
+ );
232
+ if (!previous) return "created";
233
+ return canonicalProfileStateEqual(previous, profileBinding)
234
+ ? "reused"
235
+ : "updated";
236
+ })(),
237
+ pendingIndexing: false,
238
+ });
239
+
240
+ resources.push({
241
+ kind: "project_affinity",
242
+ id: "profile",
243
+ disposition: "skipped",
244
+ pendingIndexing: false,
245
+ });
246
+ for (const capability of desired.recommendedCapabilities) {
247
+ resources.push({
248
+ kind: "capability",
249
+ id: capability,
250
+ disposition: "skipped",
251
+ pendingIndexing: false,
252
+ });
253
+ }
254
+ for (const mapping of diff.staleMappings) {
255
+ resources.push({
256
+ kind: "stale_mapping",
257
+ id: mapping.collection,
258
+ disposition: "skipped",
259
+ pendingIndexing: false,
260
+ });
261
+ }
262
+ return resources.sort(
263
+ (left, right) =>
264
+ compareCodeUnits(left.kind, right.kind) ||
265
+ compareCodeUnits(left.id, right.id) ||
266
+ compareCodeUnits(left.disposition, right.disposition)
267
+ );
268
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Runtime validation for project-profile apply receipts crossing command
3
+ * composition boundaries.
4
+ *
5
+ * @module src/core/project-profile-apply-validation
6
+ */
7
+
8
+ import { z } from "zod";
9
+
10
+ import type { ProjectProfileApplyReceipt } from "./project-profile-apply";
11
+
12
+ const diagnosticSchema = z
13
+ .object({
14
+ code: z.literal("STALE_PROFILE_MAPPING"),
15
+ severity: z.literal("warning"),
16
+ path: z.string(),
17
+ message: z.string().min(1),
18
+ remediation: z.string().min(1),
19
+ })
20
+ .strict();
21
+
22
+ const diffSchema = z
23
+ .object({
24
+ status: z.enum(["in_sync", "changes_required"]),
25
+ changes: z.array(
26
+ z
27
+ .object({
28
+ action: z.enum(["add", "update", "repair", "review"]),
29
+ field: z.string().min(1),
30
+ destructive: z.literal(false),
31
+ summary: z.string().min(1),
32
+ })
33
+ .strict()
34
+ ),
35
+ staleMappings: z.array(
36
+ z
37
+ .object({
38
+ collection: z.string().min(1),
39
+ reason: z.enum(["name_changed", "root_changed"]),
40
+ choices: z.tuple([
41
+ z.literal("repair"),
42
+ z.literal("remove_explicitly"),
43
+ ]),
44
+ })
45
+ .strict()
46
+ ),
47
+ })
48
+ .strict();
49
+
50
+ const resourceSchema = z
51
+ .object({
52
+ kind: z.enum([
53
+ "capability",
54
+ "collection",
55
+ "content_type",
56
+ "contexts",
57
+ "profile_binding",
58
+ "project_affinity",
59
+ "stale_mapping",
60
+ ]),
61
+ id: z.string().min(1),
62
+ disposition: z.enum(["created", "reused", "updated", "skipped"]),
63
+ pendingIndexing: z.boolean(),
64
+ })
65
+ .strict();
66
+
67
+ export const projectProfileApplyReceiptSchema: z.ZodType<ProjectProfileApplyReceipt> =
68
+ z
69
+ .object({
70
+ schemaVersion: z.literal("1.0"),
71
+ command: z.literal("apply"),
72
+ status: z.enum(["applied", "unchanged"]),
73
+ profile: z
74
+ .object({
75
+ fingerprint: z.string().regex(/^[a-f0-9]{64}$/),
76
+ })
77
+ .strict(),
78
+ diff: diffSchema,
79
+ resources: z.array(resourceSchema),
80
+ pendingIndexing: z.array(z.string().min(1)),
81
+ diagnostics: z.array(diagnosticSchema),
82
+ })
83
+ .strict()
84
+ .refine(
85
+ (receipt) =>
86
+ new Set(receipt.pendingIndexing).size ===
87
+ receipt.pendingIndexing.length,
88
+ { path: ["pendingIndexing"], message: "Entries must be unique." }
89
+ );
90
+
91
+ export function isProjectProfileApplyReceipt(
92
+ value: unknown
93
+ ): value is ProjectProfileApplyReceipt {
94
+ return projectProfileApplyReceiptSchema.safeParse(value).success;
95
+ }