@cyc-seattle/platform 1.39.0 → 1.39.2

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.
@@ -9,7 +9,7 @@ export function scheduleActivities(ctx) {
9
9
  for (const c of ctx.data.plan_closed_dates ?? []) {
10
10
  // The closed days are the Operations Plan's, not an area's.
11
11
  if (ctx.planOfVersion.get(c.version_id)?.kind === "program_area") {
12
- ctx.warn("closed_date_in_area_plan", `${c.name} is a closed day in a program area's plan, so it's ignored`, c.key);
12
+ ctx.warn("closed_date_in_area_plan", `${c.name} is a closed day in a Program Plan. Only the Operations Plan's closed days count`, c.key);
13
13
  continue;
14
14
  }
15
15
  closed.set(c.date, c.name);
@@ -18,7 +18,7 @@ export function scheduleActivities(ctx) {
18
18
  for (const activity of ctx.data.plan_activities) {
19
19
  const offering = ctx.offeringsById.get(activity.offering_id);
20
20
  if (offering === undefined) {
21
- ctx.warn("unknown_offering", `Activity ${activity.key} names a missing offering`, activity.key);
21
+ ctx.warn("unknown_offering", `Activity ${activity.key} is for an offering that isn't in the plan`, activity.key);
22
22
  continue;
23
23
  }
24
24
  if (!ctx.inYear(offering))
@@ -32,7 +32,7 @@ export function scheduleActivities(ctx) {
32
32
  const span = start !== null && end !== null ? end - start : 0;
33
33
  const closedFor = closed.get(activity.date);
34
34
  if (closedFor !== undefined && activity.kind !== "travel") {
35
- ctx.warn("closed_date", `${offering.name}'s ${activity.kind} falls on ${closedFor}, a closed day`, activity.key, {
35
+ ctx.warn("closed_date", `${offering.name}'s ${activity.kind} is on ${closedFor}, when ${ctx.data.organization.name} is closed`, activity.key, {
36
36
  version_id: activity.version_id,
37
37
  date: activity.date,
38
38
  });
@@ -79,7 +79,7 @@ function baseOf(ctx, owner) {
79
79
  continue;
80
80
  const product = ctx.products.get(row.product_id);
81
81
  if (plan?.kind === "program_area" && product?.program_area_id !== plan.program_area_id) {
82
- ctx.warn("bonus_outside_area", `${owner.role_name}'s bonus names ${product?.name ?? row.product_id}, which isn't in its plan's program area, so it's left out`, owner.key);
82
+ ctx.warn("bonus_outside_area", `${owner.role_name}'s bonus names ${product?.name ?? row.product_id}, which isn't in this plan's program area, so it's left out`, owner.key);
83
83
  continue;
84
84
  }
85
85
  products.add(row.product_id);
@@ -8,6 +8,17 @@ const ALLOCATION_WARNINGS = new Set([
8
8
  "closed_date",
9
9
  "position_double_booked",
10
10
  ]);
11
+ /** Each cost type as a warning names it. */
12
+ const COST_TYPES = {
13
+ coach_wages: "coach wages",
14
+ salaries: "salaries",
15
+ payroll_tax: "payroll tax",
16
+ ribs: "RIBs",
17
+ boat_maintenance: "boat maintenance",
18
+ resources: "resources",
19
+ travel: "travel",
20
+ cogs: "cost of goods sold",
21
+ };
11
22
  /** Lookups over the input, and the entries recorded so far, shared by every step. */
12
23
  export class Context {
13
24
  /** The plan year's first and last days. */
@@ -43,7 +54,7 @@ export class Context {
43
54
  this.offeringsByKey = new Map(data.plan_offerings.map((o) => [o.key, o]));
44
55
  this.rates = data.plan_rates[0];
45
56
  if (data.plan_rates.length > 1)
46
- this.warn("many_rates", "More than one plan_rates row, so the first is used");
57
+ this.warn("many_rates", "The Operations Plan has more than one set of rates. Only the first one counts");
47
58
  this.costAccounts = new Map(data.organization_cost_accounts
48
59
  .filter((c) => c.organization_id === data.organization.id)
49
60
  .map((c) => [c.cost_type, c.account_id]));
@@ -52,7 +63,7 @@ export class Context {
52
63
  costAccount(type) {
53
64
  const account = this.costAccounts.get(type);
54
65
  if (account === undefined) {
55
- this.warn("no_cost_account", `The organization has no account set for ${type}, so that cost is left out`, type);
66
+ this.warn("no_cost_account", `${this.data.organization.name} has no account for ${COST_TYPES[type]}, so that cost is left out`, type);
56
67
  }
57
68
  return account;
58
69
  }
@@ -109,7 +120,7 @@ export class Context {
109
120
  if (area === undefined)
110
121
  return product.revenue_account_id;
111
122
  if (product.revenue_account_id != null && product.revenue_account_id !== area.revenue_account_id) {
112
- this.warn("revenue_account_differs", `Product ${product.name} has its own revenue account, but its program area ${area.name}'s is used`, product.id);
123
+ this.warn("revenue_account_differs", `${product.name} has its own revenue account, but its revenue goes to ${area.name}'s account`, product.id);
113
124
  }
114
125
  return area.revenue_account_id;
115
126
  }
@@ -50,10 +50,10 @@ function checkAreas(ctx) {
50
50
  if (product === undefined)
51
51
  continue;
52
52
  if (product.kind === "program" && product.program_area_id == null) {
53
- ctx.warn("program_without_area", `Program ${product.name} has no program area`, product.id);
53
+ ctx.warn("program_without_area", `${product.name} is a program but has no program area`, product.id);
54
54
  }
55
55
  else if (product.kind !== "program" && product.program_area_id != null) {
56
- ctx.warn("area_on_non_program", `Product ${product.name} isn't a program, but has a program area`, product.id);
56
+ ctx.warn("area_on_non_program", `${product.name} isn't a program but has a program area`, product.id);
57
57
  }
58
58
  const plan = ctx.planOfVersion.get(offering.version_id);
59
59
  if (plan === undefined)
@@ -62,8 +62,10 @@ function checkAreas(ctx) {
62
62
  ? product.program_area_id != null
63
63
  : product.program_area_id != null && product.program_area_id !== plan.program_area_id;
64
64
  if (wrong) {
65
- const where = plan.kind === "operations" ? "the Operations Plan" : "another program area's plan";
66
- ctx.warn("offering_in_wrong_plan", `Offering ${offering.name} of ${product.name} is in ${where}`, offering.key);
65
+ const message = plan.kind === "operations"
66
+ ? `${offering.name} is in the Operations Plan, but its program is in a program area`
67
+ : `${offering.name} is in the Program Plan for another program area`;
68
+ ctx.warn("offering_in_wrong_plan", message, offering.key);
67
69
  }
68
70
  }
69
71
  }
@@ -77,7 +79,7 @@ function summarizePositions(ctx, coaching, charged, bonuses, doubleBooked) {
77
79
  ];
78
80
  for (const key of new Set(named)) {
79
81
  if (!known.has(key)) {
80
- ctx.warn("unknown_position", `A block or bonus names missing salaried position ${key}`, key);
82
+ ctx.warn("unknown_position", `A block or bonus names salaried position ${key}, which isn't in the plan`, key);
81
83
  }
82
84
  }
83
85
  return ctx.data.plan_salaried_positions.map((position) => {
@@ -31,8 +31,11 @@ export class CostCenterBooker {
31
31
  pending.forEach((entry, i) => {
32
32
  const w = weights[i] ?? null;
33
33
  if (w === null) {
34
- const what = entry.cost_center_type === "product" ? "Product" : "Program area";
35
- this.ctx.warn(`${entry.cost_center_type}_without_offerings`, `${what} ${entry.cost_center_key} has no offering in the plan year, so ${entry.line_key} stays there`, entry.cost_center_key ?? undefined);
34
+ const key = entry.cost_center_key ?? "";
35
+ const name = entry.cost_center_type === "product"
36
+ ? (this.ctx.products.get(key)?.name ?? key)
37
+ : (this.ctx.areas.get(key)?.name ?? key);
38
+ this.ctx.warn(`${entry.cost_center_type}_without_offerings`, `${name} has no offerings this year, so ${entry.line_name} isn't split among offerings`, entry.cost_center_key ?? undefined);
36
39
  this.ctx.book(entry);
37
40
  return;
38
41
  }
@@ -49,7 +52,7 @@ function checkCostCenter(ctx, type, key, line) {
49
52
  (type === "program_area" && key != null && ctx.areas.has(key)) ||
50
53
  (type === "pool" && key != null && ctx.data.plan_pools.some((p) => p.key === key));
51
54
  if (!known)
52
- ctx.warn("unknown_cost_center", `Line ${line} is on a missing ${type} ${key ?? ""}`, line);
55
+ ctx.warn("unknown_cost_center", `Line ${line} is charged to ${type.replace("_", " ")} ${key ?? ""}, which isn't in the plan`, line);
53
56
  return known;
54
57
  }
55
58
  /**
@@ -65,7 +68,7 @@ export function bookBudgetLines(ctx, booker) {
65
68
  const capital = ctx.accountType(line.account_id) === "asset";
66
69
  const onOrg = line.cost_center_type === "org" || capital;
67
70
  if (capital && line.cost_center_type !== "org") {
68
- ctx.warn("capital_off_org", `Line ${line.name} is capital, which only the organization pays for, so it's moved there`, line.key);
71
+ ctx.warn("capital_off_org", `${line.name} is capital, so it's charged to ${ctx.data.organization.name}, which pays for all capital`, line.key);
69
72
  }
70
73
  booker.add({
71
74
  cost_center_type: onOrg ? "org" : line.cost_center_type,
@@ -88,7 +91,7 @@ export function bookLaborLines(ctx, booker) {
88
91
  continue;
89
92
  const level = levels.get(line.staff_level_key);
90
93
  if (level === undefined) {
91
- ctx.warn("unknown_staff_level", `Labor line ${line.key} names a missing staff level`, line.key);
94
+ ctx.warn("unknown_staff_level", `Labor line ${line.key} has a staff level that isn't in the Operations Plan, so it's left out`, line.key);
92
95
  continue;
93
96
  }
94
97
  booker.add({
@@ -16,7 +16,7 @@ export function allocatePools(ctx) {
16
16
  return;
17
17
  ctx.entries.splice(0, ctx.entries.length, ...kept);
18
18
  if (total <= 0) {
19
- ctx.warn("no_mtdc", "No offering in the plan year has MTDC, so the shared costs stay with the organization");
19
+ ctx.warn("no_mtdc", `No offering this year has costs that count toward MTDC, so ${ctx.data.organization.name} keeps all the shared costs`);
20
20
  }
21
21
  for (const e of pooled) {
22
22
  const pool = pools.get(e.cost_center_key ?? "");
@@ -18,7 +18,7 @@ export function bookRevenue(ctx) {
18
18
  for (const offering of ctx.fyOfferings()) {
19
19
  const product = ctx.products.get(offering.product_id);
20
20
  if (product === undefined) {
21
- ctx.warn("unknown_product", `Offering ${offering.key} names a missing product`, offering.key);
21
+ ctx.warn("unknown_product", `Offering ${offering.key} is for a product that doesn't exist`, offering.key);
22
22
  continue;
23
23
  }
24
24
  const enrollment = ctx.enrollment.get(offering.id) ?? 0;
@@ -28,7 +28,7 @@ export function bookRevenue(ctx) {
28
28
  continue;
29
29
  const revenueAccount = ctx.revenueAccount(product);
30
30
  if (revenueAccount == null) {
31
- ctx.warn("no_revenue_account", `Product ${product.name} has no revenue account`, product.id);
31
+ ctx.warn("no_revenue_account", `${product.name} has no revenue account, so its revenue is left out`, product.id);
32
32
  continue;
33
33
  }
34
34
  const book = (account, amount, line, name, explanation, mtdc_share = 1) => ctx.book({
@@ -29,7 +29,7 @@ export function bookSalaries(ctx, coaching, booker) {
29
29
  .filter((b) => b.position_id === position.id)
30
30
  .flatMap((b) => {
31
31
  if ((b.product_id == null) === (b.program_area_id == null)) {
32
- ctx.warn("booking_cost_center", `Booking ${b.key} must name a product or a program area, not both or neither, so it's left out`, b.key);
32
+ ctx.warn("booking_cost_center", `Booking ${b.key} needs a product or a program area, but not both. It's left out`, b.key);
33
33
  return [];
34
34
  }
35
35
  return [
@@ -45,13 +45,13 @@ export function bookSalaries(ctx, coaching, booker) {
45
45
  }),
46
46
  ];
47
47
  if (rate === null && charges.length > 0) {
48
- ctx.warn("no_paid_hours", `${position.name} has hours charged to programs but no paid_hours_per_year, so they cost nothing`, position.key);
48
+ ctx.warn("no_paid_hours", `${position.name} has hours charged to programs but no paid hours per year, so those hours cost nothing`, position.key);
49
49
  }
50
50
  const available = salary * (1 - (position.ga_share ?? 0));
51
51
  const demand = charges.reduce((t, c) => t + c.hours * (rate ?? 0), 0);
52
52
  const scale = demand > available && demand > 0 ? available / demand : 1;
53
53
  if (scale < 1) {
54
- ctx.warn("position_overbooked", `${position.name}'s hours charged to programs cost more than its salary after ga_share, so they're scaled down`, position.key);
54
+ ctx.warn("position_overbooked", `${position.name}'s hours charged to programs cost more than its salary less its G&A share, so they're cut to fit`, position.key);
55
55
  }
56
56
  const sums = { coaching_hours: 0, coaching_cents: 0, booking_hours: 0, booking_cents: 0 };
57
57
  for (const c of charges) {
@@ -15,8 +15,15 @@ function sum(values) {
15
15
  * ends in "s", such as RIBs; any other name is used as given, since adding an "s" to "Laser Radial" reads wrong.
16
16
  */
17
17
  function countOf(n, name) {
18
- const plural = n !== 1 && /^\S+$/.test(name) && !/s$/i.test(name);
19
- return `${n} ${plural ? `${name}s` : name}`;
18
+ return `${n} ${n === 1 ? name : pluralOf(name)}`;
19
+ }
20
+ function pluralOf(name) {
21
+ return /^\S+$/.test(name) && !/s$/i.test(name) ? `${name}s` : name;
22
+ }
23
+ const day = new Intl.DateTimeFormat("en-US", { month: "long", day: "numeric", timeZone: "UTC" });
24
+ /** A date as a person says it: "July 13". */
25
+ function dayOf(date) {
26
+ return day.format(new Date(`${date}T00:00:00Z`));
20
27
  }
21
28
  /** Names as a sentence lists them: `a`, `a and b`, or `a, b, and c`. */
22
29
  function listOf(names) {
@@ -83,11 +90,11 @@ export class Staffing {
83
90
  continue;
84
91
  const row = rows.get(id);
85
92
  if (row === undefined) {
86
- this.ctx.warn("unknown_block", `Activity ${a.stored.key} names a missing block`, a.stored.key);
93
+ this.ctx.warn("unknown_block", `${a.offering.name}'s ${a.kind} is in a block that isn't in the plan`, a.stored.key);
87
94
  continue;
88
95
  }
89
96
  if (row.date !== a.date) {
90
- this.ctx.warn("block_date_mismatch", `${a.offering.name}'s ${a.kind} is in ${row.name}, which is on ${row.date}`, a.stored.key, { version_id: a.stored.version_id, date: a.date });
97
+ this.ctx.warn("block_date_mismatch", `${a.offering.name}'s ${a.kind} is in ${row.name}, which is on ${dayOf(row.date)}`, a.stored.key, { version_id: a.stored.version_id, date: a.date });
91
98
  }
92
99
  members.set(id, [...(members.get(id) ?? []), a]);
93
100
  }
@@ -162,14 +169,15 @@ export class Staffing {
162
169
  if (sailors <= 0)
163
170
  return;
164
171
  const scope = { version_id: block.row.version_id, date: block.row.date };
165
- const where = offeringsOf(block);
172
+ // The block's offerings, with the verb that agrees with them: "Guppies Week 2 has".
173
+ const has = `${offeringsOf(block)} ${new Set(block.members.map((a) => a.offering.id)).size > 1 ? "have" : "has"}`;
166
174
  let load = 0;
167
175
  let counted = 0;
168
176
  const required = [];
169
177
  for (const o of offerings) {
170
178
  const settings = programs.get(o.product_id);
171
179
  if (settings?.staff_level_key == null || settings.sailors_per_coach == null || settings.sailors_per_coach <= 0) {
172
- this.warnOnce("no_program_settings", `${o.name}'s program has no staff level or sailors per coach, so its sailors aren't checked against its staff`, o.product_id, { version_id: o.version_id, date: block.row.date });
180
+ this.warnOnce("no_program_settings", `${o.name}'s program has no staff level or sailors per coach. Add both to check that it has enough coaches`, o.product_id, { version_id: o.version_id, date: block.row.date });
173
181
  continue;
174
182
  }
175
183
  const n = ctx.enrollment.get(o.id) ?? 0;
@@ -188,7 +196,9 @@ export class Staffing {
188
196
  return required.some((r) => this.qualifiedFor(held, level, r)) ? s.count : 0;
189
197
  }));
190
198
  if (load > qualified + 1e-9) {
191
- ctx.warn("over_capacity", `${where}: ${counted} sailors, but the staff can take ${Math.floor((qualified * counted) / load + 1e-9)}`, block.row.key, scope);
199
+ ctx.warn("over_capacity", qualified === 0
200
+ ? `${has} ${countOf(counted, "sailor")} and no qualified coach`
201
+ : `${has} ${countOf(counted, "sailor")}, but enough qualified coaches for only ${Math.floor((qualified * counted) / load + 1e-9)}`, block.row.key, scope);
192
202
  }
193
203
  }
194
204
  if (!block.members.some((a) => a.uses_boats))
@@ -198,13 +208,15 @@ export class Staffing {
198
208
  for (const b of block.boats) {
199
209
  const perBoat = classes.get(b.boat_class_id)?.sailors_per_boat;
200
210
  if (perBoat == null || perBoat <= 0) {
201
- this.warnOnce("no_sailors_per_boat", `Boat class ${classes.get(b.boat_class_id)?.name ?? b.boat_class_id} has no sailors per boat, so its boats take no sailors`, b.boat_class_id, scope);
211
+ this.warnOnce("no_sailors_per_boat", `${classes.get(b.boat_class_id)?.name ?? b.boat_class_id} has no sailors per boat. Add it to check that the boats hold every sailor`, b.boat_class_id, scope);
202
212
  continue;
203
213
  }
204
214
  seats += (b.count + (b.sailor_owned ?? 0)) * perBoat;
205
215
  }
206
216
  if (sailors > seats) {
207
- ctx.warn("over_capacity", `${where}: ${sailors} sailors, but the boats can take ${seats}`, block.row.key, scope);
217
+ ctx.warn("over_capacity", seats === 0
218
+ ? `${has} ${countOf(sailors, "sailor")} and no boats`
219
+ : `${has} ${countOf(sailors, "sailor")}, but the boats hold only ${seats}`, block.row.key, scope);
208
220
  }
209
221
  }
210
222
  /**
@@ -226,10 +238,9 @@ export class Staffing {
226
238
  const peak = mostAtOnce(out);
227
239
  if (peak.total <= have)
228
240
  return;
229
- const whose = peak.at.length === 1
230
- ? `, for ${offeringsOf(peak.at[0].block)}`
231
- : `: ${listOf(peak.at.map(({ block, n }) => `${n} for ${offeringsOf(block)}`))}`;
232
- const message = `${countOf(peak.total, what)} out, but ${organization} has ${have}${whose}`;
241
+ const message = peak.at.length === 1
242
+ ? `${countOf(peak.total, what)} out at once for ${offeringsOf(peak.at[0].block)}, but ${organization} has only ${have}`
243
+ : `${countOf(peak.total, what)} out at once, but ${organization} has only ${have}: ${listOf(peak.at.map(({ block, n }) => `${n} for ${offeringsOf(block)}`))}`;
233
244
  for (const version of new Set(peak.at.map(({ block }) => block.row.version_id))) {
234
245
  ctx.warn(code, message, what, {
235
246
  version_id: version,
@@ -241,7 +252,7 @@ export class Staffing {
241
252
  for (const id of classIds) {
242
253
  if (fleet.get(id) == null) {
243
254
  for (const b of list.filter((x) => x.boats.some((y) => y.boat_class_id === id && y.count > 0))) {
244
- this.warnOnce("no_fleet_quantity", `The Operations Plan doesn't say how many ${classes.get(id)?.name ?? id} the organization has, so they aren't checked`, id, { version_id: b.row.version_id, date });
255
+ this.warnOnce("no_fleet_quantity", `The Operations Plan doesn't say how many ${pluralOf(classes.get(id)?.name ?? id)} ${organization} has. Add it to check that there are enough`, id, { version_id: b.row.version_id, date });
245
256
  }
246
257
  }
247
258
  check("over_inventory_boats", classes.get(id)?.name ?? id, fleet.get(id), (b) => sum(b.boats.filter((x) => x.boat_class_id === id).map((x) => x.count)));
@@ -339,7 +350,7 @@ export class Staffing {
339
350
  if (s.pay === "hourly") {
340
351
  const rate = position?.hourly_coaching_rate_cents;
341
352
  if (position !== undefined && rate == null) {
342
- this.warnOnce("no_hourly_rate", `${position.name} is paid by the hour to coach but has no hourly rate, so those hours cost nothing`, position.key, scope);
353
+ this.warnOnce("no_hourly_rate", `${position.name} is paid by the hour to coach but has no own hourly coaching rate, so those hours cost nothing`, position.key, scope);
343
354
  }
344
355
  wages += hours * (rate ?? 0);
345
356
  for (const [a, share] of block.shares) {
@@ -367,7 +378,7 @@ export class Staffing {
367
378
  }
368
379
  const level = s.staff_level_key == null ? undefined : levels.get(s.staff_level_key);
369
380
  if (level === undefined) {
370
- this.warnOnce("unknown_staff_level", `${block.row.name} has staff at a missing staff level, so they cost nothing`, s.staff_level_key ?? s.key, scope);
381
+ this.warnOnce("unknown_staff_level", `${block.row.name} has staff at a staff level that isn't in the Operations Plan, so they cost nothing`, s.staff_level_key ?? s.key, scope);
371
382
  continue;
372
383
  }
373
384
  if (level.pay_type !== "volunteer")
@@ -378,7 +389,7 @@ export class Staffing {
378
389
  for (const use of block.resources) {
379
390
  const resource = resources.get(use.resource_key);
380
391
  if (resource === undefined) {
381
- this.warnOnce("unknown_resource", `${block.row.name} uses missing resource ${use.resource_key}`, use.resource_key, scope);
392
+ this.warnOnce("unknown_resource", `${block.row.name} uses resource ${use.resource_key}, which isn't in the plan`, use.resource_key, scope);
382
393
  continue;
383
394
  }
384
395
  const rib = ribKind != null && resource.resource_kind_id === ribKind;
@@ -439,15 +450,15 @@ export class Staffing {
439
450
  .filter(travel)
440
451
  .map((a) => a.date)
441
452
  .sort()[0];
442
- this.warnOnce("trip_without_staff", `${trip.name} has no staff on a travel day's block, so no coach's airfare or lodging is charged`, trip.key, { version_id: trip.version_id, ...(first !== undefined && { date: first }) });
453
+ this.warnOnce("trip_without_staff", `${trip.name} has no staff on its travel days, so the plan has no airfare or lodging for its coaches`, trip.key, { version_id: trip.version_id, ...(first !== undefined && { date: first }) });
443
454
  }
444
455
  const lodging = ctx.rates?.lodging_per_night_cents ?? null;
445
456
  const mileage = ctx.rates?.mileage_rate_cents ?? null;
446
457
  if ((trip.nights ?? 0) > 0 && lodging === null) {
447
- this.warnOnce("no_rate", "Trips have nights but no lodging_per_night_cents rate", "lodging_per_night_cents");
458
+ this.warnOnce("no_rate", "Trips have nights, but the Operations Plan has no lodging per night", "lodging_per_night_cents");
448
459
  }
449
460
  if ((trip.miles ?? 0) > 0 && mileage === null) {
450
- this.warnOnce("no_rate", "Trips have miles but no mileage_rate_cents rate", "mileage_rate_cents");
461
+ this.warnOnce("no_rate", "Trips have miles, but the Operations Plan has no mileage rate", "mileage_rate_cents");
451
462
  }
452
463
  const amount = coaches * (trip.airfare_cents ?? 0) +
453
464
  coaches * (trip.nights ?? 0) * (lodging ?? 0) +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyc-seattle/platform",
3
- "version": "1.39.0",
3
+ "version": "1.39.2",
4
4
  "description": "Pulumi components for CYC's shared Google Cloud platform: WebService, the platform outputs reader, and the stacks that build the platform",
5
5
  "author": "CYC Community Sailing Center",
6
6
  "license": "Apache-2.0",