@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.
- package/README.md +10 -4
- package/assets/skill/SKILL.md +29 -17
- package/browser-extension/artifacts/{gno-browser-clipper-v1.25.1.zip → gno-browser-clipper-v1.26.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.26.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +146 -4
- package/spec/db/schema.sql +1 -1
- package/spec/output-schemas/project-profile-apply.schema.json +209 -0
- package/spec/output-schemas/project-profile-command.schema.json +160 -0
- package/spec/output-schemas/query-diagnose.schema.json +7 -1
- package/spec/output-schemas/setup-profile-result.schema.json +87 -0
- package/spec/project-profile.schema.json +301 -0
- package/src/cli/commands/collection/add.ts +39 -45
- package/src/cli/commands/collection/remove.ts +28 -28
- package/src/cli/commands/collection/rename.ts +55 -73
- package/src/cli/commands/context/add.ts +37 -26
- package/src/cli/commands/context/rm.ts +47 -20
- package/src/cli/commands/init.ts +55 -125
- package/src/cli/commands/models/use.ts +43 -38
- package/src/cli/commands/profile-apply.ts +334 -0
- package/src/cli/commands/profile.ts +409 -0
- package/src/cli/commands/setup-activation.ts +205 -54
- package/src/cli/commands/setup-profile.ts +223 -0
- package/src/cli/commands/setup.ts +3 -0
- package/src/cli/program.ts +110 -9
- package/src/config/index.ts +3 -0
- package/src/config/project-profile.ts +367 -0
- package/src/config/saver.ts +16 -7
- package/src/config/types.ts +42 -0
- package/src/core/config-mutation.ts +138 -76
- package/src/core/config-write-lock.ts +89 -0
- package/src/core/context-identity.ts +16 -0
- package/src/core/context-resolver.ts +2 -12
- package/src/core/folder-setup-planning.ts +6 -21
- package/src/core/folder-setup.ts +30 -2
- package/src/core/path-rules.ts +53 -0
- package/src/core/project-affinity-surface.ts +102 -7
- package/src/core/project-profile-apply-state.ts +268 -0
- package/src/core/project-profile-apply-validation.ts +95 -0
- package/src/core/project-profile-apply.ts +408 -0
- package/src/core/project-profile-canonical.ts +71 -0
- package/src/core/project-profile-diff.ts +302 -0
- package/src/core/project-profile-discovery.ts +519 -0
- package/src/core/project-profile-file.ts +37 -0
- package/src/core/project-profile-parser.ts +98 -0
- package/src/core/project-profile.ts +490 -0
- package/src/ingestion/walker.ts +85 -44
- package/src/llm/cache.ts +21 -0
- package/src/serve/config-sync.ts +2 -2
- package/src/serve/resident-runtime.ts +1 -0
- package/src/serve/routes/api.ts +1 -0
- package/src/store/migrations/021-multi-context-identity.ts +37 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +83 -0
- 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
|
|
137
|
+
if (options.disabled) return;
|
|
87
138
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
+
}
|