@djordje-stojanovic/sigmaskills 0.2.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 (53) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/LICENSE +21 -0
  3. package/README.md +342 -0
  4. package/bin/sigmaskills.js +7 -0
  5. package/manifest.json +28 -0
  6. package/package.json +42 -0
  7. package/registry/agent-hosts.json +2404 -0
  8. package/registry/schema.json +110 -0
  9. package/registry/skill-baselines.json +4 -0
  10. package/registry/source.json +6 -0
  11. package/sigmabrief/SKILL.md +56 -0
  12. package/sigmabrief/agents/openai.yaml +12 -0
  13. package/sigmabrief/references/brief-method.md +73 -0
  14. package/sigmabrief/references/prompt-contract.md +174 -0
  15. package/sigmaperformance/SKILL.md +118 -0
  16. package/sigmaperformance/agents/openai.yaml +12 -0
  17. package/sigmaperformance/references/audit-method.md +112 -0
  18. package/sigmaperformance/references/calibration.md +45 -0
  19. package/sigmaperformance/references/report-contract.md +103 -0
  20. package/sigmareview/SKILL.md +133 -0
  21. package/sigmareview/agents/openai.yaml +12 -0
  22. package/sigmareview/references/report-contract.md +217 -0
  23. package/sigmareview/references/review-method.md +233 -0
  24. package/sigmawrite/SKILL.md +45 -0
  25. package/sigmawrite/agents/openai.yaml +12 -0
  26. package/src/adoption.js +370 -0
  27. package/src/backup.js +398 -0
  28. package/src/catalog.js +211 -0
  29. package/src/cli.js +657 -0
  30. package/src/customization.js +344 -0
  31. package/src/destinations.js +491 -0
  32. package/src/interactive.js +959 -0
  33. package/src/links.js +157 -0
  34. package/src/plan.js +429 -0
  35. package/src/prepack.js +10 -0
  36. package/src/project-lock.js +169 -0
  37. package/src/purge.js +477 -0
  38. package/src/registry/automation-ci.js +411 -0
  39. package/src/registry/automation.js +554 -0
  40. package/src/registry/diff.js +149 -0
  41. package/src/registry/normalize.js +67 -0
  42. package/src/registry/parse.js +184 -0
  43. package/src/registry/sync.js +230 -0
  44. package/src/registry/validate.js +223 -0
  45. package/src/release-ci.js +12 -0
  46. package/src/release.js +837 -0
  47. package/src/restore.js +518 -0
  48. package/src/revision.js +82 -0
  49. package/src/state.js +480 -0
  50. package/src/status.js +469 -0
  51. package/src/transaction.js +636 -0
  52. package/src/uninstall.js +647 -0
  53. package/src/update.js +815 -0
@@ -0,0 +1,554 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Guarded Agent Host registry pull-request automation.
5
+ *
6
+ * Converter code always comes from an exact trusted default-branch SHA.
7
+ * Upstream TypeScript is fetched as pinned data and never executed.
8
+ * Generated trees are limited to the registry/attribution allowlist.
9
+ * Classification records whether a change may auto-merge. Safe additions
10
+ * and description-only changes can merge and publish a patch Release
11
+ * through the trusted patch primitive; authority-sensitive changes stay
12
+ * blocked for owner review.
13
+ */
14
+
15
+ import crypto from 'node:crypto';
16
+ import { parseAgents } from './parse.js';
17
+ import { buildSnapshot, normalizeHost } from './normalize.js';
18
+ import { validateSnapshot } from './validate.js';
19
+ import { diffSnapshots } from './diff.js';
20
+ import { bumpSemver, CHECKOUT_ACTION_PIN, SETUP_NODE_ACTION_PIN } from '../release.js';
21
+
22
+ export const REGISTRY_SYNC_WORKFLOW_FILE = 'registry-sync.yml';
23
+ export const GENERATED_BRANCH_PREFIX = 'registry/sync-';
24
+ export const REGISTRY_ALLOWLIST = Object.freeze([
25
+ 'registry/agent-hosts.json',
26
+ 'registry/source.json',
27
+ 'test/fixtures/vercel-skills/src/agents.ts',
28
+ ]);
29
+
30
+ const REVISION_PATTERN = /^[0-9a-f]{40}$/;
31
+ const BLOCKED_MUTATIONS = Object.freeze({
32
+ closeUnrelatedIssues: false,
33
+ closeUnrelatedPullRequests: false,
34
+ deleteUnrelatedBranches: false,
35
+ });
36
+
37
+ function sha256(text) {
38
+ return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
39
+ }
40
+
41
+ function posixPath(file) {
42
+ return String(file || '').replace(/\\/g, '/');
43
+ }
44
+
45
+ export function assertAllowlistedPaths(files) {
46
+ const denied = [];
47
+ for (const file of files || []) {
48
+ const normalized = posixPath(file);
49
+ if (!REGISTRY_ALLOWLIST.includes(normalized)) denied.push(normalized);
50
+ }
51
+ return {
52
+ ok: denied.length === 0,
53
+ denied,
54
+ };
55
+ }
56
+
57
+ export function calculateRegistryPatchVersion({
58
+ packageVersion,
59
+ npmVersions = [],
60
+ githubVersions = [],
61
+ requestedBump,
62
+ } = {}) {
63
+ if (requestedBump && requestedBump !== 'patch') {
64
+ throw new Error('registry automation can publish patch Releases only; major and minor Releases stay owner-triggered');
65
+ }
66
+ const parsed = [];
67
+ for (const value of [packageVersion, ...npmVersions, ...githubVersions]) {
68
+ const match = String(value || '').match(/^(\d+)\.(\d+)\.(\d+)$/);
69
+ if (!match) continue;
70
+ parsed.push({
71
+ major: Number(match[1]),
72
+ minor: Number(match[2]),
73
+ patch: Number(match[3]),
74
+ raw: match[0],
75
+ });
76
+ }
77
+ if (parsed.length === 0) throw new Error('registry patch version requires a published or package semantic version');
78
+ parsed.sort((a, b) => a.major - b.major || a.minor - b.minor || a.patch - b.patch);
79
+ return bumpSemver(parsed[parsed.length - 1].raw, 'patch');
80
+ }
81
+
82
+ export function classifySemanticAuthority(diff) {
83
+ const blockedReasons = [];
84
+ for (const change of diff && diff.changes ? diff.changes : []) {
85
+ if (change.kind === 'source-change') continue;
86
+ if (change.kind === 'description-change') continue;
87
+ if (change.kind === 'addition') {
88
+ if (change.authority !== 'safe') {
89
+ blockedReasons.push(`${change.id}: new host failed destination validation`);
90
+ }
91
+ continue;
92
+ }
93
+ if (change.kind === 'removal') {
94
+ blockedReasons.push(`${change.id}: removal`);
95
+ continue;
96
+ }
97
+ if (change.kind === 'change') {
98
+ const fields = change.fields && change.fields.length ? change.fields.join(', ') : 'identity';
99
+ blockedReasons.push(`${change.id}: ${fields}`);
100
+ continue;
101
+ }
102
+ blockedReasons.push(`${change.id}: unknown change ${change.kind}`);
103
+ }
104
+ return {
105
+ autoEligible: blockedReasons.length === 0,
106
+ blockedReasons,
107
+ };
108
+ }
109
+
110
+ export function evaluateAutoAuthorization(input) {
111
+ const deniedReasons = [];
112
+ if (input.origin === 'fork') deniedReasons.push('fork pull requests cannot be auto-authorized');
113
+ if (!String(input.headRef || '').startsWith(GENERATED_BRANCH_PREFIX)) {
114
+ deniedReasons.push('human branches cannot be auto-authorized; ref is not a generated registry-sync branch');
115
+ }
116
+ if (input.headSha && input.expectedGeneratedSha && input.headSha !== input.expectedGeneratedSha) {
117
+ deniedReasons.push('stale generated head cannot be auto-authorized');
118
+ }
119
+ if (input.currentDefaultSha && input.expectedHeadSha && input.currentDefaultSha !== input.expectedHeadSha) {
120
+ deniedReasons.push('moved default branch cannot be auto-authorized');
121
+ }
122
+ if (input.checkConclusion && input.checkConclusion !== 'success') {
123
+ deniedReasons.push('failed or incomplete checks cannot be auto-authorized');
124
+ }
125
+ if (input.concurrentRun) deniedReasons.push('concurrent registry-sync runs cannot be auto-authorized');
126
+ if (input.fromTrustedWorkflow !== true) {
127
+ deniedReasons.push('only a generated pull request from the trusted registry-sync workflow can proceed');
128
+ }
129
+ if (input.defaultBranchProtected !== true) {
130
+ deniedReasons.push('current protected default branch is required');
131
+ }
132
+ const allowlist = assertAllowlistedPaths(input.files);
133
+ if (!allowlist.ok) {
134
+ deniedReasons.push(`generated changes escape the registry allowlist: ${allowlist.denied.join(', ')}`);
135
+ }
136
+ if (!input.files || input.files.length === 0) {
137
+ deniedReasons.push('generated changes are missing; empty diffs cannot be auto-authorized');
138
+ }
139
+ if (input.classification && input.classification.autoEligible === false) {
140
+ deniedReasons.push(...(input.classification.blockedReasons || ['semantic classification is blocked']));
141
+ }
142
+ const autoAuthorized = deniedReasons.length === 0;
143
+ return {
144
+ autoAuthorized,
145
+ deniedReasons,
146
+ actions: {
147
+ merge: autoAuthorized,
148
+ publishNpm: autoAuthorized,
149
+ ...BLOCKED_MUTATIONS,
150
+ },
151
+ };
152
+ }
153
+
154
+ export function planGeneratedBranchCleanup({ dryRun = true, branches = [], protectedRefs = [] }) {
155
+ const protectedSet = new Set(protectedRefs);
156
+ const wouldDelete = [];
157
+ const refused = [];
158
+ for (const branch of branches) {
159
+ const name = branch.name;
160
+ if (protectedSet.has(name)) {
161
+ refused.push({ name, reason: 'protected' });
162
+ continue;
163
+ }
164
+ const generated = branch.generated === true || name.startsWith(GENERATED_BRANCH_PREFIX);
165
+ if (!generated || !name.startsWith(GENERATED_BRANCH_PREFIX)) {
166
+ refused.push({ name, reason: 'not-generated' });
167
+ continue;
168
+ }
169
+ wouldDelete.push(name);
170
+ }
171
+ return {
172
+ dryRun: dryRun !== false,
173
+ wouldDelete,
174
+ refused,
175
+ deleteUnrelatedBranches: false,
176
+ };
177
+ }
178
+
179
+ export function planRegistrySync({
180
+ expectedHeadSha,
181
+ currentHeadSha,
182
+ converterRevision,
183
+ upstreamRevision,
184
+ upstreamText,
185
+ previousSnapshot,
186
+ previousPin,
187
+ }) {
188
+ const errors = [];
189
+ if (!REVISION_PATTERN.test(String(expectedHeadSha || ''))) {
190
+ errors.push('expected head SHA must be an immutable 40-hex revision');
191
+ }
192
+ if (!REVISION_PATTERN.test(String(upstreamRevision || ''))) {
193
+ errors.push('upstream revision must be an immutable 40-hex pin');
194
+ }
195
+ if (converterRevision !== expectedHeadSha) {
196
+ errors.push('converter is not the trusted expected head; generated and upstream branch code must not run');
197
+ }
198
+ if (errors.length > 0) return { ok: false, errors, files: [], hasChanges: false };
199
+
200
+ const pin = {
201
+ repository: previousPin.repository,
202
+ pinnedRevision: upstreamRevision,
203
+ upstreamFile: previousPin.upstreamFile,
204
+ contentSha256: sha256(upstreamText),
205
+ };
206
+ const parsed = parseAgents(upstreamText);
207
+ const hosts = parsed.map((host) => normalizeHost(host, pin));
208
+ const snapshot = buildSnapshot(hosts, pin);
209
+ const validation = validateSnapshot(snapshot);
210
+ if (!validation.valid) {
211
+ return {
212
+ ok: false,
213
+ errors: [...(validation.errors || []), ...Object.values(validation.hostErrors || {})],
214
+ validation,
215
+ pin,
216
+ snapshot,
217
+ files: [],
218
+ hasChanges: false,
219
+ };
220
+ }
221
+
222
+ const diff = previousSnapshot ? diffSnapshots(previousSnapshot, snapshot) : { summary: {}, changes: [] };
223
+ const classification = classifySemanticAuthority(diff);
224
+ const files = [...REGISTRY_ALLOWLIST];
225
+ const hasChanges = JSON.stringify(previousSnapshot) !== JSON.stringify(snapshot)
226
+ || JSON.stringify(previousPin) !== JSON.stringify(pin);
227
+
228
+ return {
229
+ ok: true,
230
+ errors: [],
231
+ expectedHeadSha,
232
+ currentHeadSha,
233
+ converterRevision,
234
+ upstreamRevision,
235
+ pin,
236
+ snapshot,
237
+ upstreamText,
238
+ validation,
239
+ diff,
240
+ classification,
241
+ files,
242
+ hasChanges,
243
+ movedDefaultBranch: currentHeadSha !== expectedHeadSha,
244
+ };
245
+ }
246
+
247
+ export function formatRegistryPrBody(plan) {
248
+ const changes = (plan.diff && plan.diff.changes) || [];
249
+ const diffLines = changes.length === 0
250
+ ? ['- (no host semantic changes)']
251
+ : changes.map((change) => {
252
+ const fields = change.fields && change.fields.length ? ` [${change.fields.join(', ')}]` : '';
253
+ return `- ${change.authority || 'n/a'} ${change.id} ${change.kind}${fields}`;
254
+ });
255
+ const validation = plan.validation || {};
256
+ const hostErrors = Object.entries(validation.hostErrors || {}).map(([id, err]) => `- ${id}: ${err}`);
257
+ const blocked = (plan.classification && plan.classification.blockedReasons) || [];
258
+ return [
259
+ '## Agent Host registry sync',
260
+ '',
261
+ `Upstream commit: \`${plan.upstreamRevision}\``,
262
+ `Expected default-branch head: \`${plan.expectedHeadSha || ''}\``,
263
+ plan.generatedHeadSha ? `Generated head SHA: \`${plan.generatedHeadSha}\`` : null,
264
+ '',
265
+ '### Semantic diff',
266
+ '',
267
+ ...diffLines,
268
+ '',
269
+ '### Validation evidence',
270
+ '',
271
+ `- valid: ${validation.valid === true}`,
272
+ ...(validation.errors || []).map((err) => `- ${err}`),
273
+ ...hostErrors,
274
+ '',
275
+ '### Blocked reasons',
276
+ '',
277
+ ...(blocked.length ? blocked.map((reason) => `- ${reason}`) : ['- none; classified as later-auto-mergeable']),
278
+ '',
279
+ plan.classification && plan.classification.autoEligible
280
+ ? 'This pull request may auto-merge and publish a patch Release after required checks pass. It will not close unrelated issues or pull requests, delete human branches, or publish a major or minor Release.'
281
+ : 'This pull request requires owner review and will not auto-merge, publish npm, close unrelated issues or pull requests, or delete unrelated branches.',
282
+ '',
283
+ ].filter((line) => line !== null).join('\n');
284
+ }
285
+
286
+ export function inspectRegistrySyncWorkflow(yaml) {
287
+ const text = String(yaml || '');
288
+ const errors = [];
289
+ if (/^\s{2}pull_request:/m.test(text) || /\npull_request:/m.test(text)) {
290
+ errors.push('registry-sync must not run converter code from pull_request heads');
291
+ }
292
+ if (/github\.event\.pull_request\.head\.sha/.test(text)) {
293
+ errors.push('registry-sync must not check out pull_request.head.sha');
294
+ }
295
+ if (/npm publish/.test(text)) errors.push('registry-sync must not publish npm');
296
+ if (/gh issue close/.test(text)) errors.push('registry-sync must not close issues');
297
+ if (/uses:\s+actions\/(?:checkout|setup-node)@v\d+/.test(text)) {
298
+ errors.push('registry-sync must pin actions by commit SHA');
299
+ }
300
+ const checkoutPin = (text.match(/uses:\s+actions\/checkout@([a-f0-9]{40})/) || [])[1] || null;
301
+ const setupNodePin = (text.match(/uses:\s+actions\/setup-node@([a-f0-9]{40})/) || [])[1] || null;
302
+ if (checkoutPin !== CHECKOUT_ACTION_PIN) errors.push('actions/checkout pin is missing or drifted');
303
+ if (setupNodePin !== SETUP_NODE_ACTION_PIN) errors.push('actions/setup-node pin is missing or drifted');
304
+ if (!/node \.\/src\/registry\/automation-ci\.js generate/.test(text)) {
305
+ errors.push('generate job must run automation-ci.js generate');
306
+ }
307
+ if (!/node \.\/src\/registry\/automation-ci\.js auto-merge/.test(text)) {
308
+ errors.push('auto-merge job must run automation-ci.js auto-merge');
309
+ }
310
+ if (!/EXPECTED_HEAD/.test(text)) errors.push('workflow must pass EXPECTED_HEAD');
311
+ if (!/concurrency:/.test(text)) errors.push('workflow must serialize concurrent runs');
312
+ if (!/permissions:\s*\n\s*\{\}/.test(text) && !/permissions:\s*\{\}/.test(text)) {
313
+ errors.push('workflow default permissions must be empty');
314
+ }
315
+
316
+ const parsePerms = (block) => {
317
+ const perms = {};
318
+ for (const line of String(block || '').split(/\n/)) {
319
+ const match = line.match(/^\s+([a-z-]+):\s+(\w+)\s*$/);
320
+ if (match) perms[match[1]] = match[2];
321
+ }
322
+ return perms;
323
+ };
324
+ const generateMatch = text.match(/generate:[\s\S]*?permissions:\s*\n((?:\s{6,}[^\n]+\n)+)/);
325
+ const generate = { permissions: parsePerms(generateMatch ? generateMatch[1] : '') };
326
+ if (generate.permissions.contents !== 'write') errors.push('generate job must use contents: write');
327
+ if (generate.permissions['pull-requests'] !== 'write') errors.push('generate job must use pull-requests: write');
328
+ if (generate.permissions.issues) errors.push('generate job must not receive issues write');
329
+ if (generate.permissions['id-token']) errors.push('generate job must not receive id-token');
330
+
331
+ const autoMergeMatch = text.match(/auto-merge:[\s\S]*?permissions:\s*\n((?:\s{6,}[^\n]+\n)+)/);
332
+ const autoMerge = { permissions: parsePerms(autoMergeMatch ? autoMergeMatch[1] : '') };
333
+ if (autoMerge.permissions.contents !== 'write') errors.push('auto-merge job must use contents: write');
334
+ if (autoMerge.permissions['pull-requests'] !== 'write') errors.push('auto-merge job must use pull-requests: write');
335
+ if (autoMerge.permissions['id-token'] !== 'write') errors.push('auto-merge job must use id-token: write');
336
+ if (autoMerge.permissions.issues) errors.push('auto-merge job must not receive issues write');
337
+
338
+ const topPerms = {};
339
+ const top = text.match(/^permissions:\s*\n((?:\s{2}[^\n]+\n)+)/m);
340
+ if (top) Object.assign(topPerms, parsePerms(top[1]));
341
+
342
+ return {
343
+ ok: errors.length === 0,
344
+ errors,
345
+ checkoutPin,
346
+ setupNodePin,
347
+ usesFloatingTags: /uses:\s+actions\/(?:checkout|setup-node)@v\d+/.test(text),
348
+ permissions: topPerms,
349
+ generate,
350
+ autoMerge,
351
+ };
352
+ }
353
+
354
+ export async function executeRegistryAutoMerge(args, io = {}) {
355
+ const pr = io.inspectPullRequest ? await io.inspectPullRequest() : null;
356
+ if (!pr) {
357
+ return {
358
+ ok: true,
359
+ dryRun: args.dryRun !== false,
360
+ merged: false,
361
+ published: false,
362
+ branchDeleted: false,
363
+ skipped: true,
364
+ };
365
+ }
366
+
367
+ const expectedHead = args.expectedHead;
368
+ const currentDefaultSha = io.currentDefaultSha ? await io.currentDefaultSha() : expectedHead;
369
+ const concurrentRun = io.hasConcurrentRun ? await io.hasConcurrentRun() : false;
370
+ const fromTrustedWorkflow = io.fromTrustedWorkflow ? await io.fromTrustedWorkflow() : false;
371
+ const defaultBranchProtected = io.defaultBranchProtected ? await io.defaultBranchProtected() : false;
372
+ const classification = io.classification
373
+ ? await io.classification()
374
+ : { autoEligible: false, blockedReasons: ['missing classification'] };
375
+
376
+ const authorization = evaluateAutoAuthorization({
377
+ origin: pr.origin,
378
+ headRef: pr.headRef,
379
+ headSha: pr.headSha,
380
+ expectedGeneratedSha: pr.expectedGeneratedSha,
381
+ expectedHeadSha: expectedHead,
382
+ currentDefaultSha,
383
+ files: pr.files,
384
+ checkConclusion: pr.checkConclusion,
385
+ concurrentRun,
386
+ fromTrustedWorkflow,
387
+ defaultBranchProtected,
388
+ classification,
389
+ });
390
+
391
+ const result = {
392
+ ok: true,
393
+ dryRun: args.dryRun !== false,
394
+ autoAuthorized: authorization.autoAuthorized,
395
+ deniedReasons: authorization.deniedReasons,
396
+ actions: authorization.actions,
397
+ merged: false,
398
+ published: false,
399
+ branchDeleted: false,
400
+ };
401
+
402
+ if (!authorization.autoAuthorized) return result;
403
+ if (args.dryRun !== false) return { ...result, bump: 'patch' };
404
+
405
+ const mergedPr = await io.mergePullRequest(pr);
406
+ const verified = io.verifyMerge
407
+ ? await io.verifyMerge(mergedPr)
408
+ : { ok: Boolean(mergedPr && mergedPr.merged) };
409
+ if (!verified || verified.ok === false) {
410
+ return {
411
+ ...result,
412
+ ok: false,
413
+ errors: ['merge was not verified on the protected default branch'],
414
+ };
415
+ }
416
+ result.merged = true;
417
+
418
+ let lock = { release: async () => {} };
419
+ try {
420
+ if (io.acquireVersionLock) lock = await io.acquireVersionLock();
421
+ } catch (err) {
422
+ return {
423
+ ...result,
424
+ ok: false,
425
+ published: false,
426
+ branchDeleted: false,
427
+ errors: [err.message || String(err)],
428
+ };
429
+ }
430
+ try {
431
+ const publishedState = io.reconcilePublishedState
432
+ ? await io.reconcilePublishedState()
433
+ : { packageVersion: '0.0.0', npmVersions: [], githubVersions: [] };
434
+ const version = calculateRegistryPatchVersion(publishedState);
435
+ const identities = io.commitPatchIdentities
436
+ ? await io.commitPatchIdentities({ version })
437
+ : { commit: verified.sha, version };
438
+ const published = await io.trustedPatchRelease({
439
+ version,
440
+ commit: identities.commit,
441
+ bump: 'patch',
442
+ usesTrustedPatchPrimitive: true,
443
+ });
444
+ if (!published || published.ok === false) {
445
+ return {
446
+ ...result,
447
+ ok: false,
448
+ published: false,
449
+ branchDeleted: false,
450
+ version,
451
+ bump: 'patch',
452
+ errors: published?.errors || ['trusted patch Release failed'],
453
+ recovery: published?.recovery,
454
+ };
455
+ }
456
+ const publication = io.verifyPublication
457
+ ? await io.verifyPublication(published)
458
+ : { ok: true };
459
+ if (!publication.ok) {
460
+ return {
461
+ ...result,
462
+ ok: false,
463
+ published: false,
464
+ branchDeleted: false,
465
+ version,
466
+ bump: 'patch',
467
+ errors: ['publication did not match npm and GitHub identities'],
468
+ recovery: published.recovery,
469
+ };
470
+ }
471
+ result.published = true;
472
+ result.version = version;
473
+ result.bump = 'patch';
474
+ result.recovery = published.recovery;
475
+ if (io.deleteBranch) await io.deleteBranch(pr.headRef);
476
+ result.branchDeleted = true;
477
+ return result;
478
+ } finally {
479
+ if (lock.release) await lock.release();
480
+ }
481
+ }
482
+
483
+ export async function executeRegistryAutomation(args, io = {}) {
484
+ if (args.mode === 'auto-merge') return executeRegistryAutoMerge(args, io);
485
+ if (args.mode === 'cleanup') {
486
+ const branches = io.listBranches ? await io.listBranches() : [];
487
+ const protectedRefs = io.protectedRefs ? await io.protectedRefs() : ['main'];
488
+ const plan = planGeneratedBranchCleanup({ dryRun: args.dryRun !== false, branches, protectedRefs });
489
+ if (args.dryRun !== false) return plan;
490
+ if (io.deleteBranch) {
491
+ for (const name of plan.wouldDelete) await io.deleteBranch(name);
492
+ }
493
+ return plan;
494
+ }
495
+
496
+ const expectedHead = args.expectedHead;
497
+ const converterRevision = io.converterSha ? await io.converterSha() : expectedHead;
498
+ const currentHeadSha = io.currentDefaultSha ? await io.currentDefaultSha() : expectedHead;
499
+ const upstreamRevision = args.upstreamRevision
500
+ || (io.resolveUpstreamRevision ? await io.resolveUpstreamRevision() : null);
501
+ const upstreamText = await io.fetchUpstreamText(upstreamRevision);
502
+ const previousPin = await io.readPin();
503
+ const previousSnapshot = await io.readSnapshot();
504
+ const concurrentRun = io.hasConcurrentRun ? await io.hasConcurrentRun() : false;
505
+
506
+ const plan = planRegistrySync({
507
+ expectedHeadSha: expectedHead,
508
+ currentHeadSha,
509
+ converterRevision,
510
+ upstreamRevision,
511
+ upstreamText,
512
+ previousSnapshot,
513
+ previousPin,
514
+ });
515
+
516
+ const authorization = evaluateAutoAuthorization({
517
+ origin: 'same-repo',
518
+ headRef: `${GENERATED_BRANCH_PREFIX}${(upstreamRevision || '').slice(0, 12)}`,
519
+ headSha: args.dryRun ? expectedHead : null,
520
+ expectedGeneratedSha: args.dryRun ? expectedHead : null,
521
+ expectedHeadSha: expectedHead,
522
+ currentDefaultSha: currentHeadSha,
523
+ files: plan.files || [],
524
+ checkConclusion: args.dryRun ? 'success' : 'pending',
525
+ concurrentRun,
526
+ classification: plan.classification || { autoEligible: false, blockedReasons: plan.errors || [] },
527
+ });
528
+
529
+ const body = formatRegistryPrBody({
530
+ ...plan,
531
+ upstreamRevision,
532
+ expectedHeadSha: expectedHead,
533
+ });
534
+
535
+ const result = {
536
+ ...plan,
537
+ dryRun: args.dryRun !== false,
538
+ body,
539
+ autoAuthorization: authorization,
540
+ actions: {
541
+ merge: false,
542
+ publishNpm: false,
543
+ ...BLOCKED_MUTATIONS,
544
+ },
545
+ merged: false,
546
+ published: false,
547
+ };
548
+
549
+ if (args.dryRun !== false) return result;
550
+ if (!plan.ok || !plan.hasChanges || concurrentRun) return result;
551
+ if (io.writeAllowlisted) await io.writeAllowlisted(plan);
552
+ if (io.createPullRequest) await io.createPullRequest({ ...result, body });
553
+ return result;
554
+ }
@@ -0,0 +1,149 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * diff.js — normalized semantic diff between two attributed host registries,
5
+ * with a safe/review classification. Description-only changes and safe
6
+ * additions are classified safe; path, removal, global, detection, platform,
7
+ * alias, universal, and source changes are classified review
8
+ * (authority-sensitive).
9
+ */
10
+
11
+ import { validateHost } from './validate.js';
12
+ import { compareIds } from './normalize.js';
13
+
14
+ /** Canonical destination shape used for comparison (drops raw text noise). */
15
+ function canonicalDestination(dest) {
16
+ if (!dest) return dest;
17
+ switch (dest.kind) {
18
+ case 'literal':
19
+ return { kind: 'literal', path: dest.path };
20
+ case 'join':
21
+ return { kind: 'join', base: dest.base, segments: dest.segments };
22
+ case 'conditional':
23
+ return {
24
+ kind: 'conditional',
25
+ base: dest.base,
26
+ cases: dest.cases.map((c) => ({ probe: c.probe, formula: canonicalDestination(c.formula) })),
27
+ };
28
+ default:
29
+ return { kind: dest.kind };
30
+ }
31
+ }
32
+
33
+ const REVIEW_FIELDS = new Set([
34
+ 'name',
35
+ 'universal',
36
+ 'universalPrompt',
37
+ 'project',
38
+ 'global',
39
+ 'detection',
40
+ 'platforms',
41
+ 'aliases',
42
+ ]);
43
+
44
+ /**
45
+ * Compare two hosts that share the same id.
46
+ * Returns { kind, authority, fields } where kind is one of
47
+ * 'description-change' | 'change', and fields lists each changed dimension.
48
+ */
49
+ export function diffHost(prev, curr) {
50
+ const fields = [];
51
+
52
+ if (prev.name !== curr.name) fields.push('name');
53
+ if (prev.displayName !== curr.displayName) fields.push('displayName');
54
+ if (prev.universal !== curr.universal) fields.push('universal');
55
+ if (prev.universalPrompt !== curr.universalPrompt) fields.push('universalPrompt');
56
+ if (JSON.stringify(canonicalDestination(prev.destinations.project)) !==
57
+ JSON.stringify(canonicalDestination(curr.destinations.project))) {
58
+ fields.push('project');
59
+ }
60
+ if (JSON.stringify(canonicalDestination(prev.destinations.global)) !==
61
+ JSON.stringify(canonicalDestination(curr.destinations.global))) {
62
+ fields.push('global');
63
+ }
64
+ if (JSON.stringify(prev.detection && prev.detection.envVars) !==
65
+ JSON.stringify(curr.detection && curr.detection.envVars)) {
66
+ fields.push('detection');
67
+ }
68
+ if (JSON.stringify(prev.platforms) !== JSON.stringify(curr.platforms)) fields.push('platforms');
69
+ if (JSON.stringify(prev.aliases) !== JSON.stringify(curr.aliases)) fields.push('aliases');
70
+
71
+ const onlyDescription =
72
+ fields.length > 0 && fields.every((f) => f === 'displayName');
73
+ const authority = fields.some((f) => REVIEW_FIELDS.has(f)) ? 'review' : 'safe';
74
+ const kind = onlyDescription ? 'description-change' : 'change';
75
+ return { kind, authority, fields };
76
+ }
77
+
78
+ /** Is a host safe to add? Safe-add = the host passes validation. */
79
+ export function isSafeAddition(host) {
80
+ return validateHost(host) === null;
81
+ }
82
+
83
+ /**
84
+ * Compute the normalized diff between previous and current snapshots.
85
+ * Returns { summary, changes } with changes sorted bytewise by id.
86
+ */
87
+ export function diffSnapshots(previous, current) {
88
+ const prevById = new Map((previous.hosts || []).map((h) => [h.id, h]));
89
+ const currById = new Map((current.hosts || []).map((h) => [h.id, h]));
90
+ const changes = [];
91
+
92
+ for (const [id, curr] of currById) {
93
+ const prev = prevById.get(id);
94
+ if (!prev) {
95
+ changes.push({
96
+ id,
97
+ kind: 'addition',
98
+ authority: isSafeAddition(curr) ? 'safe' : 'review',
99
+ previous: null,
100
+ current: curr,
101
+ });
102
+ } else {
103
+ const { kind, authority, fields } = diffHost(prev, curr);
104
+ if (fields.length > 0) {
105
+ changes.push({ id, kind, authority, fields, previous: prev, current: curr });
106
+ }
107
+ }
108
+ }
109
+
110
+ for (const [id, prev] of prevById) {
111
+ if (!currById.has(id)) {
112
+ changes.push({
113
+ id,
114
+ kind: 'removal',
115
+ authority: 'review',
116
+ previous: prev,
117
+ current: null,
118
+ });
119
+ }
120
+ }
121
+
122
+ // Source pin / schema metadata drift is authority-sensitive.
123
+ const sourceDrift = JSON.stringify(previous.generatedFrom) !== JSON.stringify(current.generatedFrom);
124
+ if (sourceDrift) {
125
+ changes.push({
126
+ id: '<source>',
127
+ kind: 'source-change',
128
+ authority: 'review',
129
+ fields: ['generatedFrom'],
130
+ previous: previous.generatedFrom,
131
+ current: current.generatedFrom,
132
+ });
133
+ }
134
+
135
+ changes.sort((a, b) => {
136
+ const byId = compareIds(a.id, b.id);
137
+ return byId !== 0 ? byId : a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0;
138
+ });
139
+
140
+ const summary = {
141
+ added: changes.filter((c) => c.kind === 'addition').length,
142
+ removed: changes.filter((c) => c.kind === 'removal').length,
143
+ changed: changes.filter((c) => c.kind !== 'addition' && c.kind !== 'removal' && c.kind !== 'source-change').length,
144
+ safe: changes.filter((c) => c.authority === 'safe').length,
145
+ review: changes.filter((c) => c.authority === 'review').length,
146
+ };
147
+
148
+ return { summary, changes };
149
+ }