blogwright-analytics 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 (131) hide show
  1. package/README.md +162 -0
  2. package/dist/adapters/duckdb-ingest.d.ts +76 -0
  3. package/dist/adapters/duckdb-ingest.js +173 -0
  4. package/dist/adapters/duckdb-query.d.ts +56 -0
  5. package/dist/adapters/duckdb-query.js +80 -0
  6. package/dist/adapters/duckdb-session.d.ts +168 -0
  7. package/dist/adapters/duckdb-session.js +330 -0
  8. package/dist/app/_app/immutable/assets/0.BTQrrh5B.css +1 -0
  9. package/dist/app/_app/immutable/assets/2.CZSK3rT8.css +1 -0
  10. package/dist/app/_app/immutable/assets/BrushContext.D7c8UPey.css +1 -0
  11. package/dist/app/_app/immutable/assets/ChartAnnotations.CPxIG7Mw.css +1 -0
  12. package/dist/app/_app/immutable/assets/Circle.C5MKzgk2.css +1 -0
  13. package/dist/app/_app/immutable/assets/DefaultTooltip.C5-uctZ7.css +1 -0
  14. package/dist/app/_app/immutable/assets/Group.DV48xipa.css +1 -0
  15. package/dist/app/_app/immutable/assets/Labels.BxZ4NUVz.css +1 -0
  16. package/dist/app/_app/immutable/assets/Legend.CxnrE4Ye.css +1 -0
  17. package/dist/app/_app/immutable/assets/Line.fkmsECm9.css +1 -0
  18. package/dist/app/_app/immutable/assets/Path.CvpwNZ6g.css +1 -0
  19. package/dist/app/_app/immutable/assets/Rect.CtRaGMmQ.css +1 -0
  20. package/dist/app/_app/immutable/assets/Text.j9l35qB0.css +1 -0
  21. package/dist/app/_app/immutable/assets/TransformContext.Bs_HkpAk.css +1 -0
  22. package/dist/app/_app/immutable/assets/Voronoi.ce7atosu.css +1 -0
  23. package/dist/app/_app/immutable/chunks/-aNGNaBT.js +1 -0
  24. package/dist/app/_app/immutable/chunks/6djn-yLs.js +1 -0
  25. package/dist/app/_app/immutable/chunks/B1amyutE.js +1 -0
  26. package/dist/app/_app/immutable/chunks/B3vZDoek.js +1 -0
  27. package/dist/app/_app/immutable/chunks/B5KRA4hC.js +1 -0
  28. package/dist/app/_app/immutable/chunks/BClnVG6H.js +1 -0
  29. package/dist/app/_app/immutable/chunks/BID1NNRh.js +1 -0
  30. package/dist/app/_app/immutable/chunks/BR2LaRms.js +1 -0
  31. package/dist/app/_app/immutable/chunks/Bd1gDe3Y.js +1 -0
  32. package/dist/app/_app/immutable/chunks/Bjy-W4x2.js +81 -0
  33. package/dist/app/_app/immutable/chunks/Bl052uUt.js +1 -0
  34. package/dist/app/_app/immutable/chunks/Bye3lL0c.js +1 -0
  35. package/dist/app/_app/immutable/chunks/C58PZtCD.js +4 -0
  36. package/dist/app/_app/immutable/chunks/CAzydqEO.js +1 -0
  37. package/dist/app/_app/immutable/chunks/CCch3uox.js +1 -0
  38. package/dist/app/_app/immutable/chunks/CIlSMUH9.js +1 -0
  39. package/dist/app/_app/immutable/chunks/CO1vUXfR.js +1 -0
  40. package/dist/app/_app/immutable/chunks/CPbD8C65.js +5 -0
  41. package/dist/app/_app/immutable/chunks/CRTcXoMo.js +1 -0
  42. package/dist/app/_app/immutable/chunks/CjjyIQAO.js +1 -0
  43. package/dist/app/_app/immutable/chunks/CuXAxjvF.js +1 -0
  44. package/dist/app/_app/immutable/chunks/CvyVA_jC.js +1 -0
  45. package/dist/app/_app/immutable/chunks/CxGCFVdy.js +1 -0
  46. package/dist/app/_app/immutable/chunks/D0Ty6LN0.js +1 -0
  47. package/dist/app/_app/immutable/chunks/D2AaQUUW.js +1 -0
  48. package/dist/app/_app/immutable/chunks/D2BnX0Uk.js +3 -0
  49. package/dist/app/_app/immutable/chunks/DJc8C0NK.js +1 -0
  50. package/dist/app/_app/immutable/chunks/DKMlMI4a.js +1 -0
  51. package/dist/app/_app/immutable/chunks/DVXZkpbf.js +1 -0
  52. package/dist/app/_app/immutable/chunks/DVt8ukQ_.js +1 -0
  53. package/dist/app/_app/immutable/chunks/DZPlYdq_.js +1 -0
  54. package/dist/app/_app/immutable/chunks/Db0q5_zr.js +1 -0
  55. package/dist/app/_app/immutable/chunks/Dfvzj6n2.js +1 -0
  56. package/dist/app/_app/immutable/chunks/Dh958be7.js +1 -0
  57. package/dist/app/_app/immutable/chunks/DjKLLdnY.js +15 -0
  58. package/dist/app/_app/immutable/chunks/Doz7YX1W.js +1 -0
  59. package/dist/app/_app/immutable/chunks/DthYhn6Y.js +2 -0
  60. package/dist/app/_app/immutable/chunks/DtuTIrAM.js +1 -0
  61. package/dist/app/_app/immutable/chunks/HclGiUj8.js +1 -0
  62. package/dist/app/_app/immutable/chunks/Hx0TNsV3.js +1 -0
  63. package/dist/app/_app/immutable/chunks/RobXhXPM.js +1 -0
  64. package/dist/app/_app/immutable/chunks/V9ZjaxiY.js +1 -0
  65. package/dist/app/_app/immutable/chunks/Y5urAfNy.js +1 -0
  66. package/dist/app/_app/immutable/chunks/caXkbKD3.js +1 -0
  67. package/dist/app/_app/immutable/chunks/devYm2ud.js +1 -0
  68. package/dist/app/_app/immutable/chunks/mtZWP0zR.js +1 -0
  69. package/dist/app/_app/immutable/chunks/vDgBJUjM.js +1 -0
  70. package/dist/app/_app/immutable/chunks/xIq_fFFM.js +1 -0
  71. package/dist/app/_app/immutable/chunks/xihTtKlq.js +1 -0
  72. package/dist/app/_app/immutable/chunks/z05MoCFz.js +1 -0
  73. package/dist/app/_app/immutable/entry/app.CLAerUAN.js +2 -0
  74. package/dist/app/_app/immutable/entry/start.D3MqnNci.js +1 -0
  75. package/dist/app/_app/immutable/nodes/0.UTMEigHJ.js +1 -0
  76. package/dist/app/_app/immutable/nodes/1.Cn4f11bT.js +1 -0
  77. package/dist/app/_app/immutable/nodes/2.B39cIcr2.js +6 -0
  78. package/dist/app/_app/version.json +1 -0
  79. package/dist/app/index.html +82 -0
  80. package/dist/aws/clients.d.ts +70 -0
  81. package/dist/aws/clients.js +52 -0
  82. package/dist/aws/errors.d.ts +41 -0
  83. package/dist/aws/errors.js +70 -0
  84. package/dist/aws/firehose.d.ts +228 -0
  85. package/dist/aws/firehose.js +347 -0
  86. package/dist/aws/glue.d.ts +103 -0
  87. package/dist/aws/glue.js +225 -0
  88. package/dist/aws/lambda.d.ts +132 -0
  89. package/dist/aws/lambda.js +339 -0
  90. package/dist/aws/s3tables.d.ts +120 -0
  91. package/dist/aws/s3tables.js +281 -0
  92. package/dist/backfill.d.ts +100 -0
  93. package/dist/backfill.js +294 -0
  94. package/dist/commands.d.ts +124 -0
  95. package/dist/commands.js +336 -0
  96. package/dist/config.d.ts +162 -0
  97. package/dist/config.js +317 -0
  98. package/dist/fixture-ingest.d.ts +49 -0
  99. package/dist/fixture-ingest.js +43 -0
  100. package/dist/fixture-query.d.ts +39 -0
  101. package/dist/fixture-query.js +70 -0
  102. package/dist/index.d.ts +35 -0
  103. package/dist/index.js +35 -0
  104. package/dist/nodes.d.ts +404 -0
  105. package/dist/nodes.js +2708 -0
  106. package/dist/paths.d.ts +45 -0
  107. package/dist/paths.js +47 -0
  108. package/dist/plugin.d.ts +102 -0
  109. package/dist/plugin.js +248 -0
  110. package/dist/ports.d.ts +113 -0
  111. package/dist/ports.js +35 -0
  112. package/dist/queries.d.ts +301 -0
  113. package/dist/queries.js +414 -0
  114. package/dist/schema.d.ts +240 -0
  115. package/dist/schema.js +154 -0
  116. package/dist/server.d.ts +150 -0
  117. package/dist/server.js +499 -0
  118. package/dist/transform/bots.d.ts +47 -0
  119. package/dist/transform/bots.js +73 -0
  120. package/dist/transform/handler.d.ts +135 -0
  121. package/dist/transform/handler.js +177 -0
  122. package/dist/transform/map-record.d.ts +110 -0
  123. package/dist/transform/map-record.js +275 -0
  124. package/dist/transform/visitor-key.d.ts +83 -0
  125. package/dist/transform/visitor-key.js +120 -0
  126. package/dist/transform-bundle/index.mjs +21456 -0
  127. package/dist/transform-bundle/transform-manifest.json +4 -0
  128. package/dist/transform-hash.d.ts +135 -0
  129. package/dist/transform-hash.js +186 -0
  130. package/dist/write-transform-manifest.mjs +365 -0
  131. package/package.json +59 -0
@@ -0,0 +1,4 @@
1
+ {
2
+ "hash": "063144a446cd",
3
+ "key": "analytics/transform/transform-063144a446cd.zip"
4
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * The transform Lambda's build identity: a reproducible hash of the *source*
3
+ * the function is bundled from, the artifact names that hash is stamped
4
+ * beside, and the one derivation of the zip key that hash produces.
5
+ *
6
+ * This is step 5 of
7
+ * [§Implementation notes](../../../.specs/changes/merged/2026-07-26-analytics_plugin.md) -
8
+ * "The hash keys the uploaded zip so identical source never redeploys the
9
+ * function" - and it exists as its own module so `nodes.ts` (task 50) consumes
10
+ * a derivation rather than restating a key format. A key spelled twice is a key
11
+ * that can disagree with itself, and the disagreement is invisible: the
12
+ * function silently redeploys on every reconcile, or worse, never redeploys
13
+ * after a real source change.
14
+ *
15
+ * ## Why the *source*, never the bundle
16
+ *
17
+ * `DEVELOPMENT.md` §Repository hygiene states the rule this module obeys: "The
18
+ * build-agent manifest hashes the agent's *source*, not the built bundle, so
19
+ * image keys do not vary by platform." Bundler output varies with the
20
+ * toolchain and the host (macOS laptop vs the Linux CI runner); source bytes do
21
+ * not. Hashing the bundle would key the same code under two different zip keys
22
+ * depending on who ran `pnpm build`, redeploying the function on every
23
+ * platform switch for no change at all. So the bundle path appears nowhere in
24
+ * the input list below - which is also why `packages/analytics/src/aws/lambda.ts`
25
+ * (task 36) deliberately does not surface `CodeSha256`: that digests the zip
26
+ * Lambda holds, and comparing it to this hash would compare two different
27
+ * things.
28
+ *
29
+ * ## What is in the hash, and why each input is there
30
+ *
31
+ * The shape follows `agentSourceHash` (`packages/build-agent/src/agent-hash.ts`)
32
+ * exactly: collect, label, sort by label with a codepoint comparison, and
33
+ * digest each input as `label` NUL `bytes` NUL. The framing matters - without
34
+ * the labels a file rename would leave the hash unchanged, and without the NUL
35
+ * separators two adjacent inputs could be split differently and collide.
36
+ *
37
+ * The inputs are a deliberate superset of what rolldown actually tree-shakes
38
+ * into the bundle: an unrelated change to this package or to core can force a
39
+ * (harmless) redeploy, which is the right trade against ever shipping a stale
40
+ * transform. `core/src` is in because the bundle genuinely inlines it - the
41
+ * entry constructs core's `SigningClient` and `SecretsManagerClient` - and the
42
+ * lockfile, `tsconfig.json` and the rolldown config are in because each of them
43
+ * changes the emitted bundle without changing a single line of this package's
44
+ * own source. The rolldown config lives at `src/transform/rolldown.config.ts`,
45
+ * so the `analytics/src` collection below already carries it; it is not listed
46
+ * a second time, because hashing the same bytes under two labels adds nothing.
47
+ * `transform-hash.test.ts` proves each of those inputs is live by changing one
48
+ * byte of it and asserting the hash moves.
49
+ *
50
+ * Test files are excluded (the `.test.ts` filter, as in `agentSourceHash`):
51
+ * they are never bundled, so a test-only edit must not redeploy the function.
52
+ *
53
+ * ## No direct filesystem call here
54
+ *
55
+ * Reading crosses the {@link FileSystem} port rather than Node's `fs` module,
56
+ * so this stays a domain module under DEVELOPMENT.md §Hexagonal architecture
57
+ * and no `packages/analytics/src/` path has to join the `no-restricted-imports`
58
+ * override list in `.oxlintrc.json`. The real adapter is constructed in
59
+ * `transform/write-manifest.ts`, the build-time edge that runs this.
60
+ */
61
+ import type { FileSystem } from 'blogwright-core';
62
+ /**
63
+ * Where the build puts the Lambda artifacts, relative to the package root.
64
+ * Its own directory, not `dist/transform/` (which `tsc` fills with the
65
+ * unbundled modules), so the bundle can never be overwritten by the compiler.
66
+ *
67
+ * It holds two files, and they are not both deployment artifacts:
68
+ * {@link TRANSFORM_BUNDLE_FILE} is the zip's single entry, and
69
+ * {@link TRANSFORM_MANIFEST_FILE} is read *beside* the zip at deploy time for
70
+ * the hash and the key it derives. Task 50 zips the bundle file alone - see
71
+ * {@link TRANSFORM_BUNDLE_FILE} for why a one-file zip is the shape the
72
+ * `Handler` string assumes - and never the directory wholesale.
73
+ */
74
+ export declare const TRANSFORM_BUNDLE_DIR = "dist/transform-bundle";
75
+ /**
76
+ * The bundle's file name inside {@link TRANSFORM_BUNDLE_DIR} - and, unchanged,
77
+ * inside the zip task 50 uploads.
78
+ *
79
+ * `.mjs`, not `.js`: the bundle is ESM, and the Lambda Node runtime reads a
80
+ * `.js` file in the deployment package as CommonJS unless the zip also carries
81
+ * a `package.json` declaring `"type": "module"`. A one-file zip with a `.mjs`
82
+ * extension needs no such companion, so there is no second file to forget - and
83
+ * this file, alone, is what that zip contains.
84
+ *
85
+ * That the emitted bundle really is ESM exporting this module's binding is
86
+ * asserted by `transform/write-manifest.ts` on every build, because no test
87
+ * sees the emitted file.
88
+ */
89
+ export declare const TRANSFORM_BUNDLE_FILE = "index.mjs";
90
+ /**
91
+ * The build-time manifest carrying the hash and the key it derives, written
92
+ * beside the bundle in {@link TRANSFORM_BUNDLE_DIR} and read beside the zip -
93
+ * never packed inside it.
94
+ */
95
+ export declare const TRANSFORM_MANIFEST_FILE = "transform-manifest.json";
96
+ /**
97
+ * The `Handler` string the function is configured with (task 50), spelled here
98
+ * because it is derived from this module's artifact names and nothing else:
99
+ * the bundle's base name, then the binding `transform/entry.ts` exports.
100
+ *
101
+ * It is here rather than in `nodes.ts` because getting it wrong fails at
102
+ * *invoke* time with an AWS-side error and no build error - every record would
103
+ * land in the Firehose error prefix with nothing in this repo reporting it, and
104
+ * the only symptom is an empty dashboard. Two checks pin this constant against
105
+ * a real export rather than a comment: `transform/entry.test.ts` against the
106
+ * entry *module*'s, and `transform/write-manifest.ts` against the emitted
107
+ * *bundle*'s, on every build. A rename or a bundler-config change reddens one
108
+ * of them instead of silently emptying the warehouse.
109
+ */
110
+ export declare const TRANSFORM_LAMBDA_HANDLER = "index.handler";
111
+ /**
112
+ * A hash of the transform's *source*, computed at bundle time and stamped into
113
+ * `dist/transform-bundle/transform-manifest.json` (see
114
+ * `transform/write-manifest.ts`) so the plugin reads it at runtime without any
115
+ * access to the source tree.
116
+ *
117
+ * `dir` is the analytics package root; core and the workspace root are located
118
+ * from it the way `agentSourceHash` locates its own siblings. Only labels,
119
+ * never absolute paths, reach the digest, so the same tree checked out at two
120
+ * different paths hashes identically - a property `transform-hash.test.ts`
121
+ * asserts, because a hash that moves with the checkout directory would key the
122
+ * same code under a different zip on every machine.
123
+ */
124
+ export declare function transformSourceHash(dir: string, fs: FileSystem): Promise<string>;
125
+ /**
126
+ * The key the bundled transform's zip is stored and compared under, derived
127
+ * from {@link transformSourceHash} and spelled in this one place. Task 50's
128
+ * `analytics-transform-function` records it and skips the code update when it
129
+ * has not moved, so identical source provably maps to an identical key.
130
+ *
131
+ * A malformed hash raises rather than producing a key: `transform-undefined.zip`
132
+ * is a key that compares equal to itself forever, which would pin the deployed
133
+ * function at whatever code first shipped and never update it again.
134
+ */
135
+ export declare function transformZipKey(hash: string): string;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * The transform Lambda's build identity: a reproducible hash of the *source*
3
+ * the function is bundled from, the artifact names that hash is stamped
4
+ * beside, and the one derivation of the zip key that hash produces.
5
+ *
6
+ * This is step 5 of
7
+ * [§Implementation notes](../../../.specs/changes/merged/2026-07-26-analytics_plugin.md) -
8
+ * "The hash keys the uploaded zip so identical source never redeploys the
9
+ * function" - and it exists as its own module so `nodes.ts` (task 50) consumes
10
+ * a derivation rather than restating a key format. A key spelled twice is a key
11
+ * that can disagree with itself, and the disagreement is invisible: the
12
+ * function silently redeploys on every reconcile, or worse, never redeploys
13
+ * after a real source change.
14
+ *
15
+ * ## Why the *source*, never the bundle
16
+ *
17
+ * `DEVELOPMENT.md` §Repository hygiene states the rule this module obeys: "The
18
+ * build-agent manifest hashes the agent's *source*, not the built bundle, so
19
+ * image keys do not vary by platform." Bundler output varies with the
20
+ * toolchain and the host (macOS laptop vs the Linux CI runner); source bytes do
21
+ * not. Hashing the bundle would key the same code under two different zip keys
22
+ * depending on who ran `pnpm build`, redeploying the function on every
23
+ * platform switch for no change at all. So the bundle path appears nowhere in
24
+ * the input list below - which is also why `packages/analytics/src/aws/lambda.ts`
25
+ * (task 36) deliberately does not surface `CodeSha256`: that digests the zip
26
+ * Lambda holds, and comparing it to this hash would compare two different
27
+ * things.
28
+ *
29
+ * ## What is in the hash, and why each input is there
30
+ *
31
+ * The shape follows `agentSourceHash` (`packages/build-agent/src/agent-hash.ts`)
32
+ * exactly: collect, label, sort by label with a codepoint comparison, and
33
+ * digest each input as `label` NUL `bytes` NUL. The framing matters - without
34
+ * the labels a file rename would leave the hash unchanged, and without the NUL
35
+ * separators two adjacent inputs could be split differently and collide.
36
+ *
37
+ * The inputs are a deliberate superset of what rolldown actually tree-shakes
38
+ * into the bundle: an unrelated change to this package or to core can force a
39
+ * (harmless) redeploy, which is the right trade against ever shipping a stale
40
+ * transform. `core/src` is in because the bundle genuinely inlines it - the
41
+ * entry constructs core's `SigningClient` and `SecretsManagerClient` - and the
42
+ * lockfile, `tsconfig.json` and the rolldown config are in because each of them
43
+ * changes the emitted bundle without changing a single line of this package's
44
+ * own source. The rolldown config lives at `src/transform/rolldown.config.ts`,
45
+ * so the `analytics/src` collection below already carries it; it is not listed
46
+ * a second time, because hashing the same bytes under two labels adds nothing.
47
+ * `transform-hash.test.ts` proves each of those inputs is live by changing one
48
+ * byte of it and asserting the hash moves.
49
+ *
50
+ * Test files are excluded (the `.test.ts` filter, as in `agentSourceHash`):
51
+ * they are never bundled, so a test-only edit must not redeploy the function.
52
+ *
53
+ * ## No direct filesystem call here
54
+ *
55
+ * Reading crosses the {@link FileSystem} port rather than Node's `fs` module,
56
+ * so this stays a domain module under DEVELOPMENT.md §Hexagonal architecture
57
+ * and no `packages/analytics/src/` path has to join the `no-restricted-imports`
58
+ * override list in `.oxlintrc.json`. The real adapter is constructed in
59
+ * `transform/write-manifest.ts`, the build-time edge that runs this.
60
+ */
61
+ import { createHash } from 'node:crypto';
62
+ import { join, sep } from 'node:path';
63
+ /**
64
+ * Hex characters kept from the SHA-256 digest. Twelve, as `agentSourceHash`
65
+ * slices to: enough that a collision between two revisions of one small source
66
+ * tree is not a practical concern, short enough to read in a resource name.
67
+ */
68
+ const HASH_LENGTH = 12;
69
+ /** A well-formed {@link transformSourceHash} result. */
70
+ const HASH_PATTERN = new RegExp(`^[0-9a-f]{${HASH_LENGTH}}$`);
71
+ /**
72
+ * Where the build puts the Lambda artifacts, relative to the package root.
73
+ * Its own directory, not `dist/transform/` (which `tsc` fills with the
74
+ * unbundled modules), so the bundle can never be overwritten by the compiler.
75
+ *
76
+ * It holds two files, and they are not both deployment artifacts:
77
+ * {@link TRANSFORM_BUNDLE_FILE} is the zip's single entry, and
78
+ * {@link TRANSFORM_MANIFEST_FILE} is read *beside* the zip at deploy time for
79
+ * the hash and the key it derives. Task 50 zips the bundle file alone - see
80
+ * {@link TRANSFORM_BUNDLE_FILE} for why a one-file zip is the shape the
81
+ * `Handler` string assumes - and never the directory wholesale.
82
+ */
83
+ export const TRANSFORM_BUNDLE_DIR = 'dist/transform-bundle';
84
+ /**
85
+ * The bundle's file name inside {@link TRANSFORM_BUNDLE_DIR} - and, unchanged,
86
+ * inside the zip task 50 uploads.
87
+ *
88
+ * `.mjs`, not `.js`: the bundle is ESM, and the Lambda Node runtime reads a
89
+ * `.js` file in the deployment package as CommonJS unless the zip also carries
90
+ * a `package.json` declaring `"type": "module"`. A one-file zip with a `.mjs`
91
+ * extension needs no such companion, so there is no second file to forget - and
92
+ * this file, alone, is what that zip contains.
93
+ *
94
+ * That the emitted bundle really is ESM exporting this module's binding is
95
+ * asserted by `transform/write-manifest.ts` on every build, because no test
96
+ * sees the emitted file.
97
+ */
98
+ export const TRANSFORM_BUNDLE_FILE = 'index.mjs';
99
+ /**
100
+ * The build-time manifest carrying the hash and the key it derives, written
101
+ * beside the bundle in {@link TRANSFORM_BUNDLE_DIR} and read beside the zip -
102
+ * never packed inside it.
103
+ */
104
+ export const TRANSFORM_MANIFEST_FILE = 'transform-manifest.json';
105
+ /**
106
+ * The `Handler` string the function is configured with (task 50), spelled here
107
+ * because it is derived from this module's artifact names and nothing else:
108
+ * the bundle's base name, then the binding `transform/entry.ts` exports.
109
+ *
110
+ * It is here rather than in `nodes.ts` because getting it wrong fails at
111
+ * *invoke* time with an AWS-side error and no build error - every record would
112
+ * land in the Firehose error prefix with nothing in this repo reporting it, and
113
+ * the only symptom is an empty dashboard. Two checks pin this constant against
114
+ * a real export rather than a comment: `transform/entry.test.ts` against the
115
+ * entry *module*'s, and `transform/write-manifest.ts` against the emitted
116
+ * *bundle*'s, on every build. A rename or a bundler-config change reddens one
117
+ * of them instead of silently emptying the warehouse.
118
+ */
119
+ export const TRANSFORM_LAMBDA_HANDLER = 'index.handler';
120
+ /**
121
+ * Every non-test file under `root`, labelled by its path under `prefix`.
122
+ *
123
+ * `listFiles` is contracted to return sorted, `root`-relative paths, so the
124
+ * collection order does not depend on the host filesystem's readdir order;
125
+ * separators are normalised to `/` so a label is the same on every platform.
126
+ */
127
+ async function collectSource(fs, root, prefix) {
128
+ const files = await fs.listFiles(root);
129
+ return files
130
+ .filter((relativePath) => !relativePath.endsWith('.test.ts'))
131
+ .map((relativePath) => ({
132
+ label: `${prefix}/${relativePath.split(sep).join('/')}`,
133
+ path: join(root, relativePath),
134
+ }));
135
+ }
136
+ /**
137
+ * A hash of the transform's *source*, computed at bundle time and stamped into
138
+ * `dist/transform-bundle/transform-manifest.json` (see
139
+ * `transform/write-manifest.ts`) so the plugin reads it at runtime without any
140
+ * access to the source tree.
141
+ *
142
+ * `dir` is the analytics package root; core and the workspace root are located
143
+ * from it the way `agentSourceHash` locates its own siblings. Only labels,
144
+ * never absolute paths, reach the digest, so the same tree checked out at two
145
+ * different paths hashes identically - a property `transform-hash.test.ts`
146
+ * asserts, because a hash that moves with the checkout directory would key the
147
+ * same code under a different zip on every machine.
148
+ */
149
+ export async function transformSourceHash(dir, fs) {
150
+ const coreDir = join(dir, '..', 'core');
151
+ const rootDir = join(dir, '..', '..');
152
+ const inputs = [
153
+ ...(await collectSource(fs, join(dir, 'src'), 'analytics/src')),
154
+ ...(await collectSource(fs, join(coreDir, 'src'), 'core/src')),
155
+ { label: 'analytics/package.json', path: join(dir, 'package.json') },
156
+ { label: 'analytics/tsconfig.json', path: join(dir, 'tsconfig.json') },
157
+ { label: 'core/package.json', path: join(coreDir, 'package.json') },
158
+ { label: 'workspace/tsconfig.base.json', path: join(rootDir, 'tsconfig.base.json') },
159
+ { label: 'workspace/pnpm-lock.yaml', path: join(rootDir, 'pnpm-lock.yaml') },
160
+ // Codepoint sort, not localeCompare: collation must not depend on host locale/ICU.
161
+ ].sort((a, b) => (a.label < b.label ? -1 : 1));
162
+ const digest = createHash('sha256');
163
+ for (const { label, path } of inputs) {
164
+ digest.update(label);
165
+ digest.update('\0');
166
+ digest.update(await fs.readBytes(path));
167
+ digest.update('\0');
168
+ }
169
+ return digest.digest('hex').slice(0, HASH_LENGTH);
170
+ }
171
+ /**
172
+ * The key the bundled transform's zip is stored and compared under, derived
173
+ * from {@link transformSourceHash} and spelled in this one place. Task 50's
174
+ * `analytics-transform-function` records it and skips the code update when it
175
+ * has not moved, so identical source provably maps to an identical key.
176
+ *
177
+ * A malformed hash raises rather than producing a key: `transform-undefined.zip`
178
+ * is a key that compares equal to itself forever, which would pin the deployed
179
+ * function at whatever code first shipped and never update it again.
180
+ */
181
+ export function transformZipKey(hash) {
182
+ if (!HASH_PATTERN.test(hash)) {
183
+ throw new Error(`the analytics transform's source hash must be ${HASH_LENGTH} lowercase hex characters, not "${hash}" - rebuild the package so ${TRANSFORM_MANIFEST_FILE} is regenerated`);
184
+ }
185
+ return `analytics/transform/transform-${hash}.zip`;
186
+ }