@mailwoman/soil 10.0.0 → 10.1.0

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 (116) hide show
  1. package/README.md +118 -116
  2. package/lib/index.ts +66 -98
  3. package/lib/paths.ts +24 -0
  4. package/lib/schema.ts +188 -120
  5. package/lib/vocabulary.ts +40 -107
  6. package/out/index.d.ts +52 -70
  7. package/out/index.d.ts.map +1 -1
  8. package/out/index.js +24 -72
  9. package/out/index.js.map +1 -1
  10. package/out/paths.d.ts +19 -0
  11. package/out/paths.d.ts.map +1 -0
  12. package/out/paths.js +21 -0
  13. package/out/paths.js.map +1 -0
  14. package/out/schema.d.ts +187 -119
  15. package/out/schema.d.ts.map +1 -1
  16. package/out/schema.js +31 -31
  17. package/out/schema.js.map +1 -1
  18. package/out/sdk/acquire.d.ts +15 -24
  19. package/out/sdk/acquire.d.ts.map +1 -1
  20. package/out/sdk/acquire.js +6 -21
  21. package/out/sdk/acquire.js.map +1 -1
  22. package/out/sdk/build-soil.d.ts +67 -70
  23. package/out/sdk/build-soil.d.ts.map +1 -1
  24. package/out/sdk/build-soil.js +64 -94
  25. package/out/sdk/build-soil.js.map +1 -1
  26. package/out/sdk/cell-tiers.d.ts +7 -19
  27. package/out/sdk/cell-tiers.d.ts.map +1 -1
  28. package/out/sdk/cell-tiers.js +19 -38
  29. package/out/sdk/cell-tiers.js.map +1 -1
  30. package/out/sdk/cells.d.ts +15 -41
  31. package/out/sdk/cells.d.ts.map +1 -1
  32. package/out/sdk/cells.js +11 -38
  33. package/out/sdk/cells.js.map +1 -1
  34. package/out/sdk/client.d.ts +23 -48
  35. package/out/sdk/client.d.ts.map +1 -1
  36. package/out/sdk/client.js +19 -63
  37. package/out/sdk/client.js.map +1 -1
  38. package/out/sdk/download.d.ts +24 -47
  39. package/out/sdk/download.d.ts.map +1 -1
  40. package/out/sdk/download.js +14 -54
  41. package/out/sdk/download.js.map +1 -1
  42. package/out/sdk/ingest/chunk.d.ts +29 -25
  43. package/out/sdk/ingest/chunk.d.ts.map +1 -1
  44. package/out/sdk/ingest/chunk.js +18 -16
  45. package/out/sdk/ingest/chunk.js.map +1 -1
  46. package/out/sdk/ingest/worker.d.ts +9 -0
  47. package/out/sdk/ingest/worker.d.ts.map +1 -0
  48. package/out/{scripts/ingest-chunk.js → sdk/ingest/worker.js} +11 -10
  49. package/out/sdk/ingest/worker.js.map +1 -0
  50. package/out/sdk/ingest.d.ts +120 -0
  51. package/out/sdk/ingest.d.ts.map +1 -0
  52. package/out/sdk/ingest.js +127 -0
  53. package/out/sdk/ingest.js.map +1 -0
  54. package/out/sdk/measure-resolutions.d.ts +6 -17
  55. package/out/sdk/measure-resolutions.d.ts.map +1 -1
  56. package/out/sdk/measure-resolutions.js +4 -16
  57. package/out/sdk/measure-resolutions.js.map +1 -1
  58. package/out/sdk/reduce.d.ts +33 -59
  59. package/out/sdk/reduce.d.ts.map +1 -1
  60. package/out/sdk/reduce.js +47 -78
  61. package/out/sdk/reduce.js.map +1 -1
  62. package/out/sdk/survey-area.d.ts +16 -48
  63. package/out/sdk/survey-area.d.ts.map +1 -1
  64. package/out/sdk/survey-area.js +33 -77
  65. package/out/sdk/survey-area.js.map +1 -1
  66. package/out/sdk/tabular.d.ts +32 -31
  67. package/out/sdk/tabular.d.ts.map +1 -1
  68. package/out/sdk/tabular.js +57 -49
  69. package/out/sdk/tabular.js.map +1 -1
  70. package/out/sdk/test-kit.d.ts +57 -0
  71. package/out/sdk/test-kit.d.ts.map +1 -0
  72. package/out/{test-kit.js → sdk/test-kit.js} +18 -39
  73. package/out/sdk/test-kit.js.map +1 -0
  74. package/out/sdk/verify.d.ts +15 -51
  75. package/out/sdk/verify.d.ts.map +1 -1
  76. package/out/sdk/verify.js +19 -77
  77. package/out/sdk/verify.js.map +1 -1
  78. package/out/vocabulary.d.ts +37 -103
  79. package/out/vocabulary.d.ts.map +1 -1
  80. package/out/vocabulary.js +34 -107
  81. package/out/vocabulary.js.map +1 -1
  82. package/package.json +36 -190
  83. package/{lib/sdk → sdk}/acquire.ts +18 -28
  84. package/{lib/sdk → sdk}/build-soil.ts +113 -127
  85. package/{lib/sdk → sdk}/cell-tiers.ts +20 -39
  86. package/{lib/sdk → sdk}/cells.ts +17 -43
  87. package/sdk/client.ts +147 -0
  88. package/sdk/download.ts +131 -0
  89. package/{lib/sdk → sdk}/ingest/chunk.ts +35 -27
  90. package/{lib/scripts/ingest-chunk.ts → sdk/ingest/worker.ts} +10 -9
  91. package/sdk/ingest.ts +253 -0
  92. package/{lib/sdk → sdk}/measure-resolutions.ts +6 -17
  93. package/sdk/reduce.ts +344 -0
  94. package/{lib/sdk → sdk}/survey-area.ts +39 -83
  95. package/{lib/sdk → sdk}/tabular.ts +61 -52
  96. package/{lib → sdk}/test-kit.ts +19 -41
  97. package/{lib/sdk → sdk}/verify.ts +29 -85
  98. package/lib/sdk/client.ts +0 -184
  99. package/lib/sdk/download.ts +0 -161
  100. package/lib/sdk/index.ts +0 -20
  101. package/lib/sdk/ingest/index.ts +0 -278
  102. package/lib/sdk/reduce.ts +0 -375
  103. package/out/scripts/ingest-chunk.d.ts +0 -11
  104. package/out/scripts/ingest-chunk.d.ts.map +0 -1
  105. package/out/scripts/ingest-chunk.js.map +0 -1
  106. package/out/sdk/index.d.ts +0 -20
  107. package/out/sdk/index.d.ts.map +0 -1
  108. package/out/sdk/index.js +0 -20
  109. package/out/sdk/index.js.map +0 -1
  110. package/out/sdk/ingest/index.d.ts +0 -132
  111. package/out/sdk/ingest/index.d.ts.map +0 -1
  112. package/out/sdk/ingest/index.js +0 -170
  113. package/out/sdk/ingest/index.js.map +0 -1
  114. package/out/test-kit.d.ts +0 -79
  115. package/out/test-kit.d.ts.map +0 -1
  116. package/out/test-kit.js.map +0 -1
package/lib/index.ts CHANGED
@@ -3,43 +3,10 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * The `soil.db` reader — what the soil survey assigns at a coordinate, and on what basis.
7
- *
8
- * THREE ANSWERS, AND KEEPING THEM APART IS THE WHOLE JOB.
9
- *
10
- * 1. `designated` — the survey mapped this location and the cell's class distribution is the answer.
11
- * 2. `designated_no_rating` — the survey mapped this location and rated nothing there. A cell that is
12
- * 100% `unrated_share` or `notrateable_share` is `designated`-complete and carries no capability
13
- * reading whatsoever, and that is not a corner case: 17.1% of national components carry no capability
14
- * rating.
15
- * 3. `unknown` — no coverage row. Outside any published survey area, or inside one where the polygon
16
- * exists and the soil mapping behind it does not.
17
- *
18
- * READINGS 2 AND 3 LOOK THE SAME FROM A CLASS CODE AND ARE OPPOSITE ANSWERS FROM THE READER. A layer that
19
- * could not tell them apart would report unmapped ground as unrated ground, which is one of the four
20
- * absences this whole layer exists to keep separate.
21
- *
22
- * THE ANSWER IS A DISTRIBUTION, AND THE TOP CLASS ALWAYS ARRIVES WITH THE SHARE IT RESTS ON. NRCS's own
23
- * `muaggatt` ships `niccdcd` beside `niccdcdpct` for exactly this reason, with an observed minimum of 2%.
24
- * A caller that wants one class may take `topClass`; it cannot take it without also being handed
25
- * `topClassShare`, because a 2% plurality and an 85% majority are different claims.
26
- *
27
- * NEITHER READING IS A STATEMENT ABOUT WHETHER THE LAND CAN BE FARMED. The layer reports what the soil
28
- * survey assigns to the map unit covering a location, which is a fact about the map. NRCS states that its
29
- * data "do not eliminate the need for onsite sampling, testing, and detailed study of specific sites for
30
- * intensive uses" and are "intended for planning purposes only" — so `limits` carries the authority's own
31
- * exclusions on every answer.
32
- *
33
- * THE PROBE IS ONE PRIMARY-KEY READ. The reduction is single-resolution and one row per cell, which is
34
- * what makes it the spine key: a coordinate becomes a cell, the cell becomes a row, and the geometry tier
35
- * underneath is never touched at read time. The unsimplified rings are there for a caller that wants to
36
- * re-derive the claim, not for the probe.
37
- *
38
- * THE READER IS SYNCHRONOUS AND USES RAW PREPARED STATEMENTS, matching the resolution ladder's existing
39
- * shape. The DDL that created these tables IS Kysely — see `schema.ts`.
6
+ * Synchronous reader for `soil.db` that returns the soil survey's capability-class distribution at a coordinate.
40
7
  */
41
8
 
42
- import { parseJSONStrict } from "@mailwoman/core/json"
9
+ import { parseJSONStrict, stringifyJSON } from "@mailwoman/core/json"
43
10
  import {
44
11
  assertCoverageNotEmpty,
45
12
  singleManifestRow,
@@ -51,6 +18,7 @@ import { shortCellToInt, type H3Cell } from "@mailwoman/spatial"
51
18
  import { readCoverageAt } from "@mailwoman/spatial/h3/coverage"
52
19
  import { DatabaseClient } from "@mailwoman/sqlite/client"
53
20
  import { latLngToCell } from "h3-js"
21
+ import type { PathBuilderLike } from "path-ts"
54
22
 
55
23
  import type { SoilDatabase } from "#schema"
56
24
  import { SOIL_LAYER_NAME_PREFIX, SSURGO_PRODUCT_LIMITS } from "#vocabulary"
@@ -58,80 +26,92 @@ import { SOIL_LAYER_NAME_PREFIX, SSURGO_PRODUCT_LIMITS } from "#vocabulary"
58
26
  export { FarmlandScope, farmlandScope, SSURGO_PRODUCT_LIMITS } from "#vocabulary"
59
27
 
60
28
  /**
61
- * What the layer can say about a coordinate.
29
+ * The kinds of answer the layer gives for a coordinate, where `DesignatedNoRating`
30
+ * and `Unknown` must stay apart.
62
31
  */
63
32
  export const SoilReadingKind = {
64
33
  /**
65
- * The survey mapped this location and assigns at least one capability class here.
34
+ * The survey mapped this location and assigns at least one capability class.
66
35
  */
67
36
  Designated: "designated",
68
37
  /**
69
- * The survey mapped this location and rated nothing here — every share is an absence share.
38
+ * The survey mapped this location and assigned no rating.
39
+ * Every share is reported as an absence share.
70
40
  */
71
41
  DesignatedNoRating: "designated_no_rating",
72
42
  /**
73
- * No coverage row. Unmapped by this authority, and never a low-capability reading.
43
+ * The layer has no coverage for this location.
44
+ * This does not imply low capability.
74
45
  */
75
46
  Unknown: "unknown",
76
47
  } as const
77
48
 
49
+ /**
50
+ * One of the {@link SoilReadingKind} values.
51
+ */
78
52
  export type SoilReadingKind = (typeof SoilReadingKind)[keyof typeof SoilReadingKind]
79
53
 
80
54
  /**
81
- * The per-cell distribution, as a caller reads it.
55
+ * The capability-class distribution of one cell.
82
56
  */
83
57
  export interface SoilCapabilityDistribution {
84
58
  /**
85
- * The authority's class codes mapped to their area-weighted share, largest first.
59
+ * The authority's class codes mapped to their area-weighted shares, largest first.
86
60
  */
87
61
  classShares: Record<string, number>
88
62
  /**
89
- * Mapped soil components carrying a NULL rating — the survey did not rate them.
63
+ * The share of mapped soil components that have a NULL rating.
90
64
  */
91
65
  unratedShare: number
92
66
  /**
93
- * Miscellaneous areas the rating does not apply to.
67
+ * The share of miscellaneous areas that the rating does not apply to.
94
68
  */
95
69
  notRateableShare: number
96
70
  /**
97
- * Polygons with no soil mapping behind them.
71
+ * The share of polygons that have no soil mapping.
98
72
  */
99
73
  noDataShare: number
100
74
  /**
101
- * The truncated minority tail. The five shares sum to 1.
75
+ * The share of the truncated minority classes.
76
+ * The class shares and the four other shares sum to 1.
102
77
  */
103
78
  otherShare: number
104
79
  /**
105
- * How much of the cell any delineation covers. Below 1 at a survey-area edge.
80
+ * The fraction of the cell covered by any delineation.
81
+ * It falls below 1 at a survey-area edge.
106
82
  */
107
83
  mappedShare: number
108
84
  /**
109
- * The largest class share, and the share it rests on. Absent when the cell carries no class at all.
85
+ * The class with the largest share, absent when the cell has no class.
86
+ *
87
+ * A caller that reads `topClass` should also report `topClassShare`,
88
+ * because the top class can hold a small plurality.
110
89
  */
111
90
  topClass?: string
112
91
  topClassShare?: number
113
92
  /**
114
- * Which weighting produced these shares.
93
+ * The weighting that produced these shares.
115
94
  */
116
95
  weighting: string
117
96
  /**
118
- * How many delineations reached the cell.
97
+ * The number of delineations that reached the cell.
119
98
  */
120
99
  delineations: number
121
100
  }
122
101
 
123
102
  /**
124
- * One survey area, as the layer holds it.
103
+ * One survey area in the layer.
125
104
  */
126
105
  export interface SoilSurveyAreaRecord {
127
106
  areaSymbol: string
128
107
  areaName: string
129
108
  /**
130
- * The refresh — when this version of the data was established.
109
+ * The date on which this version of the survey data was established.
131
110
  */
132
111
  saverest: string
133
112
  /**
134
- * The FIELD survey date, which is a different fact and is usually much older.
113
+ * The field survey date.
114
+ * It can be much older than `saverest`.
135
115
  */
136
116
  surveySourceDate: string | null
137
117
  surveySourceTitle: string | null
@@ -140,69 +120,71 @@ export interface SoilSurveyAreaRecord {
140
120
  }
141
121
 
142
122
  /**
143
- * One reading, carrying everything a caller needs to re-derive it rather than take it.
123
+ * One reading at a coordinate, with the provenance a caller needs to check it.
144
124
  */
145
125
  export interface SoilCapabilityReading {
146
126
  kind: SoilReadingKind
147
127
  /**
148
- * The cell's distribution. Present on both designated readings; absent on `unknown`.
128
+ * The cell's distribution, present on both designated kinds and absent on `unknown`.
149
129
  */
150
130
  distribution?: SoilCapabilityDistribution
151
131
  /**
152
- * The authority's own definition of the top class, from the domain it shipped.
132
+ * The authority's definition of the top class, from its vocabulary.
153
133
  */
154
134
  topClassDefinition?: string
155
135
  /**
156
- * The survey area covering the location, with both its dates.
136
+ * The survey area that covers the location.
157
137
  */
158
138
  surveyArea?: SoilSurveyAreaRecord
159
139
  /**
160
- * The coverage row that licenses the reading, when there is one. Absent on `unknown`, which IS the absence.
140
+ * The coverage row that supports the reading, when one exists.
161
141
  */
162
142
  coverage?: CoverageCell & { h3CellIndex: string; resolution: number }
163
143
  /**
164
- * The index cell probed, for a receipt.
144
+ * The H3 index cell that the lookup probed.
165
145
  */
166
146
  indexCellIndex: string
167
147
  /**
168
- * What the product does not cover, in the authority's own words. Carried on every reading.
148
+ * The authority's own statements of what the product does not cover.
149
+ *
150
+ * Every reading includes them because the survey supports planning only
151
+ * and does not replace onsite study.
169
152
  */
170
153
  limits: ReadonlyArray<string>
171
154
  }
172
155
 
173
156
  /**
174
- * The layer's identity, read once at open time.
157
+ * The layer's identity, read once when the database opens.
175
158
  */
176
159
  export interface SoilLayerIdentity {
177
160
  manifest: LayerManifest
178
161
  indexResolution: number
179
162
  coverageResolution: number
180
163
  /**
181
- * The survey areas the layer covers, in symbol order.
164
+ * The survey areas the layer covers, ordered by area symbol.
182
165
  */
183
166
  surveyAreas: SoilSurveyAreaRecord[]
184
167
  /**
185
- * The class codes the layer's own vocabulary declares.
168
+ * The class codes that the layer's vocabulary declares.
186
169
  */
187
170
  classCodes: string[]
188
171
  /**
189
- * The weighting every stored share was produced under, and the sentence that says what it means.
172
+ * The code of the weighting used for every stored share, with its description.
190
173
  */
191
174
  weighting: { code: string; description: string }
192
175
  databasePath: string
193
176
  }
194
177
 
178
+ /**
179
+ * Options for {@link SoilCapabilityLookup}.
180
+ */
195
181
  export interface SoilCapabilityLookupOptions {
196
- databasePath: string
182
+ databasePath: PathBuilderLike
197
183
  }
198
184
 
199
185
  /**
200
- * Read a sealed `soil.db`.
201
- *
202
- * Everything that would make the reader answer a well-formed wrong thing is refused at CONSTRUCTION rather than at
203
- * query time: a manifest naming a different product, a coverage table with no rows, a vocabulary with no classes. Each
204
- * of those would otherwise present as a reader that simply always answers `unknown`, which on a receipt is
205
- * indistinguishable from a region the authority genuinely has not surveyed.
186
+ * Reads a sealed `soil.db`, throwing at construction on a manifest for a different product,
187
+ * an empty coverage table, an empty class vocabulary, or a missing share weighting.
206
188
  */
207
189
  export class SoilCapabilityLookup implements Disposable {
208
190
  readonly identity: SoilLayerIdentity
@@ -240,15 +222,14 @@ export class SoilCapabilityLookup implements Disposable {
240
222
  }
241
223
 
242
224
  /**
243
- * What the soil survey assigns at this coordinate.
225
+ * Returns what the soil survey assigns at this coordinate.
244
226
  */
245
227
  public lookup(latitude: number, longitude: number): SoilCapabilityReading {
246
228
  const indexCell = latLngToCell(latitude, longitude, this.identity.indexResolution) as H3Cell
247
229
  const coverage = this.#readCoverage(indexCell)
248
230
 
249
- // COVERAGE QUALIFIES THE READING, and without it there is nothing to report. Unlike a polygon hit — which is a
250
- // determination at a location and needs no coverage row to be true — every answer this layer gives is a per-cell
251
- // summary, so a summary row without a coverage row would state a determination outside the authority's footprint.
231
+ // Every answer is a per-cell summary, so a cell outside the coverage table
232
+ // is unknown even if a summary row exists.
252
233
  if (!coverage) {
253
234
  return {
254
235
  kind: SoilReadingKind.Unknown,
@@ -273,9 +254,8 @@ export class SoilCapabilityLookup implements Disposable {
273
254
  | undefined
274
255
 
275
256
  if (!row) {
276
- // A coverage row without a summary row means the coverage cell is designated and this finer cell holds nothing —
277
- // the survey-area edge. Unknown rather than "no rating": the authority's statement covers the coverage cell, and
278
- // this location may be outside the delineations it covers.
257
+ // At a survey-area edge the coarser coverage cell is covered and this index cell
258
+ // has no delineation, so the location may be outside the survey.
279
259
  return {
280
260
  kind: SoilReadingKind.Unknown,
281
261
  coverage,
@@ -315,24 +295,16 @@ export class SoilCapabilityLookup implements Disposable {
315
295
  this.#database.destroy()
316
296
  }
317
297
 
318
- /**
319
- * The coverage row for the index cell's parent at the coverage resolution.
320
- */
321
298
  #readCoverage(indexCell: H3Cell): (CoverageCell & { h3CellIndex: string; resolution: number }) | undefined {
322
299
  return readCoverageAt(this.#selectCoverage, indexCell, this.identity.coverageResolution)
323
300
  }
324
301
 
325
302
  /**
326
- * Which survey area a coordinate falls in, by the delineation bounds each area's row carries.
327
- *
328
- * A rectangle rather than the outline, and that is honest about what it is: the answer names WHICH published survey
329
- * the reading came from, and two neighbouring counties' rectangles overlap at their corners. The reading itself does
330
- * not depend on it — the cell row is the answer — so a corner ambiguity costs a label rather than a determination.
303
+ * Returns the first survey area whose bounding rectangle contains the coordinate.
304
+ * overlapping rectangles may give the wrong area near a corner.
331
305
  *
332
- * A LINEAR SCAN, WHICH THE PILOT'S 99 SURVEY AREAS MAKE FREE AND A NATIONAL BUILD WOULD NOT. It returns on the first
333
- * containing rectangle, so the pilot costs a few dozen comparisons per geocode. At the 3,380 survey areas the country
334
- * holds this wants a bounding-box index; it is left as a scan because a structure sized for a set this build does not
335
- * hold would be untested at the size it was built for.
306
+ * This affects only the `surveyArea` label.
307
+ * A spatial index would be needed to avoid the linear scan for thousands of areas.
336
308
  */
337
309
  #surveyAreaAt(latitude: number, longitude: number): SoilSurveyAreaRecord | undefined {
338
310
  for (const [index, bounds] of this.#bounds.entries()) {
@@ -350,12 +322,9 @@ export class SoilCapabilityLookup implements Disposable {
350
322
  }
351
323
  }
352
324
 
353
- /**
354
- * Read and check the layer's identity.
355
- */
356
325
  function readIdentity(
357
326
  database: DatabaseClient<SoilDatabase>,
358
- databasePath: string
327
+ databasePath: PathBuilderLike
359
328
  ): {
360
329
  identity: SoilLayerIdentity
361
330
  definitions: Map<string, string>
@@ -365,15 +334,14 @@ function readIdentity(
365
334
  Record<string, string | number | null>
366
335
  >
367
336
 
368
- // The name's SUFFIX names the region a build covers, so the reader checks the prefix rather than a whole name — which
369
- // is why it asserts its own identity instead of taking `parseManifestRows`: one authority, one product, one rating
370
- // vocabulary per artifact, over whichever survey areas were built.
337
+ // The layer name ends with the region the build covers, so the reader checks only the prefix
338
+ // rather than `parseManifestRows`, which compares whole names.
371
339
  const row = singleManifestRow(manifestRows, `soil reader: ${databasePath}`)
372
340
  const name = String(row.name)
373
341
 
374
342
  if (!name.startsWith(SOIL_LAYER_NAME_PREFIX)) {
375
343
  throw new Error(
376
- `soil reader: ${databasePath} is layer ${JSON.stringify(name)}, which is not a ${JSON.stringify(SOIL_LAYER_NAME_PREFIX)} layer — one authority, one product, one rating vocabulary per artifact`
344
+ `soil reader: ${databasePath} is layer ${stringifyJSON(name)}, which is not a ${stringifyJSON(SOIL_LAYER_NAME_PREFIX)} layer — one authority, one product, one rating vocabulary per artifact`
377
345
  )
378
346
  }
379
347
 
@@ -461,7 +429,7 @@ function readIdentity(
461
429
  })),
462
430
  classCodes: [...definitions.keys()],
463
431
  weighting: { code: weighting.code, description: weighting.definition },
464
- databasePath,
432
+ databasePath: databasePath.toString(),
465
433
  },
466
434
  definitions,
467
435
  bounds: areaRows.map((area) => ({
package/lib/paths.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Where the soil layer database and its download caches live under the data root's `db/` group.
7
+ */
8
+
9
+ import { databaseRootPath, dataRootPath } from "@mailwoman/core/data-root"
10
+ import type { PathBuilder, PathBuilderLike } from "path-ts"
11
+
12
+ /**
13
+ * `<dataRoot>/db/soil`.
14
+ */
15
+ export function soilDatabaseRoot(dataRoot: PathBuilderLike): PathBuilder {
16
+ return databaseRootPath(dataRoot)("soil")
17
+ }
18
+
19
+ /**
20
+ * `$MAILWOMAN_DATA_ROOT/db/soil`, read when a path is requested.
21
+ *
22
+ * @see {@link soilDatabaseRoot} for another data root.
23
+ */
24
+ export const soilDatabasePath: PathBuilder = soilDatabaseRoot(dataRootPath())