pairit 0.3.1 → 0.3.3

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 (2) hide show
  1. package/dist/index.js +140 -3
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9806,10 +9806,131 @@ async function attachHtmlToCompiledConfig(configPath, compiled) {
9806
9806
  }
9807
9807
  return sources;
9808
9808
  }
9809
+ function listWorkspaceComponents(config) {
9810
+ const pages = config.pages;
9811
+ const pageList = Array.isArray(pages) ? pages.filter(isRecord) : isRecord(pages) ? Object.values(pages).filter(isRecord) : [];
9812
+ const result = [];
9813
+ for (const page of pageList) {
9814
+ if (!Array.isArray(page.components))
9815
+ continue;
9816
+ for (const comp of page.components) {
9817
+ if (!isRecord(comp) || comp.type !== "live-workspace")
9818
+ continue;
9819
+ const props = isRecord(comp.props) ? comp.props : {};
9820
+ const id = typeof comp.id === "string" ? comp.id : "live-workspace";
9821
+ result.push({ id, props });
9822
+ }
9823
+ }
9824
+ return result;
9825
+ }
9826
+ async function resolveWorkspaceSchema(schema, componentId, configDir) {
9827
+ if (typeof schema !== "string" || !schema.trim().endsWith(".json")) {
9828
+ throw new Error(`live-workspace ${componentId}: schema must be a .json file`);
9829
+ }
9830
+ const absolutePath = path2.resolve(configDir, schema.trim());
9831
+ const rel = path2.relative(configDir, absolutePath);
9832
+ if (rel.startsWith("..") || path2.isAbsolute(rel)) {
9833
+ throw new Error(`live-workspace ${componentId}: schema must stay inside the config directory`);
9834
+ }
9835
+ try {
9836
+ JSON.parse(await readFile2(absolutePath, "utf8"));
9837
+ } catch (error) {
9838
+ throw new Error(`live-workspace ${componentId}: can't read schema ${schema}: ${error.message}`);
9839
+ }
9840
+ return absolutePath;
9841
+ }
9842
+ async function inspectWorkspaceFiles(configPath, config) {
9843
+ const configDir = await resolveConfigDir(configPath);
9844
+ const errors = [];
9845
+ for (const comp of listWorkspaceComponents(config)) {
9846
+ try {
9847
+ if (comp.props.renderer !== undefined) {
9848
+ await resolveHtmlSource(comp.props.renderer, comp.id, configDir);
9849
+ }
9850
+ if (comp.props.schema !== undefined) {
9851
+ await resolveWorkspaceSchema(comp.props.schema, comp.id, configDir);
9852
+ }
9853
+ } catch (error) {
9854
+ errors.push(error instanceof Error ? error.message : String(error));
9855
+ }
9856
+ }
9857
+ if (errors.length)
9858
+ throw new Error(errors.join(`
9859
+ `));
9860
+ }
9861
+ async function attachWorkspaceFilesToCompiledConfig(configPath, compiled) {
9862
+ const configDir = await resolveConfigDir(configPath);
9863
+ for (const comp of listWorkspaceComponents(compiled)) {
9864
+ if (comp.props.renderer !== undefined) {
9865
+ const resolved = await resolveHtmlSource(comp.props.renderer, comp.id, configDir);
9866
+ comp.props.rendererHtml = await readFile2(resolved.absolutePath, "utf8");
9867
+ }
9868
+ if (comp.props.schema !== undefined) {
9869
+ const schemaPath = await resolveWorkspaceSchema(comp.props.schema, comp.id, configDir);
9870
+ comp.props.schema = JSON.parse(await readFile2(schemaPath, "utf8"));
9871
+ }
9872
+ }
9873
+ }
9874
+
9875
+ // src/randomize-lint.ts
9876
+ function lintRandomization(config) {
9877
+ const errors = [];
9878
+ const groupSize = singleGroupSize(config);
9879
+ const check = (where, settings) => {
9880
+ if (settings.distribute !== true)
9881
+ return;
9882
+ if (settings.scope !== "group") {
9883
+ errors.push(`${where}: distribute: true needs scope: group`);
9884
+ }
9885
+ const conditions = settings.conditions;
9886
+ if (!Array.isArray(conditions) || conditions.length === 0) {
9887
+ errors.push(`${where}: distribute: true needs a conditions list`);
9888
+ return;
9889
+ }
9890
+ if (groupSize !== undefined && conditions.length !== groupSize) {
9891
+ errors.push(`${where}: distribute: true needs one condition per group member (${conditions.length} conditions, groups of ${groupSize})`);
9892
+ }
9893
+ };
9894
+ config.pages?.forEach((page, pageIndex) => {
9895
+ const pageName = page.id ?? `pages[${pageIndex}]`;
9896
+ page.onEnter?.forEach((action, actionIndex) => {
9897
+ const a = action;
9898
+ if (a?.type === "randomize") {
9899
+ check(`${pageName}.onEnter[${actionIndex}]`, a);
9900
+ }
9901
+ });
9902
+ page.components?.forEach((component, componentIndex) => {
9903
+ const c = component;
9904
+ if (c?.type === "randomization" && c.props) {
9905
+ check(`${pageName}.components[${componentIndex}]`, c.props);
9906
+ }
9907
+ });
9908
+ });
9909
+ return errors;
9910
+ }
9911
+ function singleGroupSize(config) {
9912
+ const sizes = new Set;
9913
+ if (Array.isArray(config.matchmaking)) {
9914
+ for (const pool of config.matchmaking) {
9915
+ const size = pool?.num_users;
9916
+ sizes.add(typeof size === "number" ? size : 2);
9917
+ }
9918
+ }
9919
+ for (const page of config.pages ?? []) {
9920
+ for (const component of page.components ?? []) {
9921
+ const c = component;
9922
+ if (c?.type !== "matchmaking" || c.props?.poolId !== undefined)
9923
+ continue;
9924
+ const size = c.props?.num_users;
9925
+ sizes.add(typeof size === "number" ? size : 2);
9926
+ }
9927
+ }
9928
+ return sizes.size === 1 ? [...sizes][0] : undefined;
9929
+ }
9809
9930
 
9810
9931
  // src/index.ts
9811
9932
  var program2 = new Command;
9812
- program2.name("pairit").description("CLI for Pairit experiment configs").version("0.3.1").option("--env <name>", "Target environment: staging or production", (value) => {
9933
+ program2.name("pairit").description("CLI for Pairit experiment configs").version("0.3.3").option("--env <name>", "Target environment: staging or production", (value) => {
9813
9934
  try {
9814
9935
  return parseEnvName(value);
9815
9936
  } catch (error) {
@@ -10101,9 +10222,11 @@ async function lintConfig(configPath) {
10101
10222
  });
10102
10223
  });
10103
10224
  }
10225
+ errors.push(...lintRandomization(config));
10104
10226
  if (errors.length) {
10105
10227
  throw new Error(errors.join(", "));
10106
10228
  }
10229
+ await inspectWorkspaceFiles(configPath, config);
10107
10230
  return inspectHtmlSources(configPath, config);
10108
10231
  }
10109
10232
  async function compileConfig(configPath) {
@@ -10162,12 +10285,17 @@ async function compileConfig(configPath) {
10162
10285
  }
10163
10286
  async function buildUploadPayload(configPath, options) {
10164
10287
  const sourceConfig = await loadConfig(configPath);
10288
+ const randomizationErrors = lintRandomization(sourceConfig);
10289
+ if (randomizationErrors.length) {
10290
+ throw new Error(randomizationErrors.join(", "));
10291
+ }
10165
10292
  const allowRetake = sourceConfig.allowRetake === true;
10166
10293
  const requireAuth = sourceConfig.requireAuth;
10167
10294
  const compiledPath = await compileConfig(configPath);
10168
10295
  const compiledContent = await readFile3(compiledPath, "utf8");
10169
10296
  const parsed = JSON.parse(compiledContent);
10170
10297
  const attachments = await attachHtmlToCompiledConfig(configPath, parsed);
10298
+ await attachWorkspaceFilesToCompiledConfig(configPath, parsed);
10171
10299
  const hashBuffer = createHash("sha256").update(JSON.stringify(parsed)).digest();
10172
10300
  const checksum = hashBuffer.toString("hex");
10173
10301
  const explicitName = options.name ?? sourceConfig.name;
@@ -10357,6 +10485,7 @@ async function exportData(configId, options) {
10357
10485
  await fetchAndWriteExport(`/data/${cfg}/groups`, path3.join(outDir, `${configId}-groups`), (r) => r.groups ?? [], format, (group) => group, "groups");
10358
10486
  await fetchAndWriteExport(`/data/${cfg}/survey-responses`, path3.join(outDir, `${configId}-survey-responses`), (r) => r.surveyResponses ?? [], format, flattenSurveyResponse, "survey responses");
10359
10487
  await fetchAndWriteExport(`/data/${cfg}/workspace-documents`, path3.join(outDir, `${configId}-workspace-documents`), (r) => r.workspaceDocuments ?? [], format, flattenWorkspaceDocument, "workspace documents");
10488
+ await fetchAndWriteExport(`/data/${cfg}/workspace-updates`, path3.join(outDir, `${configId}-workspace-updates`), (r) => r.workspaceUpdates ?? [], format, (update) => update, "workspace updates");
10360
10489
  console.log(`
10361
10490
  Files written to: ${outDir}`);
10362
10491
  }
@@ -10411,13 +10540,21 @@ function flattenSurveyResponse(response) {
10411
10540
  return flattened;
10412
10541
  }
10413
10542
  function flattenWorkspaceDocument(doc) {
10414
- const { fields, ...rest } = doc;
10415
- const flattened = { ...rest };
10543
+ const { fields, authorship, fieldAuthors, ...rest } = doc;
10544
+ const flattened = {
10545
+ ...rest,
10546
+ authorship: serializeValue(authorship)
10547
+ };
10416
10548
  if (fields && typeof fields === "object") {
10417
10549
  for (const [key, value] of Object.entries(fields)) {
10418
10550
  flattened[`fields.${key}`] = serializeValue(value);
10419
10551
  }
10420
10552
  }
10553
+ if (fieldAuthors && typeof fieldAuthors === "object") {
10554
+ for (const [key, origin] of Object.entries(fieldAuthors)) {
10555
+ flattened[`fieldAuthors.${key}`] = origin;
10556
+ }
10557
+ }
10421
10558
  return flattened;
10422
10559
  }
10423
10560
  function serializeValue(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pairit",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "CLI for the Pairit behavioral science experiment platform",
5
5
  "type": "module",
6
6
  "bin": {