@girardmedia/bootspring 2.0.22 → 2.0.23

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.
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Policy Matrix
3
+ * Comprehensive policy definitions for org-level gates
4
+ * @package bootspring
5
+ */
6
+
7
+ /**
8
+ * Policy scopes define what can be controlled
9
+ */
10
+ const POLICY_SCOPES = {
11
+ skills: {
12
+ external: 'skills.external', // Third-party/external skills
13
+ premium: 'skills.premium', // Premium skill categories
14
+ ai: 'skills.ai', // AI-powered skills
15
+ all: 'skills.*'
16
+ },
17
+ workflows: {
18
+ parallel: 'workflows.parallel', // Parallel execution
19
+ premium: 'workflows.premium', // Premium workflow packs
20
+ custom: 'workflows.custom', // Custom workflow definitions
21
+ all: 'workflows.*'
22
+ },
23
+ agents: {
24
+ technical: 'agents.technical', // Technical experts
25
+ business: 'agents.business', // Business/legal experts
26
+ enterprise: 'agents.enterprise', // Enterprise-only agents
27
+ all: 'agents.*'
28
+ },
29
+ features: {
30
+ telemetry: 'features.telemetry', // Usage telemetry
31
+ cloudSync: 'features.cloud_sync', // Cloud synchronization
32
+ teamSharing: 'features.team_sharing', // Team context sharing
33
+ auditLogs: 'features.audit_logs', // Audit logging
34
+ apiAccess: 'features.api_access', // Direct API access
35
+ all: 'features.*'
36
+ }
37
+ };
38
+
39
+ /**
40
+ * Default policy matrix by tier
41
+ * Defines what's allowed/blocked per tier
42
+ */
43
+ const TIER_POLICY_DEFAULTS = {
44
+ free: {
45
+ allowed: [
46
+ 'skills.external',
47
+ 'agents.technical',
48
+ 'features.telemetry'
49
+ ],
50
+ blocked: [
51
+ 'skills.premium',
52
+ 'skills.ai',
53
+ 'workflows.premium',
54
+ 'workflows.parallel',
55
+ 'agents.business',
56
+ 'agents.enterprise',
57
+ 'features.cloud_sync',
58
+ 'features.team_sharing',
59
+ 'features.audit_logs'
60
+ ],
61
+ limits: {
62
+ skillsPerDay: 50,
63
+ workflowsPerDay: 10,
64
+ agentInvocationsPerDay: 20
65
+ }
66
+ },
67
+ pro: {
68
+ allowed: [
69
+ 'skills.*',
70
+ 'workflows.*',
71
+ 'agents.technical',
72
+ 'agents.business',
73
+ 'features.telemetry',
74
+ 'features.cloud_sync'
75
+ ],
76
+ blocked: [
77
+ 'agents.enterprise',
78
+ 'features.team_sharing',
79
+ 'features.audit_logs'
80
+ ],
81
+ limits: {
82
+ skillsPerDay: 500,
83
+ workflowsPerDay: 100,
84
+ agentInvocationsPerDay: 200
85
+ }
86
+ },
87
+ team: {
88
+ allowed: [
89
+ 'skills.*',
90
+ 'workflows.*',
91
+ 'agents.*',
92
+ 'features.telemetry',
93
+ 'features.cloud_sync',
94
+ 'features.team_sharing',
95
+ 'features.audit_logs'
96
+ ],
97
+ blocked: [],
98
+ limits: {
99
+ skillsPerDay: 2000,
100
+ workflowsPerDay: 500,
101
+ agentInvocationsPerDay: 1000,
102
+ teamMembers: 10
103
+ }
104
+ },
105
+ enterprise: {
106
+ allowed: ['*'],
107
+ blocked: [],
108
+ limits: {
109
+ skillsPerDay: -1, // Unlimited
110
+ workflowsPerDay: -1,
111
+ agentInvocationsPerDay: -1,
112
+ teamMembers: -1
113
+ }
114
+ }
115
+ };
116
+
117
+ /**
118
+ * Profile-specific policy overrides
119
+ * Applied on top of tier defaults
120
+ */
121
+ const PROFILE_OVERRIDES = {
122
+ startup: {
123
+ // Startup profile: permissive, fast iteration
124
+ overrides: {},
125
+ additionalBlocked: []
126
+ },
127
+ regulated: {
128
+ // Regulated profile: compliance-focused
129
+ overrides: {
130
+ requireApproval: ['workflows.custom', 'skills.external'],
131
+ auditAll: true,
132
+ dataResidency: true
133
+ },
134
+ additionalBlocked: [
135
+ 'skills.external',
136
+ 'workflows.growth-pack'
137
+ ]
138
+ },
139
+ enterprise: {
140
+ // Enterprise profile: full control
141
+ overrides: {
142
+ ssoRequired: true,
143
+ auditAll: true,
144
+ approvalWorkflow: true
145
+ },
146
+ additionalBlocked: []
147
+ }
148
+ };
149
+
150
+ /**
151
+ * Member role permissions
152
+ * What each role can do within an org
153
+ */
154
+ const ROLE_PERMISSIONS = {
155
+ owner: {
156
+ canManageOrg: true,
157
+ canManageMembers: true,
158
+ canManagePolicies: true,
159
+ canManageBilling: true,
160
+ canUseAllFeatures: true
161
+ },
162
+ admin: {
163
+ canManageOrg: false,
164
+ canManageMembers: true,
165
+ canManagePolicies: true,
166
+ canManageBilling: false,
167
+ canUseAllFeatures: true
168
+ },
169
+ member: {
170
+ canManageOrg: false,
171
+ canManageMembers: false,
172
+ canManagePolicies: false,
173
+ canManageBilling: false,
174
+ canUseAllFeatures: true
175
+ },
176
+ viewer: {
177
+ canManageOrg: false,
178
+ canManageMembers: false,
179
+ canManagePolicies: false,
180
+ canManageBilling: false,
181
+ canUseAllFeatures: false
182
+ }
183
+ };
184
+
185
+ /**
186
+ * Check if a scope matches a pattern
187
+ * @param {string} scope - Specific scope (e.g., 'skills.external')
188
+ * @param {string} pattern - Pattern to match (e.g., 'skills.*' or 'skills.external')
189
+ * @returns {boolean}
190
+ */
191
+ function matchesScope(scope, pattern) {
192
+ if (pattern === '*') return true;
193
+ if (pattern === scope) return true;
194
+ if (pattern.endsWith('.*')) {
195
+ const prefix = pattern.slice(0, -2);
196
+ return scope.startsWith(prefix + '.');
197
+ }
198
+ return false;
199
+ }
200
+
201
+ /**
202
+ * Check if a scope is allowed by policy
203
+ * @param {string} scope - Scope to check
204
+ * @param {string[]} allowed - Allowed patterns
205
+ * @param {string[]} blocked - Blocked patterns
206
+ * @returns {boolean}
207
+ */
208
+ function isScopeAllowed(scope, allowed, blocked) {
209
+ // Check blocked first (blocked takes precedence)
210
+ for (const pattern of blocked) {
211
+ if (matchesScope(scope, pattern)) {
212
+ return false;
213
+ }
214
+ }
215
+ // Check allowed
216
+ for (const pattern of allowed) {
217
+ if (matchesScope(scope, pattern)) {
218
+ return true;
219
+ }
220
+ }
221
+ return false;
222
+ }
223
+
224
+ /**
225
+ * Build effective policy for an org member
226
+ * @param {string} tier - Org tier (free/pro/team/enterprise)
227
+ * @param {string} profile - Policy profile (startup/regulated/enterprise)
228
+ * @param {object} memberOverrides - Per-member policy overrides
229
+ * @returns {object} Effective policy
230
+ */
231
+ function buildEffectivePolicy(tier, profile, memberOverrides = {}) {
232
+ const tierDefaults = TIER_POLICY_DEFAULTS[tier] || TIER_POLICY_DEFAULTS.free;
233
+ const profileOverrides = PROFILE_OVERRIDES[profile] || PROFILE_OVERRIDES.startup;
234
+
235
+ // Merge allowed/blocked lists
236
+ const allowed = [...tierDefaults.allowed];
237
+ const blocked = [
238
+ ...tierDefaults.blocked,
239
+ ...profileOverrides.additionalBlocked
240
+ ];
241
+
242
+ // Apply member overrides
243
+ if (memberOverrides.additionalAllowed) {
244
+ allowed.push(...memberOverrides.additionalAllowed);
245
+ }
246
+ if (memberOverrides.additionalBlocked) {
247
+ blocked.push(...memberOverrides.additionalBlocked);
248
+ }
249
+
250
+ return {
251
+ tier,
252
+ profile,
253
+ allowed: [...new Set(allowed)],
254
+ blocked: [...new Set(blocked)],
255
+ limits: { ...tierDefaults.limits, ...(memberOverrides.limits || {}) },
256
+ overrides: { ...profileOverrides.overrides, ...(memberOverrides.overrides || {}) }
257
+ };
258
+ }
259
+
260
+ /**
261
+ * Check access against effective policy
262
+ * @param {string} scope - Scope to check
263
+ * @param {object} policy - Effective policy
264
+ * @returns {object} Access result
265
+ */
266
+ function checkPolicyAccess(scope, policy) {
267
+ const allowed = isScopeAllowed(scope, policy.allowed, policy.blocked);
268
+
269
+ if (!allowed) {
270
+ return {
271
+ allowed: false,
272
+ code: 'policy_blocked',
273
+ scope,
274
+ reason: `Scope "${scope}" is blocked by ${policy.profile} policy`
275
+ };
276
+ }
277
+
278
+ // Check if approval is required
279
+ if (policy.overrides.requireApproval?.some(p => matchesScope(scope, p))) {
280
+ return {
281
+ allowed: true,
282
+ requiresApproval: true,
283
+ scope,
284
+ reason: `Scope "${scope}" requires approval under ${policy.profile} policy`
285
+ };
286
+ }
287
+
288
+ return {
289
+ allowed: true,
290
+ scope
291
+ };
292
+ }
293
+
294
+ module.exports = {
295
+ POLICY_SCOPES,
296
+ TIER_POLICY_DEFAULTS,
297
+ PROFILE_OVERRIDES,
298
+ ROLE_PERMISSIONS,
299
+ matchesScope,
300
+ isScopeAllowed,
301
+ buildEffectivePolicy,
302
+ checkPolicyAccess
303
+ };
@@ -36,6 +36,7 @@ const EXEMPT_COMMANDS = [
36
36
  'billing', // Billing status/info accessible without project
37
37
  'preseed', // Preseed works locally, auth enhances features
38
38
  'seed', // Seed works locally for scaffolding
39
+ 'org', // Org policy accessible without project
39
40
  ];
40
41
 
41
42
  // Sub-commands of auth that are exempt
@@ -8,6 +8,7 @@ const { PHASE_AGENTS, TECHNICAL_TRIGGERS } = require('./phases');
8
8
  const { WORKFLOWS } = require('./workflows');
9
9
  const { REMEDIATION_PATHS } = require('./remediation');
10
10
  const { FAILURE_SIGNATURES, SEVERITY_WEIGHTS, PARALLEL_FAILURE_STRATEGIES } = require('./failure-signatures');
11
+ const packLifecycle = require('./pack-lifecycle');
11
12
 
12
13
  module.exports = {
13
14
  PHASE_AGENTS,
@@ -16,5 +17,7 @@ module.exports = {
16
17
  REMEDIATION_PATHS,
17
18
  FAILURE_SIGNATURES,
18
19
  SEVERITY_WEIGHTS,
19
- PARALLEL_FAILURE_STRATEGIES
20
+ PARALLEL_FAILURE_STRATEGIES,
21
+ // Pack lifecycle exports
22
+ ...packLifecycle
20
23
  };
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Pack Lifecycle Configuration
3
+ * Manages premium pack rollout stages: draft -> QA -> canary -> GA
4
+ * @package bootspring
5
+ */
6
+
7
+ /**
8
+ * Lifecycle stages
9
+ */
10
+ const LIFECYCLE_STAGES = {
11
+ DRAFT: 'draft', // Internal development, not visible to users
12
+ QA: 'qa', // Internal testing, visible to team accounts
13
+ CANARY: 'canary', // Limited rollout (25% of eligible users)
14
+ GA: 'ga' // General availability (100% rollout)
15
+ };
16
+
17
+ /**
18
+ * Stage order for progression
19
+ */
20
+ const STAGE_ORDER = ['draft', 'qa', 'canary', 'ga'];
21
+
22
+ /**
23
+ * Default rollout percentages per stage
24
+ */
25
+ const DEFAULT_ROLLOUT = {
26
+ draft: 0, // Not available to users
27
+ qa: 5, // 5% of team/enterprise users for internal QA
28
+ canary: 25, // 25% of eligible users
29
+ ga: 100 // All eligible users
30
+ };
31
+
32
+ /**
33
+ * Pack lifecycle definitions
34
+ * Tracks current stage and rollout status for each premium pack
35
+ */
36
+ const PACK_LIFECYCLE = {
37
+ 'launch-pack': {
38
+ name: 'Launch Pack',
39
+ description: 'Premium guided launch workflow from freeze to production validation',
40
+ currentStage: 'ga',
41
+ stageHistory: [
42
+ { stage: 'draft', date: '2026-01-15', approved_by: 'system' },
43
+ { stage: 'qa', date: '2026-01-22', approved_by: 'system' },
44
+ { stage: 'canary', date: '2026-02-01', approved_by: 'system' },
45
+ { stage: 'ga', date: '2026-02-15', approved_by: 'system' }
46
+ ],
47
+ rolloutPercentage: 100,
48
+ qualityGates: {
49
+ errorRateThreshold: 0.01, // Max 1% error rate to advance
50
+ minUsageCount: 10, // Min 10 uses in current stage
51
+ minSuccessRate: 0.95 // 95% completion rate
52
+ },
53
+ featureFlags: {}
54
+ },
55
+ 'reliability-pack': {
56
+ name: 'Reliability Pack',
57
+ description: 'Premium reliability hardening workflow for incidents and regressions',
58
+ currentStage: 'ga',
59
+ stageHistory: [
60
+ { stage: 'draft', date: '2026-01-15', approved_by: 'system' },
61
+ { stage: 'qa', date: '2026-01-22', approved_by: 'system' },
62
+ { stage: 'canary', date: '2026-02-01', approved_by: 'system' },
63
+ { stage: 'ga', date: '2026-02-15', approved_by: 'system' }
64
+ ],
65
+ rolloutPercentage: 100,
66
+ qualityGates: {
67
+ errorRateThreshold: 0.01,
68
+ minUsageCount: 10,
69
+ minSuccessRate: 0.95
70
+ },
71
+ featureFlags: {}
72
+ },
73
+ 'growth-pack': {
74
+ name: 'Growth Pack',
75
+ description: 'Premium experimentation workflow for activation and conversion improvements',
76
+ currentStage: 'ga',
77
+ stageHistory: [
78
+ { stage: 'draft', date: '2026-01-15', approved_by: 'system' },
79
+ { stage: 'qa', date: '2026-01-22', approved_by: 'system' },
80
+ { stage: 'canary', date: '2026-02-01', approved_by: 'system' },
81
+ { stage: 'ga', date: '2026-02-15', approved_by: 'system' }
82
+ ],
83
+ rolloutPercentage: 100,
84
+ qualityGates: {
85
+ errorRateThreshold: 0.01,
86
+ minUsageCount: 10,
87
+ minSuccessRate: 0.95
88
+ },
89
+ featureFlags: {}
90
+ }
91
+ };
92
+
93
+ /**
94
+ * Get pack lifecycle info
95
+ * @param {string} packName - Pack name (e.g., 'launch-pack')
96
+ * @returns {object|null} Pack lifecycle info
97
+ */
98
+ function getPackLifecycle(packName) {
99
+ return PACK_LIFECYCLE[packName] || null;
100
+ }
101
+
102
+ /**
103
+ * Get current stage for a pack
104
+ * @param {string} packName - Pack name
105
+ * @returns {string} Current stage
106
+ */
107
+ function getPackStage(packName) {
108
+ const lifecycle = getPackLifecycle(packName);
109
+ return lifecycle?.currentStage || 'draft';
110
+ }
111
+
112
+ /**
113
+ * Check if pack is visible to user based on stage and rollout
114
+ * @param {string} packName - Pack name
115
+ * @param {object} options - Options including tier, userId
116
+ * @returns {boolean} Whether pack is visible
117
+ */
118
+ function isPackVisibleToUser(packName, options = {}) {
119
+ const lifecycle = getPackLifecycle(packName);
120
+ if (!lifecycle) return false;
121
+
122
+ const stage = lifecycle.currentStage;
123
+ const rollout = lifecycle.rolloutPercentage;
124
+
125
+ // Draft: never visible
126
+ if (stage === 'draft') return false;
127
+
128
+ // QA: only visible to team/enterprise
129
+ if (stage === 'qa') {
130
+ const tier = options.tier || 'free';
131
+ return ['team', 'enterprise'].includes(tier);
132
+ }
133
+
134
+ // Canary/GA: check rollout percentage
135
+ if (rollout === 100) return true;
136
+ if (rollout === 0) return false;
137
+
138
+ // Hash user ID to determine if in rollout
139
+ const userId = options.userId || options.deviceId || 'anonymous';
140
+ const hash = simpleHash(userId + packName);
141
+ const bucket = hash % 100;
142
+
143
+ return bucket < rollout;
144
+ }
145
+
146
+ /**
147
+ * Simple hash function for rollout bucketing
148
+ * @param {string} str - String to hash
149
+ * @returns {number} Hash value 0-999999
150
+ */
151
+ function simpleHash(str) {
152
+ let hash = 0;
153
+ for (let i = 0; i < str.length; i++) {
154
+ const char = str.charCodeAt(i);
155
+ hash = ((hash << 5) - hash) + char;
156
+ hash = hash & hash; // Convert to 32-bit integer
157
+ }
158
+ return Math.abs(hash);
159
+ }
160
+
161
+ /**
162
+ * Check if stage can be advanced based on quality gates
163
+ * @param {string} packName - Pack name
164
+ * @param {object} metrics - Current metrics { errorRate, usageCount, successRate }
165
+ * @returns {object} { canAdvance, reasons }
166
+ */
167
+ function checkQualityGates(packName, metrics = {}) {
168
+ const lifecycle = getPackLifecycle(packName);
169
+ if (!lifecycle) {
170
+ return { canAdvance: false, reasons: ['Pack not found'] };
171
+ }
172
+
173
+ const gates = lifecycle.qualityGates;
174
+ const reasons = [];
175
+
176
+ // Check error rate
177
+ if (metrics.errorRate !== undefined && metrics.errorRate > gates.errorRateThreshold) {
178
+ reasons.push(`Error rate ${(metrics.errorRate * 100).toFixed(2)}% exceeds threshold ${(gates.errorRateThreshold * 100).toFixed(2)}%`);
179
+ }
180
+
181
+ // Check usage count
182
+ if (metrics.usageCount !== undefined && metrics.usageCount < gates.minUsageCount) {
183
+ reasons.push(`Usage count ${metrics.usageCount} below minimum ${gates.minUsageCount}`);
184
+ }
185
+
186
+ // Check success rate
187
+ if (metrics.successRate !== undefined && metrics.successRate < gates.minSuccessRate) {
188
+ reasons.push(`Success rate ${(metrics.successRate * 100).toFixed(2)}% below threshold ${(gates.minSuccessRate * 100).toFixed(2)}%`);
189
+ }
190
+
191
+ return {
192
+ canAdvance: reasons.length === 0,
193
+ reasons
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Get next stage in lifecycle
199
+ * @param {string} currentStage - Current stage
200
+ * @returns {string|null} Next stage or null if at GA
201
+ */
202
+ function getNextStage(currentStage) {
203
+ const index = STAGE_ORDER.indexOf(currentStage);
204
+ if (index === -1 || index === STAGE_ORDER.length - 1) return null;
205
+ return STAGE_ORDER[index + 1];
206
+ }
207
+
208
+ /**
209
+ * Get previous stage in lifecycle
210
+ * @param {string} currentStage - Current stage
211
+ * @returns {string|null} Previous stage or null if at draft
212
+ */
213
+ function getPreviousStage(currentStage) {
214
+ const index = STAGE_ORDER.indexOf(currentStage);
215
+ if (index <= 0) return null;
216
+ return STAGE_ORDER[index - 1];
217
+ }
218
+
219
+ /**
220
+ * List all pack lifecycle statuses
221
+ * @returns {Array} Pack status list
222
+ */
223
+ function listPackStatuses() {
224
+ return Object.entries(PACK_LIFECYCLE).map(([key, pack]) => ({
225
+ key,
226
+ name: pack.name,
227
+ description: pack.description,
228
+ currentStage: pack.currentStage,
229
+ rolloutPercentage: pack.rolloutPercentage,
230
+ lastUpdate: pack.stageHistory[pack.stageHistory.length - 1]?.date
231
+ }));
232
+ }
233
+
234
+ /**
235
+ * Get stage badge for display
236
+ * @param {string} stage - Stage name
237
+ * @returns {string} Badge string
238
+ */
239
+ function getStageBadge(stage) {
240
+ const badges = {
241
+ draft: '[draft]',
242
+ qa: '[QA]',
243
+ canary: '[canary]',
244
+ ga: '[GA]'
245
+ };
246
+ return badges[stage] || `[${stage}]`;
247
+ }
248
+
249
+ module.exports = {
250
+ LIFECYCLE_STAGES,
251
+ STAGE_ORDER,
252
+ DEFAULT_ROLLOUT,
253
+ PACK_LIFECYCLE,
254
+ getPackLifecycle,
255
+ getPackStage,
256
+ isPackVisibleToUser,
257
+ checkQualityGates,
258
+ getNextStage,
259
+ getPreviousStage,
260
+ listPackStatuses,
261
+ getStageBadge
262
+ };