featuredrop 1.2.0 → 1.3.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 (84) hide show
  1. package/README.md +171 -0
  2. package/dist/admin.cjs +212 -0
  3. package/dist/admin.cjs.map +1 -0
  4. package/dist/admin.d.cts +176 -0
  5. package/dist/admin.d.ts +176 -0
  6. package/dist/admin.js +207 -0
  7. package/dist/admin.js.map +1 -0
  8. package/dist/angular.cjs +13 -3
  9. package/dist/angular.cjs.map +1 -1
  10. package/dist/angular.d.cts +4 -0
  11. package/dist/angular.d.ts +4 -0
  12. package/dist/angular.js +13 -3
  13. package/dist/angular.js.map +1 -1
  14. package/dist/bridges.cjs +401 -0
  15. package/dist/bridges.cjs.map +1 -0
  16. package/dist/bridges.d.cts +194 -0
  17. package/dist/bridges.d.ts +194 -0
  18. package/dist/bridges.js +394 -0
  19. package/dist/bridges.js.map +1 -0
  20. package/dist/ci.cjs +328 -0
  21. package/dist/ci.cjs.map +1 -0
  22. package/dist/ci.d.cts +176 -0
  23. package/dist/ci.d.ts +176 -0
  24. package/dist/ci.js +324 -0
  25. package/dist/ci.js.map +1 -0
  26. package/dist/featuredrop.cjs +139 -18
  27. package/dist/featuredrop.cjs.map +1 -1
  28. package/dist/flags.cjs +51 -0
  29. package/dist/flags.cjs.map +1 -0
  30. package/dist/flags.d.cts +48 -0
  31. package/dist/flags.d.ts +48 -0
  32. package/dist/flags.js +47 -0
  33. package/dist/flags.js.map +1 -0
  34. package/dist/index.cjs +2583 -665
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +743 -206
  37. package/dist/index.d.ts +743 -206
  38. package/dist/index.js +2552 -666
  39. package/dist/index.js.map +1 -1
  40. package/dist/preact.cjs +710 -209
  41. package/dist/preact.cjs.map +1 -1
  42. package/dist/preact.d.cts +67 -120
  43. package/dist/preact.d.ts +67 -120
  44. package/dist/preact.js +696 -195
  45. package/dist/preact.js.map +1 -1
  46. package/dist/react.cjs +710 -209
  47. package/dist/react.cjs.map +1 -1
  48. package/dist/react.d.cts +67 -120
  49. package/dist/react.d.ts +67 -120
  50. package/dist/react.js +696 -195
  51. package/dist/react.js.map +1 -1
  52. package/dist/schema.cjs +78 -1
  53. package/dist/schema.cjs.map +1 -1
  54. package/dist/schema.d.cts +142 -0
  55. package/dist/schema.d.ts +142 -0
  56. package/dist/schema.js +78 -1
  57. package/dist/schema.js.map +1 -1
  58. package/dist/solid.cjs +13 -3
  59. package/dist/solid.cjs.map +1 -1
  60. package/dist/solid.d.cts +4 -0
  61. package/dist/solid.d.ts +4 -0
  62. package/dist/solid.js +13 -3
  63. package/dist/solid.js.map +1 -1
  64. package/dist/svelte.cjs +13 -3
  65. package/dist/svelte.cjs.map +1 -1
  66. package/dist/svelte.js +13 -3
  67. package/dist/svelte.js.map +1 -1
  68. package/dist/testing.cjs +136 -15
  69. package/dist/testing.cjs.map +1 -1
  70. package/dist/testing.d.cts +22 -0
  71. package/dist/testing.d.ts +22 -0
  72. package/dist/testing.js +136 -15
  73. package/dist/testing.js.map +1 -1
  74. package/dist/vue.cjs +13 -3
  75. package/dist/vue.cjs.map +1 -1
  76. package/dist/vue.js +13 -3
  77. package/dist/vue.js.map +1 -1
  78. package/dist/web-components.cjs +14 -4
  79. package/dist/web-components.cjs.map +1 -1
  80. package/dist/web-components.d.cts +4 -0
  81. package/dist/web-components.d.ts +4 -0
  82. package/dist/web-components.js +14 -4
  83. package/dist/web-components.js.map +1 -1
  84. package/package.json +59 -1
package/dist/ci.cjs ADDED
@@ -0,0 +1,328 @@
1
+ 'use strict';
2
+
3
+ var zod = require('zod');
4
+
5
+ // src/dependencies.ts
6
+ function getDirectDependencies(feature) {
7
+ const dependsOn = feature.dependsOn;
8
+ if (!dependsOn) return [];
9
+ const seen = dependsOn.seen ?? [];
10
+ const clicked = dependsOn.clicked ?? [];
11
+ const dismissed = dependsOn.dismissed ?? [];
12
+ const unique = /* @__PURE__ */ new Set();
13
+ for (const id of [...seen, ...clicked, ...dismissed]) {
14
+ if (id) unique.add(id);
15
+ }
16
+ return Array.from(unique);
17
+ }
18
+ function hasDependencyCycle(manifest) {
19
+ const ids = new Set(manifest.map((feature) => feature.id));
20
+ const outgoing = /* @__PURE__ */ new Map();
21
+ const indegree = /* @__PURE__ */ new Map();
22
+ for (const feature of manifest) {
23
+ outgoing.set(feature.id, /* @__PURE__ */ new Set());
24
+ indegree.set(feature.id, 0);
25
+ }
26
+ for (const feature of manifest) {
27
+ for (const dependencyId of getDirectDependencies(feature)) {
28
+ if (!ids.has(dependencyId)) continue;
29
+ const edges = outgoing.get(dependencyId);
30
+ if (!edges || edges.has(feature.id)) continue;
31
+ edges.add(feature.id);
32
+ indegree.set(feature.id, (indegree.get(feature.id) ?? 0) + 1);
33
+ }
34
+ }
35
+ const queue = [];
36
+ for (const feature of manifest) {
37
+ if ((indegree.get(feature.id) ?? 0) === 0) queue.push(feature.id);
38
+ }
39
+ let visited = 0;
40
+ while (queue.length > 0) {
41
+ const id = queue.shift();
42
+ if (!id) continue;
43
+ visited += 1;
44
+ const edges = outgoing.get(id);
45
+ if (!edges) continue;
46
+ for (const nextId of edges) {
47
+ const nextDegree = (indegree.get(nextId) ?? 0) - 1;
48
+ indegree.set(nextId, nextDegree);
49
+ if (nextDegree === 0) queue.push(nextId);
50
+ }
51
+ }
52
+ return visited !== manifest.length;
53
+ }
54
+ function isRecord(value) {
55
+ return !!value && typeof value === "object" && !Array.isArray(value);
56
+ }
57
+ function isValidDate(value) {
58
+ return Number.isFinite(new Date(value).getTime());
59
+ }
60
+ var nonEmptyString = zod.z.string().trim().min(1, "must be a non-empty string");
61
+ var isoDateString = nonEmptyString.refine(isValidDate, {
62
+ message: "must be a valid date",
63
+ params: { featuredropCode: "invalid_date" }
64
+ });
65
+ var dependsOnSchema = zod.z.object({
66
+ seen: zod.z.array(zod.z.string()).optional(),
67
+ clicked: zod.z.array(zod.z.string()).optional(),
68
+ dismissed: zod.z.array(zod.z.string()).optional()
69
+ }).optional();
70
+ var ctaSchema = zod.z.object({
71
+ label: nonEmptyString,
72
+ url: nonEmptyString
73
+ }).optional();
74
+ var featureEntrySchema = zod.z.object({
75
+ id: nonEmptyString,
76
+ label: nonEmptyString,
77
+ releasedAt: isoDateString,
78
+ showNewUntil: isoDateString,
79
+ description: zod.z.string().optional(),
80
+ flagKey: zod.z.string().optional(),
81
+ product: zod.z.string().optional(),
82
+ url: zod.z.string().optional(),
83
+ image: zod.z.string().optional(),
84
+ type: zod.z.enum(["feature", "improvement", "fix", "breaking"]).optional(),
85
+ priority: zod.z.enum(["critical", "normal", "low"]).optional(),
86
+ cta: ctaSchema,
87
+ meta: zod.z.record(zod.z.unknown()).optional(),
88
+ dependsOn: dependsOnSchema
89
+ }).passthrough();
90
+ zod.z.array(featureEntrySchema);
91
+ function toIssuePath(path) {
92
+ if (path.length === 0) return "$";
93
+ let output = "";
94
+ for (const part of path) {
95
+ if (typeof part === "number") output += `[${part}]`;
96
+ else output += output ? `.${part}` : part;
97
+ }
98
+ return output;
99
+ }
100
+ function mapZodIssue(issue) {
101
+ const codeParam = issue.params?.featuredropCode;
102
+ if (codeParam === "invalid_date") {
103
+ return {
104
+ path: toIssuePath(issue.path),
105
+ message: issue.message,
106
+ code: "invalid_date"
107
+ };
108
+ }
109
+ if (issue.code === "invalid_type") {
110
+ return {
111
+ path: toIssuePath(issue.path),
112
+ message: issue.message,
113
+ code: issue.received === "undefined" ? "missing_required" : "invalid_type"
114
+ };
115
+ }
116
+ return {
117
+ path: toIssuePath(issue.path),
118
+ message: issue.message,
119
+ code: "invalid_value"
120
+ };
121
+ }
122
+ var UNSAFE_META_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
123
+ function isSafeUrl(value) {
124
+ const normalized = value.trim();
125
+ if (!normalized) return false;
126
+ if (/^(\/|\.\/|\.\.\/|\?|#)/.test(normalized)) return true;
127
+ if (/^https?:\/\//i.test(normalized)) return true;
128
+ return false;
129
+ }
130
+ function findUnsafeMetaPath(value, path = "meta") {
131
+ if (Array.isArray(value)) {
132
+ for (let index = 0; index < value.length; index++) {
133
+ const nested = findUnsafeMetaPath(value[index], `${path}[${index}]`);
134
+ if (nested) return nested;
135
+ }
136
+ return null;
137
+ }
138
+ if (!isRecord(value)) return null;
139
+ for (const [key, nestedValue] of Object.entries(value)) {
140
+ if (UNSAFE_META_KEYS.has(key)) {
141
+ return `${path}.${key}`;
142
+ }
143
+ const nested = findUnsafeMetaPath(nestedValue, `${path}.${key}`);
144
+ if (nested) return nested;
145
+ }
146
+ return null;
147
+ }
148
+ function validateFeatureEntry(raw, index) {
149
+ if (!isRecord(raw)) {
150
+ return {
151
+ issues: [
152
+ {
153
+ path: `[${index}]`,
154
+ message: "Feature entry must be an object",
155
+ code: "invalid_type"
156
+ }
157
+ ]
158
+ };
159
+ }
160
+ const parsed = featureEntrySchema.safeParse(raw);
161
+ if (!parsed.success) {
162
+ return {
163
+ issues: parsed.error.issues.map((issue) => ({
164
+ ...mapZodIssue(issue),
165
+ path: `[${index}]${issue.path.length > 0 ? `.${toIssuePath(issue.path)}` : ""}`
166
+ }))
167
+ };
168
+ }
169
+ return {
170
+ issues: [],
171
+ entry: parsed.data
172
+ };
173
+ }
174
+ function validateManifest(data) {
175
+ const errors = [];
176
+ if (!Array.isArray(data)) {
177
+ return {
178
+ valid: false,
179
+ errors: [
180
+ {
181
+ path: "$",
182
+ message: "Manifest must be an array",
183
+ code: "invalid_type"
184
+ }
185
+ ]
186
+ };
187
+ }
188
+ const entries = [];
189
+ const seenIds = /* @__PURE__ */ new Set();
190
+ data.forEach((item, index) => {
191
+ const result = validateFeatureEntry(item, index);
192
+ errors.push(...result.issues);
193
+ if (!result.entry) return;
194
+ if (seenIds.has(result.entry.id)) {
195
+ errors.push({
196
+ path: `[${index}].id`,
197
+ message: `Duplicate feature id "${result.entry.id}"`,
198
+ code: "duplicate_id"
199
+ });
200
+ return;
201
+ }
202
+ seenIds.add(result.entry.id);
203
+ entries.push(result.entry);
204
+ });
205
+ if (entries.length > 0 && hasDependencyCycle(entries)) {
206
+ errors.push({
207
+ path: "$",
208
+ message: "Circular dependsOn relationship detected",
209
+ code: "circular_dependency"
210
+ });
211
+ }
212
+ for (let index = 0; index < entries.length; index++) {
213
+ const entry = entries[index];
214
+ if (new Date(entry.showNewUntil).getTime() <= new Date(entry.releasedAt).getTime()) {
215
+ errors.push({
216
+ path: `[${index}].showNewUntil`,
217
+ message: "showNewUntil must be after releasedAt",
218
+ code: "invalid_value"
219
+ });
220
+ }
221
+ if (entry.url && !isSafeUrl(entry.url)) {
222
+ errors.push({
223
+ path: `[${index}].url`,
224
+ message: "url must be http, https, or relative",
225
+ code: "invalid_value"
226
+ });
227
+ }
228
+ if (entry.image && !isSafeUrl(entry.image)) {
229
+ errors.push({
230
+ path: `[${index}].image`,
231
+ message: "image must be http, https, or relative",
232
+ code: "invalid_value"
233
+ });
234
+ }
235
+ if (entry.cta?.url && !isSafeUrl(entry.cta.url)) {
236
+ errors.push({
237
+ path: `[${index}].cta.url`,
238
+ message: "cta.url must be http, https, or relative",
239
+ code: "invalid_value"
240
+ });
241
+ }
242
+ const unsafeMetaPath = findUnsafeMetaPath(entry.meta);
243
+ if (unsafeMetaPath) {
244
+ errors.push({
245
+ path: `[${index}].${unsafeMetaPath}`,
246
+ message: `meta contains unsafe key "${unsafeMetaPath.split(".").pop()}"`,
247
+ code: "invalid_value"
248
+ });
249
+ }
250
+ }
251
+ return {
252
+ valid: errors.length === 0,
253
+ errors
254
+ };
255
+ }
256
+
257
+ // src/ci.ts
258
+ function isRecord2(value) {
259
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
260
+ }
261
+ function collectChangedFields(beforeValue, afterValue, path, output) {
262
+ if (beforeValue === afterValue) return;
263
+ if (Array.isArray(beforeValue) && Array.isArray(afterValue)) {
264
+ if (beforeValue.length !== afterValue.length) {
265
+ output.push(path);
266
+ return;
267
+ }
268
+ for (let i = 0; i < beforeValue.length; i += 1) {
269
+ collectChangedFields(beforeValue[i], afterValue[i], `${path}[${i}]`, output);
270
+ }
271
+ return;
272
+ }
273
+ if (isRecord2(beforeValue) && isRecord2(afterValue)) {
274
+ const keys = /* @__PURE__ */ new Set([...Object.keys(beforeValue), ...Object.keys(afterValue)]);
275
+ keys.forEach((key) => {
276
+ const nextPath = path ? `${path}.${key}` : key;
277
+ collectChangedFields(beforeValue[key], afterValue[key], nextPath, output);
278
+ });
279
+ return;
280
+ }
281
+ output.push(path);
282
+ }
283
+ function diffManifest(before, after) {
284
+ const beforeById = new Map(before.map((feature) => [feature.id, feature]));
285
+ const afterById = new Map(after.map((feature) => [feature.id, feature]));
286
+ const added = after.filter((feature) => !beforeById.has(feature.id));
287
+ const removed = before.filter((feature) => !afterById.has(feature.id));
288
+ const changed = after.filter((feature) => beforeById.has(feature.id)).map((feature) => {
289
+ const previous = beforeById.get(feature.id);
290
+ if (!previous) return null;
291
+ const changedFields = [];
292
+ collectChangedFields(previous, feature, "", changedFields);
293
+ if (changedFields.length === 0) return null;
294
+ return {
295
+ id: feature.id,
296
+ before: previous,
297
+ after: feature,
298
+ changedFields
299
+ };
300
+ }).filter((item) => item !== null);
301
+ return { added, removed, changed };
302
+ }
303
+ function generateChangelogDiff(diff, options = {}) {
304
+ const parts = [];
305
+ if (diff.added.length > 0) {
306
+ parts.push(`Added: ${diff.added.map((feature) => feature.label).join(", ")}`);
307
+ }
308
+ if (diff.changed.length > 0) {
309
+ const changedText = diff.changed.map((item) => {
310
+ if (!options.includeFieldChanges) return item.after.label;
311
+ return `${item.after.label} [${item.changedFields.join(", ")}]`;
312
+ });
313
+ parts.push(`Changed: ${changedText.join(", ")}`);
314
+ }
315
+ if (diff.removed.length > 0) {
316
+ parts.push(`Removed: ${diff.removed.map((feature) => feature.label).join(", ")}`);
317
+ }
318
+ return parts.length > 0 ? parts.join(". ") : "No manifest changes.";
319
+ }
320
+ function validateManifestForCI(manifest) {
321
+ return validateManifest(manifest);
322
+ }
323
+
324
+ exports.diffManifest = diffManifest;
325
+ exports.generateChangelogDiff = generateChangelogDiff;
326
+ exports.validateManifestForCI = validateManifestForCI;
327
+ //# sourceMappingURL=ci.cjs.map
328
+ //# sourceMappingURL=ci.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dependencies.ts","../src/schema.ts","../src/ci.ts"],"names":["z","isRecord"],"mappings":";;;;;AAEA,SAAS,sBAAsB,OAAA,EAAiC;AAC9D,EAAA,MAAM,YAAY,OAAA,CAAQ,SAAA;AAC1B,EAAA,IAAI,CAAC,SAAA,EAAW,OAAO,EAAC;AACxB,EAAA,MAAM,IAAA,GAAO,SAAA,CAAU,IAAA,IAAQ,EAAC;AAChC,EAAA,MAAM,OAAA,GAAU,SAAA,CAAU,OAAA,IAAW,EAAC;AACtC,EAAA,MAAM,SAAA,GAAY,SAAA,CAAU,SAAA,IAAa,EAAC;AAC1C,EAAA,MAAM,MAAA,uBAAa,GAAA,EAAY;AAC/B,EAAA,KAAA,MAAW,EAAA,IAAM,CAAC,GAAG,IAAA,EAAM,GAAG,OAAA,EAAS,GAAG,SAAS,CAAA,EAAG;AACpD,IAAA,IAAI,EAAA,EAAI,MAAA,CAAO,GAAA,CAAI,EAAE,CAAA;AAAA,EACvB;AACA,EAAA,OAAO,KAAA,CAAM,KAAK,MAAM,CAAA;AAC1B;AAqDO,SAAS,mBAAmB,QAAA,EAAoC;AACrE,EAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,QAAA,CAAS,IAAI,CAAC,OAAA,KAAY,OAAA,CAAQ,EAAE,CAAC,CAAA;AACzD,EAAA,MAAM,QAAA,uBAAe,GAAA,EAAyB;AAC9C,EAAA,MAAM,QAAA,uBAAe,GAAA,EAAoB;AAEzC,EAAA,KAAA,MAAW,WAAW,QAAA,EAAU;AAC9B,IAAA,QAAA,CAAS,GAAA,CAAI,OAAA,CAAQ,EAAA,kBAAI,IAAI,KAAK,CAAA;AAClC,IAAA,QAAA,CAAS,GAAA,CAAI,OAAA,CAAQ,EAAA,EAAI,CAAC,CAAA;AAAA,EAC5B;AAEA,EAAA,KAAA,MAAW,WAAW,QAAA,EAAU;AAC9B,IAAA,KAAA,MAAW,YAAA,IAAgB,qBAAA,CAAsB,OAAO,CAAA,EAAG;AACzD,MAAA,IAAI,CAAC,GAAA,CAAI,GAAA,CAAI,YAAY,CAAA,EAAG;AAC5B,MAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,GAAA,CAAI,YAAY,CAAA;AACvC,MAAA,IAAI,CAAC,KAAA,IAAS,KAAA,CAAM,GAAA,CAAI,OAAA,CAAQ,EAAE,CAAA,EAAG;AACrC,MAAA,KAAA,CAAM,GAAA,CAAI,QAAQ,EAAE,CAAA;AACpB,MAAA,QAAA,CAAS,GAAA,CAAI,QAAQ,EAAA,EAAA,CAAK,QAAA,CAAS,IAAI,OAAA,CAAQ,EAAE,CAAA,IAAK,CAAA,IAAK,CAAC,CAAA;AAAA,IAC9D;AAAA,EACF;AAEA,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,KAAA,MAAW,WAAW,QAAA,EAAU;AAC9B,IAAA,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,OAAA,CAAQ,EAAE,CAAA,IAAK,OAAO,CAAA,EAAG,KAAA,CAAM,IAAA,CAAK,OAAA,CAAQ,EAAE,CAAA;AAAA,EAClE;AAEA,EAAA,IAAI,OAAA,GAAU,CAAA;AACd,EAAA,OAAO,KAAA,CAAM,SAAS,CAAA,EAAG;AACvB,IAAA,MAAM,EAAA,GAAK,MAAM,KAAA,EAAM;AACvB,IAAA,IAAI,CAAC,EAAA,EAAI;AACT,IAAA,OAAA,IAAW,CAAA;AACX,IAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,GAAA,CAAI,EAAE,CAAA;AAC7B,IAAA,IAAI,CAAC,KAAA,EAAO;AACZ,IAAA,KAAA,MAAW,UAAU,KAAA,EAAO;AAC1B,MAAA,MAAM,UAAA,GAAA,CAAc,QAAA,CAAS,GAAA,CAAI,MAAM,KAAK,CAAA,IAAK,CAAA;AACjD,MAAA,QAAA,CAAS,GAAA,CAAI,QAAQ,UAAU,CAAA;AAC/B,MAAA,IAAI,UAAA,KAAe,CAAA,EAAG,KAAA,CAAM,IAAA,CAAK,MAAM,CAAA;AAAA,IACzC;AAAA,EACF;AAEA,EAAA,OAAO,YAAY,QAAA,CAAS,MAAA;AAC9B;ACtDA,SAAS,SAAS,KAAA,EAAkD;AAClE,EAAA,OAAO,CAAC,CAAC,KAAA,IAAS,OAAO,UAAU,QAAA,IAAY,CAAC,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA;AACrE;AAEA,SAAS,YAAY,KAAA,EAAwB;AAC3C,EAAA,OAAO,OAAO,QAAA,CAAS,IAAI,KAAK,KAAK,CAAA,CAAE,SAAS,CAAA;AAClD;AAEA,IAAM,cAAA,GAAiBA,MAAE,MAAA,EAAO,CAAE,MAAK,CAAE,GAAA,CAAI,GAAG,4BAA4B,CAAA;AAE5E,IAAM,aAAA,GAAgB,cAAA,CAAe,MAAA,CAAO,WAAA,EAAa;AAAA,EACvD,OAAA,EAAS,sBAAA;AAAA,EACT,MAAA,EAAQ,EAAE,eAAA,EAAiB,cAAA;AAC7B,CAAC,CAAA;AAED,IAAM,eAAA,GAAkBA,MACrB,MAAA,CAAO;AAAA,EACN,MAAMA,KAAA,CAAE,KAAA,CAAMA,MAAE,MAAA,EAAQ,EAAE,QAAA,EAAS;AAAA,EACnC,SAASA,KAAA,CAAE,KAAA,CAAMA,MAAE,MAAA,EAAQ,EAAE,QAAA,EAAS;AAAA,EACtC,WAAWA,KAAA,CAAE,KAAA,CAAMA,MAAE,MAAA,EAAQ,EAAE,QAAA;AACjC,CAAC,EACA,QAAA,EAAS;AAEZ,IAAM,SAAA,GAAYA,MACf,MAAA,CAAO;AAAA,EACN,KAAA,EAAO,cAAA;AAAA,EACP,GAAA,EAAK;AACP,CAAC,EACA,QAAA,EAAS;AAEL,IAAM,kBAAA,GAAqBA,MAC/B,MAAA,CAAO;AAAA,EACN,EAAA,EAAI,cAAA;AAAA,EACJ,KAAA,EAAO,cAAA;AAAA,EACP,UAAA,EAAY,aAAA;AAAA,EACZ,YAAA,EAAc,aAAA;AAAA,EACd,WAAA,EAAaA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EACjC,OAAA,EAASA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EAC7B,OAAA,EAASA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EAC7B,GAAA,EAAKA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EACzB,KAAA,EAAOA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EAC3B,IAAA,EAAMA,KAAA,CAAE,IAAA,CAAK,CAAC,SAAA,EAAW,eAAe,KAAA,EAAO,UAAU,CAAC,CAAA,CAAE,QAAA,EAAS;AAAA,EACrE,QAAA,EAAUA,MAAE,IAAA,CAAK,CAAC,YAAY,QAAA,EAAU,KAAK,CAAC,CAAA,CAAE,QAAA,EAAS;AAAA,EACzD,GAAA,EAAK,SAAA;AAAA,EACL,MAAMA,KAAA,CAAE,MAAA,CAAOA,MAAE,OAAA,EAAS,EAAE,QAAA,EAAS;AAAA,EACrC,SAAA,EAAW;AACb,CAAC,EACA,WAAA,EAAY;AAEsBA,KAAA,CAAE,KAAA,CAAM,kBAAkB;AAE/D,SAAS,YAAY,IAAA,EAAsC;AACzD,EAAA,IAAI,IAAA,CAAK,MAAA,KAAW,CAAA,EAAG,OAAO,GAAA;AAC9B,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,KAAA,MAAW,QAAQ,IAAA,EAAM;AACvB,IAAA,IAAI,OAAO,IAAA,KAAS,QAAA,EAAU,MAAA,IAAU,IAAI,IAAI,CAAA,CAAA,CAAA;AAAA,SAC3C,MAAA,IAAU,MAAA,GAAS,CAAA,CAAA,EAAI,IAAI,CAAA,CAAA,GAAK,IAAA;AAAA,EACvC;AACA,EAAA,OAAO,MAAA;AACT;AAEA,SAAS,YAAY,KAAA,EAAoC;AACvD,EAAA,MAAM,SAAA,GAAa,MAA+C,MAAA,EAAQ,eAAA;AAC1E,EAAA,IAAI,cAAc,cAAA,EAAgB;AAChC,IAAA,OAAO;AAAA,MACL,IAAA,EAAM,WAAA,CAAY,KAAA,CAAM,IAAI,CAAA;AAAA,MAC5B,SAAS,KAAA,CAAM,OAAA;AAAA,MACf,IAAA,EAAM;AAAA,KACR;AAAA,EACF;AACA,EAAA,IAAI,KAAA,CAAM,SAAS,cAAA,EAAgB;AACjC,IAAA,OAAO;AAAA,MACL,IAAA,EAAM,WAAA,CAAY,KAAA,CAAM,IAAI,CAAA;AAAA,MAC5B,SAAS,KAAA,CAAM,OAAA;AAAA,MACf,IAAA,EAAM,KAAA,CAAM,QAAA,KAAa,WAAA,GAAc,kBAAA,GAAqB;AAAA,KAC9D;AAAA,EACF;AACA,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,WAAA,CAAY,KAAA,CAAM,IAAI,CAAA;AAAA,IAC5B,SAAS,KAAA,CAAM,OAAA;AAAA,IACf,IAAA,EAAM;AAAA,GACR;AACF;AAEA,IAAM,mCAAmB,IAAI,GAAA,CAAI,CAAC,WAAA,EAAa,aAAA,EAAe,WAAW,CAAC,CAAA;AAE1E,SAAS,UAAU,KAAA,EAAwB;AACzC,EAAA,MAAM,UAAA,GAAa,MAAM,IAAA,EAAK;AAC9B,EAAA,IAAI,CAAC,YAAY,OAAO,KAAA;AACxB,EAAA,IAAI,wBAAA,CAAyB,IAAA,CAAK,UAAU,CAAA,EAAG,OAAO,IAAA;AACtD,EAAA,IAAI,eAAA,CAAgB,IAAA,CAAK,UAAU,CAAA,EAAG,OAAO,IAAA;AAC7C,EAAA,OAAO,KAAA;AACT;AAEA,SAAS,kBAAA,CAAmB,KAAA,EAAgB,IAAA,GAAO,MAAA,EAAuB;AACxE,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACxB,IAAA,KAAA,IAAS,KAAA,GAAQ,CAAA,EAAG,KAAA,GAAQ,KAAA,CAAM,QAAQ,KAAA,EAAA,EAAS;AACjD,MAAA,MAAM,MAAA,GAAS,mBAAmB,KAAA,CAAM,KAAK,GAAG,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA,CAAG,CAAA;AACnE,MAAA,IAAI,QAAQ,OAAO,MAAA;AAAA,IACrB;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,IAAI,CAAC,QAAA,CAAS,KAAK,CAAA,EAAG,OAAO,IAAA;AAC7B,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,WAAW,KAAK,MAAA,CAAO,OAAA,CAAQ,KAAK,CAAA,EAAG;AACtD,IAAA,IAAI,gBAAA,CAAiB,GAAA,CAAI,GAAG,CAAA,EAAG;AAC7B,MAAA,OAAO,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA;AAAA,IACvB;AACA,IAAA,MAAM,SAAS,kBAAA,CAAmB,WAAA,EAAa,GAAG,IAAI,CAAA,CAAA,EAAI,GAAG,CAAA,CAAE,CAAA;AAC/D,IAAA,IAAI,QAAQ,OAAO,MAAA;AAAA,EACrB;AACA,EAAA,OAAO,IAAA;AACT;AAEA,SAAS,oBAAA,CAAqB,KAAc,KAAA,EAAoE;AAC9G,EAAA,IAAI,CAAC,QAAA,CAAS,GAAG,CAAA,EAAG;AAClB,IAAA,OAAO;AAAA,MACL,MAAA,EAAQ;AAAA,QACN;AAAA,UACE,IAAA,EAAM,IAAI,KAAK,CAAA,CAAA,CAAA;AAAA,UACf,OAAA,EAAS,iCAAA;AAAA,UACT,IAAA,EAAM;AAAA;AACR;AACF,KACF;AAAA,EACF;AAEA,EAAA,MAAM,MAAA,GAAS,kBAAA,CAAmB,SAAA,CAAU,GAAG,CAAA;AAC/C,EAAA,IAAI,CAAC,OAAO,OAAA,EAAS;AACnB,IAAA,OAAO;AAAA,MACL,QAAQ,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,GAAA,CAAI,CAAC,KAAA,MAAW;AAAA,QAC1C,GAAG,YAAY,KAAK,CAAA;AAAA,QACpB,IAAA,EAAM,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA,EAAI,MAAM,IAAA,CAAK,MAAA,GAAS,CAAA,GAAI,CAAA,CAAA,EAAI,WAAA,CAAY,KAAA,CAAM,IAAI,CAAC,KAAK,EAAE,CAAA;AAAA,OAC/E,CAAE;AAAA,KACJ;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,QAAQ,EAAC;AAAA,IACT,OAAO,MAAA,CAAO;AAAA,GAChB;AACF;AAEO,SAAS,iBAAiB,IAAA,EAAiC;AAChE,EAAA,MAAM,SAA4B,EAAC;AACnC,EAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA,EAAG;AACxB,IAAA,OAAO;AAAA,MACL,KAAA,EAAO,KAAA;AAAA,MACP,MAAA,EAAQ;AAAA,QACN;AAAA,UACE,IAAA,EAAM,GAAA;AAAA,UACN,OAAA,EAAS,2BAAA;AAAA,UACT,IAAA,EAAM;AAAA;AACR;AACF,KACF;AAAA,EACF;AAEA,EAAA,MAAM,UAA0B,EAAC;AACjC,EAAA,MAAM,OAAA,uBAAc,GAAA,EAAY;AAChC,EAAA,IAAA,CAAK,OAAA,CAAQ,CAAC,IAAA,EAAM,KAAA,KAAU;AAC5B,IAAA,MAAM,MAAA,GAAS,oBAAA,CAAqB,IAAA,EAAM,KAAK,CAAA;AAC/C,IAAA,MAAA,CAAO,IAAA,CAAK,GAAG,MAAA,CAAO,MAAM,CAAA;AAC5B,IAAA,IAAI,CAAC,OAAO,KAAA,EAAO;AACnB,IAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,MAAA,CAAO,KAAA,CAAM,EAAE,CAAA,EAAG;AAChC,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,IAAA,EAAM,IAAI,KAAK,CAAA,IAAA,CAAA;AAAA,QACf,OAAA,EAAS,CAAA,sBAAA,EAAyB,MAAA,CAAO,KAAA,CAAM,EAAE,CAAA,CAAA,CAAA;AAAA,QACjD,IAAA,EAAM;AAAA,OACP,CAAA;AACD,MAAA;AAAA,IACF;AACA,IAAA,OAAA,CAAQ,GAAA,CAAI,MAAA,CAAO,KAAA,CAAM,EAAE,CAAA;AAC3B,IAAA,OAAA,CAAQ,IAAA,CAAK,OAAO,KAAK,CAAA;AAAA,EAC3B,CAAC,CAAA;AAED,EAAA,IAAI,OAAA,CAAQ,MAAA,GAAS,CAAA,IAAK,kBAAA,CAAmB,OAA0B,CAAA,EAAG;AACxE,IAAA,MAAA,CAAO,IAAA,CAAK;AAAA,MACV,IAAA,EAAM,GAAA;AAAA,MACN,OAAA,EAAS,0CAAA;AAAA,MACT,IAAA,EAAM;AAAA,KACP,CAAA;AAAA,EACH;AAEA,EAAA,KAAA,IAAS,KAAA,GAAQ,CAAA,EAAG,KAAA,GAAQ,OAAA,CAAQ,QAAQ,KAAA,EAAA,EAAS;AACnD,IAAA,MAAM,KAAA,GAAQ,QAAQ,KAAK,CAAA;AAC3B,IAAA,IAAI,IAAI,IAAA,CAAK,KAAA,CAAM,YAAY,CAAA,CAAE,OAAA,EAAQ,IAAK,IAAI,IAAA,CAAK,KAAA,CAAM,UAAU,CAAA,CAAE,SAAQ,EAAG;AAClF,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,IAAA,EAAM,IAAI,KAAK,CAAA,cAAA,CAAA;AAAA,QACf,OAAA,EAAS,uCAAA;AAAA,QACT,IAAA,EAAM;AAAA,OACP,CAAA;AAAA,IACH;AAEA,IAAA,IAAI,MAAM,GAAA,IAAO,CAAC,SAAA,CAAU,KAAA,CAAM,GAAG,CAAA,EAAG;AACtC,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,IAAA,EAAM,IAAI,KAAK,CAAA,KAAA,CAAA;AAAA,QACf,OAAA,EAAS,sCAAA;AAAA,QACT,IAAA,EAAM;AAAA,OACP,CAAA;AAAA,IACH;AAEA,IAAA,IAAI,MAAM,KAAA,IAAS,CAAC,SAAA,CAAU,KAAA,CAAM,KAAK,CAAA,EAAG;AAC1C,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,IAAA,EAAM,IAAI,KAAK,CAAA,OAAA,CAAA;AAAA,QACf,OAAA,EAAS,wCAAA;AAAA,QACT,IAAA,EAAM;AAAA,OACP,CAAA;AAAA,IACH;AAEA,IAAA,IAAI,KAAA,CAAM,KAAK,GAAA,IAAO,CAAC,UAAU,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA,EAAG;AAC/C,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,IAAA,EAAM,IAAI,KAAK,CAAA,SAAA,CAAA;AAAA,QACf,OAAA,EAAS,0CAAA;AAAA,QACT,IAAA,EAAM;AAAA,OACP,CAAA;AAAA,IACH;AAEA,IAAA,MAAM,cAAA,GAAiB,kBAAA,CAAmB,KAAA,CAAM,IAAI,CAAA;AACpD,IAAA,IAAI,cAAA,EAAgB;AAClB,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,IAAA,EAAM,CAAA,CAAA,EAAI,KAAK,CAAA,EAAA,EAAK,cAAc,CAAA,CAAA;AAAA,QAClC,SAAS,CAAA,0BAAA,EAA6B,cAAA,CAAe,MAAM,GAAG,CAAA,CAAE,KAAK,CAAA,CAAA,CAAA;AAAA,QACrE,IAAA,EAAM;AAAA,OACP,CAAA;AAAA,IACH;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,KAAA,EAAO,OAAO,MAAA,KAAW,CAAA;AAAA,IACzB;AAAA,GACF;AACF;;;AC5QA,SAASC,UAAS,KAAA,EAAkD;AAClE,EAAA,OAAO,OAAA,CAAQ,KAAK,CAAA,IAAK,OAAO,UAAU,QAAA,IAAY,CAAC,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA;AAC5E;AAEA,SAAS,oBAAA,CACP,WAAA,EACA,UAAA,EACA,IAAA,EACA,MAAA,EACM;AACN,EAAA,IAAI,gBAAgB,UAAA,EAAY;AAEhC,EAAA,IAAI,MAAM,OAAA,CAAQ,WAAW,KAAK,KAAA,CAAM,OAAA,CAAQ,UAAU,CAAA,EAAG;AAC3D,IAAA,IAAI,WAAA,CAAY,MAAA,KAAW,UAAA,CAAW,MAAA,EAAQ;AAC5C,MAAA,MAAA,CAAO,KAAK,IAAI,CAAA;AAChB,MAAA;AAAA,IACF;AACA,IAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,WAAA,CAAY,MAAA,EAAQ,KAAK,CAAA,EAAG;AAC9C,MAAA,oBAAA,CAAqB,WAAA,CAAY,CAAC,CAAA,EAAG,UAAA,CAAW,CAAC,CAAA,EAAG,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,CAAC,CAAA,CAAA,CAAA,EAAK,MAAM,CAAA;AAAA,IAC7E;AACA,IAAA;AAAA,EACF;AAEA,EAAA,IAAIA,SAAAA,CAAS,WAAW,CAAA,IAAKA,SAAAA,CAAS,UAAU,CAAA,EAAG;AACjD,IAAA,MAAM,IAAA,mBAAO,IAAI,GAAA,CAAI,CAAC,GAAG,MAAA,CAAO,IAAA,CAAK,WAAW,CAAA,EAAG,GAAG,MAAA,CAAO,IAAA,CAAK,UAAU,CAAC,CAAC,CAAA;AAC9E,IAAA,IAAA,CAAK,OAAA,CAAQ,CAAC,GAAA,KAAQ;AACpB,MAAA,MAAM,WAAW,IAAA,GAAO,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA,GAAK,GAAA;AAC3C,MAAA,oBAAA,CAAqB,YAAY,GAAG,CAAA,EAAG,WAAW,GAAG,CAAA,EAAG,UAAU,MAAM,CAAA;AAAA,IAC1E,CAAC,CAAA;AACD,IAAA;AAAA,EACF;AAEA,EAAA,MAAA,CAAO,KAAK,IAAI,CAAA;AAClB;AAEO,SAAS,YAAA,CAAa,QAAyB,KAAA,EAAsC;AAC1F,EAAA,MAAM,UAAA,GAAa,IAAI,GAAA,CAAI,MAAA,CAAO,GAAA,CAAI,CAAC,OAAA,KAAY,CAAC,OAAA,CAAQ,EAAA,EAAI,OAAO,CAAC,CAAC,CAAA;AACzE,EAAA,MAAM,SAAA,GAAY,IAAI,GAAA,CAAI,KAAA,CAAM,GAAA,CAAI,CAAC,OAAA,KAAY,CAAC,OAAA,CAAQ,EAAA,EAAI,OAAO,CAAC,CAAC,CAAA;AAEvE,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,MAAA,CAAO,CAAC,OAAA,KAAY,CAAC,UAAA,CAAW,GAAA,CAAI,OAAA,CAAQ,EAAE,CAAC,CAAA;AACnE,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,MAAA,CAAO,CAAC,OAAA,KAAY,CAAC,SAAA,CAAU,GAAA,CAAI,OAAA,CAAQ,EAAE,CAAC,CAAA;AAErE,EAAA,MAAM,OAAA,GAAU,KAAA,CACb,MAAA,CAAO,CAAC,OAAA,KAAY,UAAA,CAAW,GAAA,CAAI,OAAA,CAAQ,EAAE,CAAC,CAAA,CAC9C,GAAA,CAAI,CAAC,OAAA,KAAY;AAChB,IAAA,MAAM,QAAA,GAAW,UAAA,CAAW,GAAA,CAAI,OAAA,CAAQ,EAAE,CAAA;AAC1C,IAAA,IAAI,CAAC,UAAU,OAAO,IAAA;AACtB,IAAA,MAAM,gBAA0B,EAAC;AACjC,IAAA,oBAAA,CAAqB,QAAA,EAAU,OAAA,EAAS,EAAA,EAAI,aAAa,CAAA;AACzD,IAAA,IAAI,aAAA,CAAc,MAAA,KAAW,CAAA,EAAG,OAAO,IAAA;AACvC,IAAA,OAAO;AAAA,MACL,IAAI,OAAA,CAAQ,EAAA;AAAA,MACZ,MAAA,EAAQ,QAAA;AAAA,MACR,KAAA,EAAO,OAAA;AAAA,MACP;AAAA,KACF;AAAA,EACF,CAAC,CAAA,CACA,MAAA,CAAO,CAAC,IAAA,KAAiC,SAAS,IAAI,CAAA;AAEzD,EAAA,OAAO,EAAE,KAAA,EAAO,OAAA,EAAS,OAAA,EAAQ;AACnC;AAMO,SAAS,qBAAA,CACd,IAAA,EACA,OAAA,GAAgC,EAAC,EACzB;AACR,EAAA,MAAM,QAAkB,EAAC;AAEzB,EAAA,IAAI,IAAA,CAAK,KAAA,CAAM,MAAA,GAAS,CAAA,EAAG;AACzB,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,OAAA,EAAU,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,CAAC,OAAA,KAAY,OAAA,CAAQ,KAAK,CAAA,CAAE,IAAA,CAAK,IAAI,CAAC,CAAA,CAAE,CAAA;AAAA,EAC9E;AAEA,EAAA,IAAI,IAAA,CAAK,OAAA,CAAQ,MAAA,GAAS,CAAA,EAAG;AAC3B,IAAA,MAAM,WAAA,GAAc,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,CAAC,IAAA,KAAS;AAC7C,MAAA,IAAI,CAAC,OAAA,CAAQ,mBAAA,EAAqB,OAAO,KAAK,KAAA,CAAM,KAAA;AACpD,MAAA,OAAO,CAAA,EAAG,KAAK,KAAA,CAAM,KAAK,KAAK,IAAA,CAAK,aAAA,CAAc,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA,CAAA;AAAA,IAC9D,CAAC,CAAA;AACD,IAAA,KAAA,CAAM,KAAK,CAAA,SAAA,EAAY,WAAA,CAAY,IAAA,CAAK,IAAI,CAAC,CAAA,CAAE,CAAA;AAAA,EACjD;AAEA,EAAA,IAAI,IAAA,CAAK,OAAA,CAAQ,MAAA,GAAS,CAAA,EAAG;AAC3B,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,SAAA,EAAY,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,CAAC,OAAA,KAAY,OAAA,CAAQ,KAAK,CAAA,CAAE,IAAA,CAAK,IAAI,CAAC,CAAA,CAAE,CAAA;AAAA,EAClF;AAEA,EAAA,OAAO,MAAM,MAAA,GAAS,CAAA,GAAI,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA,GAAI,sBAAA;AAC/C;AAEO,SAAS,sBAAsB,QAAA,EAA2B;AAC/D,EAAA,OAAO,iBAAiB,QAAQ,CAAA;AAClC","file":"ci.cjs","sourcesContent":["import type { FeatureEntry, FeatureManifest } from \"./types\";\n\nfunction getDirectDependencies(feature: FeatureEntry): string[] {\n const dependsOn = feature.dependsOn;\n if (!dependsOn) return [];\n const seen = dependsOn.seen ?? [];\n const clicked = dependsOn.clicked ?? [];\n const dismissed = dependsOn.dismissed ?? [];\n const unique = new Set<string>();\n for (const id of [...seen, ...clicked, ...dismissed]) {\n if (id) unique.add(id);\n }\n return Array.from(unique);\n}\n\nexport function resolveDependencyOrder(manifest: FeatureManifest): string[] {\n const ids = new Set(manifest.map((feature) => feature.id));\n const outgoing = new Map<string, Set<string>>();\n const indegree = new Map<string, number>();\n\n for (const feature of manifest) {\n outgoing.set(feature.id, new Set());\n indegree.set(feature.id, 0);\n }\n\n for (const feature of manifest) {\n for (const dependencyId of getDirectDependencies(feature)) {\n if (!ids.has(dependencyId)) continue;\n const edges = outgoing.get(dependencyId);\n if (!edges || edges.has(feature.id)) continue;\n edges.add(feature.id);\n indegree.set(feature.id, (indegree.get(feature.id) ?? 0) + 1);\n }\n }\n\n const queue: string[] = [];\n for (const feature of manifest) {\n if ((indegree.get(feature.id) ?? 0) === 0) queue.push(feature.id);\n }\n\n const ordered: string[] = [];\n while (queue.length > 0) {\n const id = queue.shift();\n if (!id) continue;\n ordered.push(id);\n const edges = outgoing.get(id);\n if (!edges) continue;\n for (const nextId of edges) {\n const nextDegree = (indegree.get(nextId) ?? 0) - 1;\n indegree.set(nextId, nextDegree);\n if (nextDegree === 0) queue.push(nextId);\n }\n }\n\n // On cycles, append remaining IDs in original order to keep behavior stable.\n if (ordered.length < manifest.length) {\n const included = new Set(ordered);\n for (const feature of manifest) {\n if (included.has(feature.id)) continue;\n ordered.push(feature.id);\n }\n }\n\n return ordered;\n}\n\nexport function hasDependencyCycle(manifest: FeatureManifest): boolean {\n const ids = new Set(manifest.map((feature) => feature.id));\n const outgoing = new Map<string, Set<string>>();\n const indegree = new Map<string, number>();\n\n for (const feature of manifest) {\n outgoing.set(feature.id, new Set());\n indegree.set(feature.id, 0);\n }\n\n for (const feature of manifest) {\n for (const dependencyId of getDirectDependencies(feature)) {\n if (!ids.has(dependencyId)) continue;\n const edges = outgoing.get(dependencyId);\n if (!edges || edges.has(feature.id)) continue;\n edges.add(feature.id);\n indegree.set(feature.id, (indegree.get(feature.id) ?? 0) + 1);\n }\n }\n\n const queue: string[] = [];\n for (const feature of manifest) {\n if ((indegree.get(feature.id) ?? 0) === 0) queue.push(feature.id);\n }\n\n let visited = 0;\n while (queue.length > 0) {\n const id = queue.shift();\n if (!id) continue;\n visited += 1;\n const edges = outgoing.get(id);\n if (!edges) continue;\n for (const nextId of edges) {\n const nextDegree = (indegree.get(nextId) ?? 0) - 1;\n indegree.set(nextId, nextDegree);\n if (nextDegree === 0) queue.push(nextId);\n }\n }\n\n return visited !== manifest.length;\n}\n\nexport function sortFeaturesByDependencies(features: FeatureEntry[]): FeatureEntry[] {\n if (features.length <= 1) return [...features];\n const order = resolveDependencyOrder(features);\n const rank = new Map(order.map((id, index) => [id, index]));\n return [...features].sort((a, b) => {\n const ra = rank.get(a.id);\n const rb = rank.get(b.id);\n if (ra === undefined || rb === undefined) return 0;\n return ra - rb;\n });\n}\n","import { hasDependencyCycle } from \"./dependencies\";\nimport type { FeatureEntry, FeatureManifest } from \"./types\";\nimport { z } from \"zod\";\n\nexport interface ValidationIssue {\n path: string;\n message: string;\n code:\n | \"invalid_type\"\n | \"missing_required\"\n | \"invalid_value\"\n | \"invalid_date\"\n | \"duplicate_id\"\n | \"circular_dependency\";\n}\n\nexport interface ValidationResult {\n valid: boolean;\n errors: ValidationIssue[];\n}\n\nexport const featureEntryJsonSchema = {\n type: \"object\",\n required: [\"id\", \"label\", \"releasedAt\", \"showNewUntil\"],\n properties: {\n id: { type: \"string\" },\n label: { type: \"string\" },\n description: { type: \"string\" },\n releasedAt: { type: \"string\", format: \"date-time\" },\n showNewUntil: { type: \"string\", format: \"date-time\" },\n flagKey: { type: \"string\" },\n product: { type: \"string\" },\n url: { type: \"string\" },\n image: { type: \"string\" },\n type: { enum: [\"feature\", \"improvement\", \"fix\", \"breaking\"] },\n priority: { enum: [\"critical\", \"normal\", \"low\"] },\n cta: {\n type: \"object\",\n properties: {\n label: { type: \"string\" },\n url: { type: \"string\" },\n },\n },\n meta: { type: \"object\" },\n },\n} as const;\n\nexport const featureManifestJsonSchema = {\n type: \"array\",\n items: featureEntryJsonSchema,\n} as const;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction isValidDate(value: string): boolean {\n return Number.isFinite(new Date(value).getTime());\n}\n\nconst nonEmptyString = z.string().trim().min(1, \"must be a non-empty string\");\n\nconst isoDateString = nonEmptyString.refine(isValidDate, {\n message: \"must be a valid date\",\n params: { featuredropCode: \"invalid_date\" },\n});\n\nconst dependsOnSchema = z\n .object({\n seen: z.array(z.string()).optional(),\n clicked: z.array(z.string()).optional(),\n dismissed: z.array(z.string()).optional(),\n })\n .optional();\n\nconst ctaSchema = z\n .object({\n label: nonEmptyString,\n url: nonEmptyString,\n })\n .optional();\n\nexport const featureEntrySchema = z\n .object({\n id: nonEmptyString,\n label: nonEmptyString,\n releasedAt: isoDateString,\n showNewUntil: isoDateString,\n description: z.string().optional(),\n flagKey: z.string().optional(),\n product: z.string().optional(),\n url: z.string().optional(),\n image: z.string().optional(),\n type: z.enum([\"feature\", \"improvement\", \"fix\", \"breaking\"]).optional(),\n priority: z.enum([\"critical\", \"normal\", \"low\"]).optional(),\n cta: ctaSchema,\n meta: z.record(z.unknown()).optional(),\n dependsOn: dependsOnSchema,\n })\n .passthrough();\n\nexport const featureManifestSchema = z.array(featureEntrySchema);\n\nfunction toIssuePath(path: Array<string | number>): string {\n if (path.length === 0) return \"$\";\n let output = \"\";\n for (const part of path) {\n if (typeof part === \"number\") output += `[${part}]`;\n else output += output ? `.${part}` : part;\n }\n return output;\n}\n\nfunction mapZodIssue(issue: z.ZodIssue): ValidationIssue {\n const codeParam = (issue as { params?: Record<string, unknown> }).params?.featuredropCode;\n if (codeParam === \"invalid_date\") {\n return {\n path: toIssuePath(issue.path),\n message: issue.message,\n code: \"invalid_date\",\n };\n }\n if (issue.code === \"invalid_type\") {\n return {\n path: toIssuePath(issue.path),\n message: issue.message,\n code: issue.received === \"undefined\" ? \"missing_required\" : \"invalid_type\",\n };\n }\n return {\n path: toIssuePath(issue.path),\n message: issue.message,\n code: \"invalid_value\",\n };\n}\n\nconst UNSAFE_META_KEYS = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\n\nfunction isSafeUrl(value: string): boolean {\n const normalized = value.trim();\n if (!normalized) return false;\n if (/^(\\/|\\.\\/|\\.\\.\\/|\\?|#)/.test(normalized)) return true;\n if (/^https?:\\/\\//i.test(normalized)) return true;\n return false;\n}\n\nfunction findUnsafeMetaPath(value: unknown, path = \"meta\"): string | null {\n if (Array.isArray(value)) {\n for (let index = 0; index < value.length; index++) {\n const nested = findUnsafeMetaPath(value[index], `${path}[${index}]`);\n if (nested) return nested;\n }\n return null;\n }\n\n if (!isRecord(value)) return null;\n for (const [key, nestedValue] of Object.entries(value)) {\n if (UNSAFE_META_KEYS.has(key)) {\n return `${path}.${key}`;\n }\n const nested = findUnsafeMetaPath(nestedValue, `${path}.${key}`);\n if (nested) return nested;\n }\n return null;\n}\n\nfunction validateFeatureEntry(raw: unknown, index: number): { entry?: FeatureEntry; issues: ValidationIssue[] } {\n if (!isRecord(raw)) {\n return {\n issues: [\n {\n path: `[${index}]`,\n message: \"Feature entry must be an object\",\n code: \"invalid_type\",\n },\n ],\n };\n }\n\n const parsed = featureEntrySchema.safeParse(raw);\n if (!parsed.success) {\n return {\n issues: parsed.error.issues.map((issue) => ({\n ...mapZodIssue(issue),\n path: `[${index}]${issue.path.length > 0 ? `.${toIssuePath(issue.path)}` : \"\"}`,\n })),\n };\n }\n\n return {\n issues: [],\n entry: parsed.data as FeatureEntry,\n };\n}\n\nexport function validateManifest(data: unknown): ValidationResult {\n const errors: ValidationIssue[] = [];\n if (!Array.isArray(data)) {\n return {\n valid: false,\n errors: [\n {\n path: \"$\",\n message: \"Manifest must be an array\",\n code: \"invalid_type\",\n },\n ],\n };\n }\n\n const entries: FeatureEntry[] = [];\n const seenIds = new Set<string>();\n data.forEach((item, index) => {\n const result = validateFeatureEntry(item, index);\n errors.push(...result.issues);\n if (!result.entry) return;\n if (seenIds.has(result.entry.id)) {\n errors.push({\n path: `[${index}].id`,\n message: `Duplicate feature id \"${result.entry.id}\"`,\n code: \"duplicate_id\",\n });\n return;\n }\n seenIds.add(result.entry.id);\n entries.push(result.entry);\n });\n\n if (entries.length > 0 && hasDependencyCycle(entries as FeatureManifest)) {\n errors.push({\n path: \"$\",\n message: \"Circular dependsOn relationship detected\",\n code: \"circular_dependency\",\n });\n }\n\n for (let index = 0; index < entries.length; index++) {\n const entry = entries[index];\n if (new Date(entry.showNewUntil).getTime() <= new Date(entry.releasedAt).getTime()) {\n errors.push({\n path: `[${index}].showNewUntil`,\n message: \"showNewUntil must be after releasedAt\",\n code: \"invalid_value\",\n });\n }\n\n if (entry.url && !isSafeUrl(entry.url)) {\n errors.push({\n path: `[${index}].url`,\n message: \"url must be http, https, or relative\",\n code: \"invalid_value\",\n });\n }\n\n if (entry.image && !isSafeUrl(entry.image)) {\n errors.push({\n path: `[${index}].image`,\n message: \"image must be http, https, or relative\",\n code: \"invalid_value\",\n });\n }\n\n if (entry.cta?.url && !isSafeUrl(entry.cta.url)) {\n errors.push({\n path: `[${index}].cta.url`,\n message: \"cta.url must be http, https, or relative\",\n code: \"invalid_value\",\n });\n }\n\n const unsafeMetaPath = findUnsafeMetaPath(entry.meta);\n if (unsafeMetaPath) {\n errors.push({\n path: `[${index}].${unsafeMetaPath}`,\n message: `meta contains unsafe key \"${unsafeMetaPath.split(\".\").pop()}\"`,\n code: \"invalid_value\",\n });\n }\n }\n\n return {\n valid: errors.length === 0,\n errors,\n };\n}\n","import { validateManifest } from \"./schema\";\nimport type { FeatureEntry, FeatureManifest } from \"./types\";\n\nexport interface ChangedFeature {\n id: string;\n before: FeatureEntry;\n after: FeatureEntry;\n changedFields: string[];\n}\n\nexport interface ManifestDiff {\n added: FeatureEntry[];\n removed: FeatureEntry[];\n changed: ChangedFeature[];\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction collectChangedFields(\n beforeValue: unknown,\n afterValue: unknown,\n path: string,\n output: string[],\n): void {\n if (beforeValue === afterValue) return;\n\n if (Array.isArray(beforeValue) && Array.isArray(afterValue)) {\n if (beforeValue.length !== afterValue.length) {\n output.push(path);\n return;\n }\n for (let i = 0; i < beforeValue.length; i += 1) {\n collectChangedFields(beforeValue[i], afterValue[i], `${path}[${i}]`, output);\n }\n return;\n }\n\n if (isRecord(beforeValue) && isRecord(afterValue)) {\n const keys = new Set([...Object.keys(beforeValue), ...Object.keys(afterValue)]);\n keys.forEach((key) => {\n const nextPath = path ? `${path}.${key}` : key;\n collectChangedFields(beforeValue[key], afterValue[key], nextPath, output);\n });\n return;\n }\n\n output.push(path);\n}\n\nexport function diffManifest(before: FeatureManifest, after: FeatureManifest): ManifestDiff {\n const beforeById = new Map(before.map((feature) => [feature.id, feature]));\n const afterById = new Map(after.map((feature) => [feature.id, feature]));\n\n const added = after.filter((feature) => !beforeById.has(feature.id));\n const removed = before.filter((feature) => !afterById.has(feature.id));\n\n const changed = after\n .filter((feature) => beforeById.has(feature.id))\n .map((feature) => {\n const previous = beforeById.get(feature.id);\n if (!previous) return null;\n const changedFields: string[] = [];\n collectChangedFields(previous, feature, \"\", changedFields);\n if (changedFields.length === 0) return null;\n return {\n id: feature.id,\n before: previous,\n after: feature,\n changedFields,\n } satisfies ChangedFeature;\n })\n .filter((item): item is ChangedFeature => item !== null);\n\n return { added, removed, changed };\n}\n\nexport interface ChangelogDiffOptions {\n includeFieldChanges?: boolean;\n}\n\nexport function generateChangelogDiff(\n diff: ManifestDiff,\n options: ChangelogDiffOptions = {},\n): string {\n const parts: string[] = [];\n\n if (diff.added.length > 0) {\n parts.push(`Added: ${diff.added.map((feature) => feature.label).join(\", \")}`);\n }\n\n if (diff.changed.length > 0) {\n const changedText = diff.changed.map((item) => {\n if (!options.includeFieldChanges) return item.after.label;\n return `${item.after.label} [${item.changedFields.join(\", \")}]`;\n });\n parts.push(`Changed: ${changedText.join(\", \")}`);\n }\n\n if (diff.removed.length > 0) {\n parts.push(`Removed: ${diff.removed.map((feature) => feature.label).join(\", \")}`);\n }\n\n return parts.length > 0 ? parts.join(\". \") : \"No manifest changes.\";\n}\n\nexport function validateManifestForCI(manifest: FeatureManifest) {\n return validateManifest(manifest);\n}\n"]}
package/dist/ci.d.cts ADDED
@@ -0,0 +1,176 @@
1
+ interface ValidationIssue {
2
+ path: string;
3
+ message: string;
4
+ code: "invalid_type" | "missing_required" | "invalid_value" | "invalid_date" | "duplicate_id" | "circular_dependency";
5
+ }
6
+ interface ValidationResult {
7
+ valid: boolean;
8
+ errors: ValidationIssue[];
9
+ }
10
+
11
+ /** Entry type label — determines default icon/color in UI */
12
+ type FeatureType = "feature" | "improvement" | "fix" | "breaking";
13
+ /** Priority level for announcements */
14
+ type FeaturePriority = "critical" | "normal" | "low";
15
+ /** Call-to-action for a feature entry */
16
+ interface FeatureCTA {
17
+ /** Button/link label */
18
+ label: string;
19
+ /** URL to navigate to */
20
+ url: string;
21
+ }
22
+ /** Variant-level overrides for A/B announcement testing */
23
+ interface FeatureVariant {
24
+ /** Optional variant-specific label override */
25
+ label?: string;
26
+ /** Optional variant-specific description override */
27
+ description?: string;
28
+ /** Optional variant-specific image override */
29
+ image?: string;
30
+ /** Optional variant-specific CTA override */
31
+ cta?: FeatureCTA;
32
+ /** Optional variant-specific metadata overrides */
33
+ meta?: Record<string, unknown>;
34
+ }
35
+ /** Audience targeting rule — determines which user segments see a feature */
36
+ interface AudienceRule {
37
+ /** Plans that should see this feature (e.g. ["pro", "enterprise"]) */
38
+ plan?: string[];
39
+ /** Roles that should see this feature (e.g. ["admin", "editor"]) */
40
+ role?: string[];
41
+ /** Regions that should see this feature (e.g. ["us", "eu"]) */
42
+ region?: string[];
43
+ /** Arbitrary key-value pairs for custom matching logic */
44
+ custom?: Record<string, unknown>;
45
+ }
46
+ /** Dependency gates for progressive feature discovery */
47
+ interface FeatureDependencies {
48
+ /** Features the user must have seen before this one can surface */
49
+ seen?: string[];
50
+ /** Features the user must have clicked before this one can surface */
51
+ clicked?: string[];
52
+ /** Features the user must have dismissed before this one can surface */
53
+ dismissed?: string[];
54
+ }
55
+ /** Runtime context used by trigger evaluation */
56
+ interface TriggerContext {
57
+ /** Current app route/path */
58
+ path?: string;
59
+ /** Named events observed in this session */
60
+ events?: ReadonlySet<string>;
61
+ /** Named milestone flags reached in this session */
62
+ milestones?: ReadonlySet<string>;
63
+ /** Usage counters keyed by event/pattern name */
64
+ usage?: Record<string, number>;
65
+ /** Session elapsed time in milliseconds */
66
+ elapsedMs?: number;
67
+ /** Scroll completion percentage (0-100) */
68
+ scrollPercent?: number;
69
+ /** Optional additional trigger context */
70
+ metadata?: Record<string, unknown>;
71
+ }
72
+ type FeatureTrigger = {
73
+ type: "page";
74
+ match: string | RegExp;
75
+ } | {
76
+ type: "usage";
77
+ event: string;
78
+ minActions?: number;
79
+ } | {
80
+ type: "time";
81
+ minSeconds: number;
82
+ } | {
83
+ type: "milestone";
84
+ event: string;
85
+ } | {
86
+ type: "frustration";
87
+ pattern: string;
88
+ threshold?: number;
89
+ } | {
90
+ type: "scroll";
91
+ minPercent?: number;
92
+ } | {
93
+ type: "custom";
94
+ evaluate: (context: TriggerContext) => boolean;
95
+ };
96
+ /** A single feature entry in the manifest */
97
+ interface FeatureEntry {
98
+ /** Unique identifier for the feature */
99
+ id: string;
100
+ /** Human-readable label (e.g. "Decision Journal") */
101
+ label: string;
102
+ /** Optional longer description (supports markdown in UI components) */
103
+ description?: string;
104
+ /**
105
+ * Semantic version targeting.
106
+ * If provided as an object, requires `appVersion` to be supplied to the provider/helpers.
107
+ * - introduced: earliest app version that includes this feature
108
+ * - showNewUntil: stop showing "new" once appVersion reaches this
109
+ * - deprecatedAt: hide feature for app versions at or above this (optional safety)
110
+ * - showIn: range string, e.g. ">=2.5.0 <3.0.0"
111
+ */
112
+ version?: string | {
113
+ introduced?: string;
114
+ showNewUntil?: string;
115
+ deprecatedAt?: string;
116
+ showIn?: string;
117
+ };
118
+ /** ISO date when this feature was released */
119
+ releasedAt: string;
120
+ /** ISO date after which the "new" badge should stop showing */
121
+ showNewUntil: string;
122
+ /** Optional key to match navigation items (e.g. "/journal", "settings") */
123
+ sidebarKey?: string;
124
+ /** Optional grouping category (e.g. "ai", "billing", "core") */
125
+ category?: string;
126
+ /** Optional product scope (`"*"`, `"askverdict"`, etc.) for multi-product manifests */
127
+ product?: string;
128
+ /** Optional URL to link to (e.g. docs page, changelog entry) */
129
+ url?: string;
130
+ /** Optional feature flag key; requires a flag bridge to evaluate */
131
+ flagKey?: string;
132
+ /** Entry type — determines default icon/color in UI components */
133
+ type?: FeatureType;
134
+ /** Priority level — critical entries get special treatment in UI */
135
+ priority?: FeaturePriority;
136
+ /** Optional image/screenshot URL */
137
+ image?: string;
138
+ /** Optional call-to-action button */
139
+ cta?: FeatureCTA;
140
+ /** ISO date — entry is hidden until this date (scheduled publishing) */
141
+ publishAt?: string;
142
+ /** Optional arbitrary metadata */
143
+ meta?: Record<string, unknown>;
144
+ /** A/B variants keyed by variant name (e.g. control, treatment_a) */
145
+ variants?: Record<string, FeatureVariant>;
146
+ /** Percentage split per variant (same order as variants object keys) */
147
+ variantSplit?: number[];
148
+ /** Audience targeting — if set, only matching users see this feature */
149
+ audience?: AudienceRule;
150
+ /** Dependency requirements (progressive disclosure sequencing) */
151
+ dependsOn?: FeatureDependencies;
152
+ /** Contextual trigger rule */
153
+ trigger?: FeatureTrigger;
154
+ }
155
+ /** The full feature manifest — an array of feature entries */
156
+ type FeatureManifest = readonly FeatureEntry[];
157
+
158
+ interface ChangedFeature {
159
+ id: string;
160
+ before: FeatureEntry;
161
+ after: FeatureEntry;
162
+ changedFields: string[];
163
+ }
164
+ interface ManifestDiff {
165
+ added: FeatureEntry[];
166
+ removed: FeatureEntry[];
167
+ changed: ChangedFeature[];
168
+ }
169
+ declare function diffManifest(before: FeatureManifest, after: FeatureManifest): ManifestDiff;
170
+ interface ChangelogDiffOptions {
171
+ includeFieldChanges?: boolean;
172
+ }
173
+ declare function generateChangelogDiff(diff: ManifestDiff, options?: ChangelogDiffOptions): string;
174
+ declare function validateManifestForCI(manifest: FeatureManifest): ValidationResult;
175
+
176
+ export { type ChangedFeature, type ChangelogDiffOptions, type ManifestDiff, diffManifest, generateChangelogDiff, validateManifestForCI };