crewly 1.20.193 → 1.20.194

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 (144) hide show
  1. package/config/skills/agent/core/experiment-card/SKILL.md +96 -0
  2. package/config/skills/agent/core/experiment-card/execute.sh +147 -0
  3. package/config/skills/agent/core/experiment-card/execute.test.sh +91 -0
  4. package/config/skills/registry.json +6 -5
  5. package/dist/backend/backend/src/constants.d.ts +58 -1
  6. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  7. package/dist/backend/backend/src/constants.js +61 -1
  8. package/dist/backend/backend/src/constants.js.map +1 -1
  9. package/dist/backend/backend/src/controllers/experiments/experiments.routes.d.ts +27 -0
  10. package/dist/backend/backend/src/controllers/experiments/experiments.routes.d.ts.map +1 -0
  11. package/dist/backend/backend/src/controllers/experiments/experiments.routes.js +96 -0
  12. package/dist/backend/backend/src/controllers/experiments/experiments.routes.js.map +1 -0
  13. package/dist/backend/backend/src/index.d.ts.map +1 -1
  14. package/dist/backend/backend/src/index.js +39 -3
  15. package/dist/backend/backend/src/index.js.map +1 -1
  16. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  17. package/dist/backend/backend/src/routes/api.routes.js +3 -0
  18. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  19. package/dist/backend/backend/src/services/agent/auditor-scheduler.service.d.ts.map +1 -1
  20. package/dist/backend/backend/src/services/agent/auditor-scheduler.service.js +3 -0
  21. package/dist/backend/backend/src/services/agent/auditor-scheduler.service.js.map +1 -1
  22. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  23. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -0
  24. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  25. package/dist/backend/backend/src/services/canva/canva-token.service.d.ts.map +1 -1
  26. package/dist/backend/backend/src/services/canva/canva-token.service.js +5 -0
  27. package/dist/backend/backend/src/services/canva/canva-token.service.js.map +1 -1
  28. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +14 -3
  29. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
  30. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +28 -4
  31. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
  32. package/dist/backend/backend/src/services/experiments/experiment-verdict.d.ts +88 -0
  33. package/dist/backend/backend/src/services/experiments/experiment-verdict.d.ts.map +1 -0
  34. package/dist/backend/backend/src/services/experiments/experiment-verdict.js +226 -0
  35. package/dist/backend/backend/src/services/experiments/experiment-verdict.js.map +1 -0
  36. package/dist/backend/backend/src/services/experiments/experiment.service.d.ts +301 -0
  37. package/dist/backend/backend/src/services/experiments/experiment.service.d.ts.map +1 -0
  38. package/dist/backend/backend/src/services/experiments/experiment.service.js +754 -0
  39. package/dist/backend/backend/src/services/experiments/experiment.service.js.map +1 -0
  40. package/dist/backend/backend/src/services/experiments/experiment.wiring.d.ts +73 -0
  41. package/dist/backend/backend/src/services/experiments/experiment.wiring.d.ts.map +1 -0
  42. package/dist/backend/backend/src/services/experiments/experiment.wiring.js +142 -0
  43. package/dist/backend/backend/src/services/experiments/experiment.wiring.js.map +1 -0
  44. package/dist/backend/backend/src/services/experiments/seo-ops-metric.fetcher.d.ts +60 -0
  45. package/dist/backend/backend/src/services/experiments/seo-ops-metric.fetcher.d.ts.map +1 -0
  46. package/dist/backend/backend/src/services/experiments/seo-ops-metric.fetcher.js +113 -0
  47. package/dist/backend/backend/src/services/experiments/seo-ops-metric.fetcher.js.map +1 -0
  48. package/dist/backend/backend/src/services/google/google-workspace-token.service.d.ts.map +1 -1
  49. package/dist/backend/backend/src/services/google/google-workspace-token.service.js +5 -0
  50. package/dist/backend/backend/src/services/google/google-workspace-token.service.js.map +1 -1
  51. package/dist/backend/backend/src/services/messaging/queue-processor.service.d.ts +11 -3
  52. package/dist/backend/backend/src/services/messaging/queue-processor.service.d.ts.map +1 -1
  53. package/dist/backend/backend/src/services/messaging/queue-processor.service.js +24 -7
  54. package/dist/backend/backend/src/services/messaging/queue-processor.service.js.map +1 -1
  55. package/dist/backend/backend/src/services/microsoft/microsoft-token.service.d.ts.map +1 -1
  56. package/dist/backend/backend/src/services/microsoft/microsoft-token.service.js +5 -0
  57. package/dist/backend/backend/src/services/microsoft/microsoft-token.service.js.map +1 -1
  58. package/dist/backend/backend/src/services/orc/orc-status-router.service.d.ts.map +1 -1
  59. package/dist/backend/backend/src/services/orc/orc-status-router.service.js +3 -2
  60. package/dist/backend/backend/src/services/orc/orc-status-router.service.js.map +1 -1
  61. package/dist/backend/backend/src/services/people/acting-for.service.d.ts +42 -5
  62. package/dist/backend/backend/src/services/people/acting-for.service.d.ts.map +1 -1
  63. package/dist/backend/backend/src/services/people/acting-for.service.js +80 -5
  64. package/dist/backend/backend/src/services/people/acting-for.service.js.map +1 -1
  65. package/dist/backend/backend/src/services/people/grant-sharing.d.ts +5 -4
  66. package/dist/backend/backend/src/services/people/grant-sharing.d.ts.map +1 -1
  67. package/dist/backend/backend/src/services/people/grant-sharing.js +12 -4
  68. package/dist/backend/backend/src/services/people/grant-sharing.js.map +1 -1
  69. package/dist/backend/backend/src/services/people/people-directory.service.d.ts +71 -13
  70. package/dist/backend/backend/src/services/people/people-directory.service.d.ts.map +1 -1
  71. package/dist/backend/backend/src/services/people/people-directory.service.js +164 -27
  72. package/dist/backend/backend/src/services/people/people-directory.service.js.map +1 -1
  73. package/dist/backend/backend/src/services/slack/slack-bot-ids.d.ts +33 -0
  74. package/dist/backend/backend/src/services/slack/slack-bot-ids.d.ts.map +1 -0
  75. package/dist/backend/backend/src/services/slack/slack-bot-ids.js +56 -0
  76. package/dist/backend/backend/src/services/slack/slack-bot-ids.js.map +1 -0
  77. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts +12 -0
  78. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  79. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +27 -8
  80. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  81. package/dist/backend/backend/src/services/slack/slack.service.d.ts +6 -0
  82. package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
  83. package/dist/backend/backend/src/services/slack/slack.service.js +8 -0
  84. package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
  85. package/dist/backend/backend/src/services/wiki/wiki-ingest.service.d.ts +3 -1
  86. package/dist/backend/backend/src/services/wiki/wiki-ingest.service.d.ts.map +1 -1
  87. package/dist/backend/backend/src/services/wiki/wiki-ingest.service.js.map +1 -1
  88. package/dist/backend/backend/src/services/workflow/message-scheduler.service.d.ts.map +1 -1
  89. package/dist/backend/backend/src/services/workflow/message-scheduler.service.js +3 -0
  90. package/dist/backend/backend/src/services/workflow/message-scheduler.service.js.map +1 -1
  91. package/dist/backend/backend/src/services/workflow/scheduler.service.d.ts.map +1 -1
  92. package/dist/backend/backend/src/services/workflow/scheduler.service.js +3 -0
  93. package/dist/backend/backend/src/services/workflow/scheduler.service.js.map +1 -1
  94. package/dist/backend/backend/src/types/experiment.types.d.ts +153 -0
  95. package/dist/backend/backend/src/types/experiment.types.d.ts.map +1 -0
  96. package/dist/backend/backend/src/types/experiment.types.js +32 -0
  97. package/dist/backend/backend/src/types/experiment.types.js.map +1 -0
  98. package/dist/backend/backend/src/types/messaging.types.d.ts +6 -0
  99. package/dist/backend/backend/src/types/messaging.types.d.ts.map +1 -1
  100. package/dist/backend/backend/src/types/messaging.types.js.map +1 -1
  101. package/dist/backend/build-info.json +2 -2
  102. package/dist/cli/backend/src/constants.d.ts +58 -1
  103. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  104. package/dist/cli/backend/src/constants.js +61 -1
  105. package/dist/cli/backend/src/constants.js.map +1 -1
  106. package/dist/cli/backend/src/services/agent/auditor-scheduler.service.d.ts.map +1 -1
  107. package/dist/cli/backend/src/services/agent/auditor-scheduler.service.js +3 -0
  108. package/dist/cli/backend/src/services/agent/auditor-scheduler.service.js.map +1 -1
  109. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  110. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -0
  111. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  112. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +14 -3
  113. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
  114. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +28 -4
  115. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
  116. package/dist/cli/backend/src/services/messaging/queue-processor.service.d.ts +11 -3
  117. package/dist/cli/backend/src/services/messaging/queue-processor.service.d.ts.map +1 -1
  118. package/dist/cli/backend/src/services/messaging/queue-processor.service.js +24 -7
  119. package/dist/cli/backend/src/services/messaging/queue-processor.service.js.map +1 -1
  120. package/dist/cli/backend/src/services/people/acting-for.service.d.ts +42 -5
  121. package/dist/cli/backend/src/services/people/acting-for.service.d.ts.map +1 -1
  122. package/dist/cli/backend/src/services/people/acting-for.service.js +80 -5
  123. package/dist/cli/backend/src/services/people/acting-for.service.js.map +1 -1
  124. package/dist/cli/backend/src/services/people/people-directory.service.d.ts +71 -13
  125. package/dist/cli/backend/src/services/people/people-directory.service.d.ts.map +1 -1
  126. package/dist/cli/backend/src/services/people/people-directory.service.js +164 -27
  127. package/dist/cli/backend/src/services/people/people-directory.service.js.map +1 -1
  128. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts +12 -0
  129. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  130. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +27 -8
  131. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  132. package/dist/cli/backend/src/services/slack/slack.service.d.ts +6 -0
  133. package/dist/cli/backend/src/services/slack/slack.service.d.ts.map +1 -1
  134. package/dist/cli/backend/src/services/slack/slack.service.js +8 -0
  135. package/dist/cli/backend/src/services/slack/slack.service.js.map +1 -1
  136. package/dist/cli/backend/src/services/wiki/wiki-ingest.service.d.ts +3 -1
  137. package/dist/cli/backend/src/services/wiki/wiki-ingest.service.d.ts.map +1 -1
  138. package/dist/cli/backend/src/services/wiki/wiki-ingest.service.js.map +1 -1
  139. package/dist/cli/backend/src/types/messaging.types.d.ts +6 -0
  140. package/dist/cli/backend/src/types/messaging.types.d.ts.map +1 -1
  141. package/dist/cli/backend/src/types/messaging.types.js.map +1 -1
  142. package/frontend/dist/assets/{index-465d1f8f.js → index-800118ca.js} +274 -274
  143. package/frontend/dist/index.html +1 -1
  144. package/package.json +1 -1
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Experiment windows and verdicts (issue #986).
3
+ *
4
+ * Windows: the baseline is the `windowDays` days that had settled when the
5
+ * change shipped (ending `lag` days before the ship day); the observation
6
+ * window is the `windowDays` days after the ship day (the ship day itself is
7
+ * a partial day and is left out). The result is due once the observation
8
+ * window's last day has settled (`lag` days later).
9
+ *
10
+ * Verdicts use simple rules with a minimum volume:
11
+ * - counts (clicks, impressions, sessions, events): equal-length Poisson
12
+ * comparison, z = (b − a) / √(a + b), with at least MIN_COUNT_VOLUME events
13
+ * across both windows;
14
+ * - ctr: two-proportion z-test, with enough impressions in each window and
15
+ * enough clicks overall;
16
+ * - position: Welch's t on the daily positions, with enough days that had
17
+ * impressions in each window (lower is better).
18
+ * |z| ≥ SIGNIFICANCE_Z in the hypothesis's direction → worked; enough volume
19
+ * but no such change → didn't; too little volume → inconclusive.
20
+ *
21
+ * specs/experiment-cards.md
22
+ *
23
+ * @module services/experiments/experiment-verdict
24
+ */
25
+ import type { ExperimentDirection, ExperimentMeasure, ExperimentSource, ExperimentVerdict, Measurement } from '../../types/experiment.types.js';
26
+ /** A date range (inclusive, YYYY-MM-DD). */
27
+ export interface DateRange {
28
+ start: string;
29
+ end: string;
30
+ }
31
+ /** The two windows of an experiment and when it can be measured. */
32
+ export interface ExperimentWindows {
33
+ baseline: DateRange;
34
+ observation: DateRange;
35
+ /** ISO time the result can be fetched (observation end + lag, start of the next day UTC) */
36
+ dueAt: string;
37
+ }
38
+ /** A verdict with its numbers. */
39
+ export interface VerdictOutcome {
40
+ verdict: ExperimentVerdict;
41
+ /** One line with the numbers */
42
+ reason: string;
43
+ /** Test statistic (z or Welch t), when there was enough volume to compute it */
44
+ statistic?: number;
45
+ /** Relative change of the headline value (null when the baseline is 0 / unknown) */
46
+ lift?: number | null;
47
+ }
48
+ /**
49
+ * Baseline / observation windows and the due time for a shipped experiment.
50
+ *
51
+ * @param shippedAt - When the change went live (ISO)
52
+ * @param windowDays - Days per window
53
+ * @param source - Metric source (sets the lag)
54
+ * @returns Windows
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * experimentWindows('2026-10-10T15:00:00Z', 14, 'gsc');
59
+ * // baseline 2026-09-24..2026-10-07, observation 2026-10-11..2026-10-24, due 2026-10-28
60
+ * ```
61
+ */
62
+ export declare function experimentWindows(shippedAt: string, windowDays: number, source: ExperimentSource): ExperimentWindows;
63
+ /**
64
+ * The direction a hypothesis means when it doesn't say: lower is better for position.
65
+ *
66
+ * @param measure - Measure
67
+ * @returns Default direction
68
+ */
69
+ export declare function defaultDirection(measure: ExperimentMeasure): ExperimentDirection;
70
+ /**
71
+ * Format a number for a verdict line.
72
+ *
73
+ * @param v - Value
74
+ * @param measure - Measure (ctr as a percentage, position to one decimal)
75
+ * @returns Text
76
+ */
77
+ export declare function formatValue(v: number | null | undefined, measure: ExperimentMeasure): string;
78
+ /**
79
+ * Label a measured experiment.
80
+ *
81
+ * @param measure - What was measured
82
+ * @param direction - Which way the hypothesis says it moves
83
+ * @param baseline - Baseline window
84
+ * @param result - Observation window
85
+ * @returns Verdict, one-line reason and the statistic
86
+ */
87
+ export declare function decideVerdict(measure: ExperimentMeasure, direction: ExperimentDirection, baseline: Measurement, result: Measurement): VerdictOutcome;
88
+ //# sourceMappingURL=experiment-verdict.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"experiment-verdict.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/experiments/experiment-verdict.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAIhJ,4CAA4C;AAC5C,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,oEAAoE;AACpE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,SAAS,CAAC;IACpB,WAAW,EAAE,SAAS,CAAC;IACvB,4FAA4F;IAC5F,KAAK,EAAE,MAAM,CAAC;CACf;AAED,kCAAkC;AAClC,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAyBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,GAAG,iBAAiB,CAWpH;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,mBAAmB,CAEhF;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAK5F;AA+DD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,iBAAiB,EAC1B,SAAS,EAAE,mBAAmB,EAC9B,QAAQ,EAAE,WAAW,EACrB,MAAM,EAAE,WAAW,GAClB,cAAc,CA2DhB"}
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Experiment windows and verdicts (issue #986).
3
+ *
4
+ * Windows: the baseline is the `windowDays` days that had settled when the
5
+ * change shipped (ending `lag` days before the ship day); the observation
6
+ * window is the `windowDays` days after the ship day (the ship day itself is
7
+ * a partial day and is left out). The result is due once the observation
8
+ * window's last day has settled (`lag` days later).
9
+ *
10
+ * Verdicts use simple rules with a minimum volume:
11
+ * - counts (clicks, impressions, sessions, events): equal-length Poisson
12
+ * comparison, z = (b − a) / √(a + b), with at least MIN_COUNT_VOLUME events
13
+ * across both windows;
14
+ * - ctr: two-proportion z-test, with enough impressions in each window and
15
+ * enough clicks overall;
16
+ * - position: Welch's t on the daily positions, with enough days that had
17
+ * impressions in each window (lower is better).
18
+ * |z| ≥ SIGNIFICANCE_Z in the hypothesis's direction → worked; enough volume
19
+ * but no such change → didn't; too little volume → inconclusive.
20
+ *
21
+ * specs/experiment-cards.md
22
+ *
23
+ * @module services/experiments/experiment-verdict
24
+ */
25
+ import { EXPERIMENT_CONSTANTS } from '../../constants.js';
26
+ const DAY_MS = 24 * 60 * 60 * 1000;
27
+ /**
28
+ * Format a UTC date as YYYY-MM-DD.
29
+ *
30
+ * @param ms - Epoch millis
31
+ * @returns Day string
32
+ */
33
+ function day(ms) {
34
+ return new Date(ms).toISOString().slice(0, 10);
35
+ }
36
+ /**
37
+ * Midnight UTC of the day an instant falls on.
38
+ *
39
+ * @param iso - ISO time
40
+ * @returns Epoch millis
41
+ * @throws Error on an invalid time
42
+ */
43
+ function dayStart(iso) {
44
+ const t = Date.parse(iso);
45
+ if (Number.isNaN(t))
46
+ throw new Error(`Invalid time: ${iso}`);
47
+ return Math.floor(t / DAY_MS) * DAY_MS;
48
+ }
49
+ /**
50
+ * Baseline / observation windows and the due time for a shipped experiment.
51
+ *
52
+ * @param shippedAt - When the change went live (ISO)
53
+ * @param windowDays - Days per window
54
+ * @param source - Metric source (sets the lag)
55
+ * @returns Windows
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * experimentWindows('2026-10-10T15:00:00Z', 14, 'gsc');
60
+ * // baseline 2026-09-24..2026-10-07, observation 2026-10-11..2026-10-24, due 2026-10-28
61
+ * ```
62
+ */
63
+ export function experimentWindows(shippedAt, windowDays, source) {
64
+ const ship = dayStart(shippedAt);
65
+ const lag = EXPERIMENT_CONSTANTS.SOURCE_LAG_DAYS[source] ?? 0;
66
+ const baseEnd = ship - lag * DAY_MS;
67
+ const obsStart = ship + DAY_MS;
68
+ const obsEnd = ship + windowDays * DAY_MS;
69
+ return {
70
+ baseline: { start: day(baseEnd - (windowDays - 1) * DAY_MS), end: day(baseEnd) },
71
+ observation: { start: day(obsStart), end: day(obsEnd) },
72
+ dueAt: new Date(obsEnd + (lag + 1) * DAY_MS).toISOString(),
73
+ };
74
+ }
75
+ /**
76
+ * The direction a hypothesis means when it doesn't say: lower is better for position.
77
+ *
78
+ * @param measure - Measure
79
+ * @returns Default direction
80
+ */
81
+ export function defaultDirection(measure) {
82
+ return EXPERIMENT_CONSTANTS.LOWER_IS_BETTER.includes(measure) ? 'decrease' : 'increase';
83
+ }
84
+ /**
85
+ * Format a number for a verdict line.
86
+ *
87
+ * @param v - Value
88
+ * @param measure - Measure (ctr as a percentage, position to one decimal)
89
+ * @returns Text
90
+ */
91
+ export function formatValue(v, measure) {
92
+ if (v === null || v === undefined || !Number.isFinite(v))
93
+ return 'n/a';
94
+ if (measure === 'ctr')
95
+ return `${(v * 100).toFixed(2)}%`;
96
+ if (measure === 'position')
97
+ return v.toFixed(1);
98
+ return String(Math.round(v * 100) / 100);
99
+ }
100
+ /**
101
+ * Relative change.
102
+ *
103
+ * @param from - Baseline
104
+ * @param to - Result
105
+ * @returns (to − from) / from, or null when from is 0 / missing
106
+ */
107
+ function liftOf(from, to) {
108
+ if (from === null || from === undefined || to === null || to === undefined || from === 0)
109
+ return null;
110
+ return (to - from) / from;
111
+ }
112
+ /**
113
+ * "+40%" / "−12%" / "".
114
+ *
115
+ * @param lift - Relative change
116
+ * @returns Text with a leading space, or empty
117
+ */
118
+ function liftText(lift) {
119
+ if (lift === null)
120
+ return '';
121
+ const pct = Math.round(lift * 1000) / 10;
122
+ return ` (${pct >= 0 ? '+' : ''}${pct}%)`;
123
+ }
124
+ /**
125
+ * Label from a statistic: worked when it is significant in the hypothesis's direction.
126
+ *
127
+ * @param stat - z or t (positive = the metric rose)
128
+ * @param direction - Hypothesis direction
129
+ * @returns worked / didnt and the wording
130
+ */
131
+ function judge(stat, direction) {
132
+ const z = EXPERIMENT_CONSTANTS.SIGNIFICANCE_Z;
133
+ const wanted = direction === 'increase' ? stat >= z : stat <= -z;
134
+ if (wanted)
135
+ return { verdict: 'worked', words: `a real ${direction}` };
136
+ const wrongWay = direction === 'increase' ? stat <= -z : stat >= z;
137
+ return { verdict: 'didnt', words: wrongWay ? 'it moved the wrong way' : 'no real change' };
138
+ }
139
+ /**
140
+ * Daily values of the days that had volume.
141
+ *
142
+ * @param m - Measurement
143
+ * @returns Values
144
+ */
145
+ function dailyValues(m) {
146
+ return m.days.filter((d) => d.volume > 0 && d.value !== null && Number.isFinite(d.value)).map((d) => d.value);
147
+ }
148
+ /**
149
+ * Mean and sample variance.
150
+ *
151
+ * @param xs - Values (at least 2)
152
+ * @returns Mean and variance
153
+ */
154
+ function meanVar(xs) {
155
+ const mean = xs.reduce((a, b) => a + b, 0) / xs.length;
156
+ const variance = xs.reduce((a, b) => a + (b - mean) ** 2, 0) / Math.max(1, xs.length - 1);
157
+ return { mean, variance };
158
+ }
159
+ /**
160
+ * Label a measured experiment.
161
+ *
162
+ * @param measure - What was measured
163
+ * @param direction - Which way the hypothesis says it moves
164
+ * @param baseline - Baseline window
165
+ * @param result - Observation window
166
+ * @returns Verdict, one-line reason and the statistic
167
+ */
168
+ export function decideVerdict(measure, direction, baseline, result) {
169
+ const C = EXPERIMENT_CONSTANTS;
170
+ if (measure === 'ctr') {
171
+ const n1 = baseline.impressions ?? baseline.volume;
172
+ const n2 = result.impressions ?? result.volume;
173
+ const c1 = baseline.clicks ?? 0;
174
+ const c2 = result.clicks ?? 0;
175
+ const head = `CTR ${formatValue(n1 ? c1 / n1 : null, measure)} → ${formatValue(n2 ? c2 / n2 : null, measure)}`;
176
+ const lift = liftOf(n1 ? c1 / n1 : null, n2 ? c2 / n2 : null);
177
+ if (n1 < C.MIN_IMPRESSIONS_PER_WINDOW || n2 < C.MIN_IMPRESSIONS_PER_WINDOW || c1 + c2 < C.MIN_CTR_CLICKS) {
178
+ return {
179
+ verdict: 'inconclusive',
180
+ reason: `${head}: too little data (${n1} / ${n2} impressions, ${c1 + c2} clicks; need ${C.MIN_IMPRESSIONS_PER_WINDOW} impressions per window and ${C.MIN_CTR_CLICKS} clicks)`,
181
+ lift,
182
+ };
183
+ }
184
+ const p = (c1 + c2) / (n1 + n2);
185
+ const se = Math.sqrt(p * (1 - p) * (1 / n1 + 1 / n2));
186
+ const stat = se > 0 ? (c2 / n2 - c1 / n1) / se : 0;
187
+ const j = judge(stat, direction);
188
+ return { verdict: j.verdict, reason: `${head}${liftText(lift)}, z = ${stat.toFixed(2)}: ${j.words}`, statistic: stat, lift };
189
+ }
190
+ if (measure === 'position') {
191
+ const a = dailyValues(baseline);
192
+ const b = dailyValues(result);
193
+ const head = `Average position ${formatValue(baseline.total, measure)} → ${formatValue(result.total, measure)}`;
194
+ const lift = liftOf(baseline.total, result.total);
195
+ const imps1 = baseline.impressions ?? baseline.volume;
196
+ const imps2 = result.impressions ?? result.volume;
197
+ if (a.length < C.MIN_POSITION_DAYS || b.length < C.MIN_POSITION_DAYS || imps1 < C.MIN_IMPRESSIONS_PER_WINDOW || imps2 < C.MIN_IMPRESSIONS_PER_WINDOW) {
198
+ return {
199
+ verdict: 'inconclusive',
200
+ reason: `${head}: too little data (${a.length} / ${b.length} days with impressions, ${imps1} / ${imps2} impressions; need ${C.MIN_POSITION_DAYS} days and ${C.MIN_IMPRESSIONS_PER_WINDOW} impressions per window)`,
201
+ lift,
202
+ };
203
+ }
204
+ const s1 = meanVar(a);
205
+ const s2 = meanVar(b);
206
+ const se = Math.sqrt(s1.variance / a.length + s2.variance / b.length);
207
+ const diff = s2.mean - s1.mean;
208
+ const stat = se > 0 ? diff / se : diff === 0 ? 0 : Math.sign(diff) * Infinity;
209
+ const j = judge(stat, direction);
210
+ const statText = Number.isFinite(stat) ? stat.toFixed(2) : stat > 0 ? '∞' : '−∞';
211
+ return { verdict: j.verdict, reason: `${head}, t = ${statText}: ${j.words}`, statistic: stat, lift };
212
+ }
213
+ // Counts: clicks, impressions, sessions, events.
214
+ const a = baseline.total ?? 0;
215
+ const b = result.total ?? 0;
216
+ const min = C.MIN_COUNT_VOLUME[measure] ?? 0;
217
+ const head = `${measure} ${formatValue(a, measure)} → ${formatValue(b, measure)}`;
218
+ const lift = liftOf(a, b);
219
+ if (a + b < min) {
220
+ return { verdict: 'inconclusive', reason: `${head}: too little data (${a + b} across both windows; need ${min})`, lift };
221
+ }
222
+ const stat = (b - a) / Math.sqrt(a + b);
223
+ const j = judge(stat, direction);
224
+ return { verdict: j.verdict, reason: `${head}${liftText(lift)}, z = ${stat.toFixed(2)}: ${j.words}`, statistic: stat, lift };
225
+ }
226
+ //# sourceMappingURL=experiment-verdict.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"experiment-verdict.js","sourceRoot":"","sources":["../../../../../../backend/src/services/experiments/experiment-verdict.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Experiment cards (issue #986, epic #982).
3
+ *
4
+ * An optimisation ticket carries an experiment: a hypothesis ("change X →
5
+ * metric Y from a to b"), a metric (Search Console / GA4 through the seo-ops
6
+ * skill), a baseline captured when the change ships and an observation
7
+ * window (default 14 days). When the window has settled the harness fetches
8
+ * the metric, labels the result worked / didn't / inconclusive (simple
9
+ * significance rules plus a minimum volume, see experiment-verdict.ts),
10
+ * resolves the agent's prediction, appends the result to the wiki
11
+ * experiment log and tells the owner.
12
+ *
13
+ * The experiment is a trace root: `traceId` (`exp:EXP-n`) names the run, and
14
+ * the timeline records every step (created, shipped, baseline, each fetch
15
+ * failure, measured, logged, reported), so the whole run can be reviewed.
16
+ *
17
+ * State: `~/.crewly/experiments.json`. A tick (every 15 min) moves
18
+ * experiments forward, so restarts lose nothing.
19
+ *
20
+ * specs/experiment-cards.md
21
+ *
22
+ * @module services/experiments/experiment.service
23
+ */
24
+ import { type ComponentLogger } from '../core/logger.service.js';
25
+ import { type Experiment, type ExperimentMetric, type ExperimentStatus, type ExperimentTicketLink } from '../../types/experiment.types.js';
26
+ import type { MetricFetcher } from './seo-ops-metric.fetcher.js';
27
+ /** A notice for the owner (same shape as the ticket autopilot's). */
28
+ export interface ExperimentOwnerNotice {
29
+ title: string;
30
+ message: string;
31
+ urgent: boolean;
32
+ }
33
+ /** The prediction calls the service makes (record-prediction / resolve-prediction). */
34
+ export interface ExperimentPredictions {
35
+ make(session: string, statement: string, confidence: number, resolveBy: string): Promise<{
36
+ id: string;
37
+ }>;
38
+ resolve(session: string, id: string, outcome: string, accurate: boolean): Promise<unknown>;
39
+ }
40
+ /** Dependencies. */
41
+ export interface ExperimentServiceDeps {
42
+ /** JSON store */
43
+ storeFile: string;
44
+ /** Fetch a metric over a window (seo-ops) */
45
+ fetchMetric: MetricFetcher;
46
+ /** When the linked ticket shipped (ISO), or null while it has not */
47
+ ticketShippedAt?: (link: ExperimentTicketLink) => Promise<string | null>;
48
+ /** Note the experiment on its ticket (best effort) */
49
+ noteOnTicket?: (link: ExperimentTicketLink, note: string) => Promise<void>;
50
+ predictions?: ExperimentPredictions;
51
+ /** Append a result to the wiki experiment log; false = not written (no vault) */
52
+ writeLog?: (experiment: Experiment, entry: string) => Promise<boolean>;
53
+ /** Tell the owner; false = not sent (Slack not connected) */
54
+ notifyOwner?: (notice: ExperimentOwnerNotice) => Promise<boolean>;
55
+ /** Does a file exist (the seo-ops config) */
56
+ fileExists?: (file: string) => Promise<boolean>;
57
+ now?: () => Date;
58
+ logger?: ComponentLogger;
59
+ }
60
+ /** Input of {@link ExperimentService.create}. */
61
+ export interface CreateExperimentInput {
62
+ title?: unknown;
63
+ hypothesis?: unknown;
64
+ direction?: unknown;
65
+ expected?: unknown;
66
+ metric?: unknown;
67
+ windowDays?: unknown;
68
+ ticket?: unknown;
69
+ confidence?: unknown;
70
+ }
71
+ /** An error with an HTTP status. */
72
+ export declare class ExperimentError extends Error {
73
+ readonly status: number;
74
+ /**
75
+ * @param status - HTTP status
76
+ * @param message - What went wrong
77
+ */
78
+ constructor(status: number, message: string);
79
+ }
80
+ /**
81
+ * Validate a metric definition.
82
+ *
83
+ * @param raw - Body
84
+ * @returns Metric
85
+ * @throws ExperimentError(400) with what is wrong
86
+ */
87
+ export declare function validateMetric(raw: unknown): ExperimentMetric;
88
+ /**
89
+ * Validate a ticket link.
90
+ *
91
+ * @param raw - Body
92
+ * @returns Link or undefined
93
+ * @throws ExperimentError(400)
94
+ */
95
+ export declare function validateTicketLink(raw: unknown): ExperimentTicketLink | undefined;
96
+ /**
97
+ * How a ticket link reads.
98
+ *
99
+ * @param link - Link
100
+ * @returns e.g. "ce-site/T-12" or "TKT-40"
101
+ */
102
+ export declare function ticketLabel(link: ExperimentTicketLink): string;
103
+ /**
104
+ * The metric as one line.
105
+ *
106
+ * @param m - Metric
107
+ * @returns e.g. "gsc clicks · page https://… · query visa"
108
+ */
109
+ export declare function metricLabel(m: ExperimentMetric): string;
110
+ /**
111
+ * The result as markdown (wiki log entry and owner message share it).
112
+ *
113
+ * @param e - A measured experiment
114
+ * @returns Markdown
115
+ */
116
+ export declare function resultSummary(e: Experiment): string;
117
+ /**
118
+ * Experiment cards: store, lifecycle and the measuring tick.
119
+ */
120
+ export declare class ExperimentService {
121
+ private readonly deps;
122
+ private static instance;
123
+ private readonly logger;
124
+ private readonly now;
125
+ private chain;
126
+ private timer;
127
+ /** A tick is running (fetches can be slow; ticks never overlap) */
128
+ private ticking;
129
+ /**
130
+ * @param deps - Dependencies
131
+ */
132
+ constructor(deps: ExperimentServiceDeps);
133
+ /**
134
+ * The installed service (null before boot).
135
+ *
136
+ * @returns Service or null
137
+ */
138
+ static getInstance(): ExperimentService | null;
139
+ /**
140
+ * Install the process service.
141
+ *
142
+ * @param service - Service, or null to clear
143
+ */
144
+ static setInstance(service: ExperimentService | null): void;
145
+ /**
146
+ * Start the periodic tick (runs once right away).
147
+ *
148
+ * @param intervalMs - Interval (0 = no timer; tests call {@link tick})
149
+ */
150
+ start(intervalMs?: number): void;
151
+ /** Stop the tick. */
152
+ stop(): void;
153
+ /**
154
+ * Run one store operation at a time.
155
+ *
156
+ * @param fn - Operation
157
+ * @returns Its result
158
+ */
159
+ private serial;
160
+ /**
161
+ * Read the store.
162
+ *
163
+ * @returns Store data (empty when missing)
164
+ */
165
+ private load;
166
+ /**
167
+ * Write the store atomically.
168
+ *
169
+ * @param data - Store data
170
+ */
171
+ private save;
172
+ /**
173
+ * Load, change one experiment, save.
174
+ *
175
+ * @param id - Experiment id
176
+ * @param fn - Change (mutates the experiment)
177
+ * @returns The experiment after the change
178
+ * @throws ExperimentError(404) when not found
179
+ */
180
+ private mutate;
181
+ /**
182
+ * Add a timeline entry.
183
+ *
184
+ * @param e - Experiment
185
+ * @param event - Event name
186
+ * @param detail - Optional detail
187
+ */
188
+ private record;
189
+ /**
190
+ * All experiments, newest first.
191
+ *
192
+ * @param filter - Optional status / ticket filter
193
+ * @returns Experiments
194
+ */
195
+ list(filter?: {
196
+ status?: string;
197
+ ticket?: string;
198
+ }): Promise<Experiment[]>;
199
+ /**
200
+ * One experiment.
201
+ *
202
+ * @param id - EXP-n
203
+ * @returns Experiment or null
204
+ */
205
+ get(id: string): Promise<Experiment | null>;
206
+ /**
207
+ * Create an experiment card.
208
+ *
209
+ * @param input - Hypothesis, metric, window, ticket, confidence
210
+ * @param caller - Agent session, or `owner`
211
+ * @returns The new experiment (status planned)
212
+ * @throws ExperimentError(400) on invalid input
213
+ */
214
+ create(input: CreateExperimentInput, caller: string): Promise<Experiment>;
215
+ /**
216
+ * Mark an experiment shipped: fix its windows, record the prediction and
217
+ * capture the baseline.
218
+ *
219
+ * @param id - EXP-n
220
+ * @param caller - Who shipped it
221
+ * @param shippedAt - When it went live (default now; not in the future)
222
+ * @returns The experiment
223
+ * @throws ExperimentError(404 / 409 / 400)
224
+ */
225
+ ship(id: string, caller: string, shippedAt?: string): Promise<Experiment>;
226
+ /**
227
+ * Cancel an experiment that has not been measured.
228
+ *
229
+ * @param id - EXP-n
230
+ * @param caller - Who
231
+ * @param reason - Why
232
+ * @returns The experiment
233
+ * @throws ExperimentError(404 / 409)
234
+ */
235
+ cancel(id: string, caller: string, reason?: string): Promise<Experiment>;
236
+ /**
237
+ * Measure a due experiment now (instead of waiting for the tick).
238
+ *
239
+ * @param id - EXP-n
240
+ * @returns The experiment
241
+ * @throws ExperimentError(404 / 409) when it is not running or not yet due
242
+ */
243
+ measureNow(id: string): Promise<Experiment>;
244
+ /**
245
+ * Move every experiment forward: ship planned ones whose ticket shipped,
246
+ * retry missing baselines, measure due ones, finish follow-ups.
247
+ *
248
+ * @returns Counts of what happened (zeros when a tick is already running)
249
+ */
250
+ tick(): Promise<{
251
+ shipped: number;
252
+ measured: number;
253
+ }>;
254
+ /**
255
+ * One pass over all experiments (see {@link tick}).
256
+ *
257
+ * @returns Counts of what happened
258
+ */
259
+ private runTick;
260
+ /**
261
+ * Record the creating agent's prediction that the hypothesis holds.
262
+ *
263
+ * @param id - EXP-n
264
+ */
265
+ private recordPrediction;
266
+ /**
267
+ * A fetch failed: count it, and tell the owner once when it keeps failing.
268
+ *
269
+ * @param id - EXP-n
270
+ * @param step - baseline / result
271
+ * @param err - The error
272
+ */
273
+ private fetchFailed;
274
+ /**
275
+ * Fetch the baseline window.
276
+ *
277
+ * @param id - EXP-n
278
+ */
279
+ private captureBaseline;
280
+ /**
281
+ * Fetch the observation window and label the result.
282
+ *
283
+ * @param id - EXP-n
284
+ */
285
+ private measure;
286
+ /**
287
+ * After a result: resolve the prediction, write the wiki log, tell the
288
+ * owner. Each step is retried by later ticks until it lands.
289
+ *
290
+ * @param id - EXP-n
291
+ */
292
+ private followUp;
293
+ }
294
+ /**
295
+ * Is this a status filter value?
296
+ *
297
+ * @param v - Query value
298
+ * @returns The status or undefined
299
+ */
300
+ export declare function statusFilter(v: unknown): ExperimentStatus | undefined;
301
+ //# sourceMappingURL=experiment.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"experiment.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/experiments/experiment.service.ts"],"names":[],"mappings":"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"}