@verbatra/sdk 0.6.2 → 0.6.4

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.
package/dist/index.d.cts CHANGED
@@ -527,6 +527,7 @@ type GlossaryProvenance = {
527
527
  readonly path: string;
528
528
  };
529
529
 
530
+ /** Where {@link loadConfig} should obtain the configuration. The three sources are alternatives. */
530
531
  interface LoadConfigOptions {
531
532
  /** Directory to start the search from. Defaults to the current working directory. */
532
533
  readonly cwd?: string;
@@ -972,7 +973,7 @@ interface DiffDeps {
972
973
  declare function diff(input: DiffInput, deps?: DiffDeps): Promise<DiffSummary>;
973
974
 
974
975
  /** Why {@link gateCandidateValue} rejected a candidate value. */
975
- type IntegrityGateReason = "placeholder" | "icu" | "degenerate";
976
+ type IntegrityGateReason = "placeholder" | "icu" | "degenerate" | "empty";
976
977
 
977
978
  /** Input for {@link editEntry}: the validated config, exactly one target locale/key pair, and the human-typed replacement value. */
978
979
  interface EditEntryInput {
@@ -1348,7 +1349,7 @@ type RetranslateEntryResult = {
1348
1349
  declare function retranslateEntry(input: RetranslateEntryInput, deps?: RetranslateEntryDeps): Promise<RetranslateEntryResult>;
1349
1350
 
1350
1351
  /** A stable code for an SDK-originated notice (not a provider notice). */
1351
- type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED" | "BLANK_ROW_BASELINE_RETAINED" | "BUDGET_TOKENS_EXCEEDED";
1352
+ type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED" | "BLANK_ROW_BASELINE_RETAINED" | "BUDGET_TOKENS_EXCEEDED" | "CACHE_VERSION_UNRECOGNIZED";
1352
1353
  /** Summed token usage across every provider call in a scope (one locale, or the whole run). */
1353
1354
  interface UsageSummary {
1354
1355
  readonly inputTokens: number;
@@ -1432,7 +1433,27 @@ interface LocaleSummary {
1432
1433
  * {@link LocaleSummary.malformedRows}, and {@link LocaleSummary.duplicateKeys}, deliberately do NOT
1433
1434
  * feed this status: they are surfaced in their own lists (and the CLI) but are not withholdings of
1434
1435
  * an attempted translation, so a locale that dropped a malformed row, collapsed a duplicate key, or
1435
- * left a `changed` row unfilled while accepting its other rows can still be `"succeeded"`.
1436
+ * left a row unfilled while accepting its other rows can still be `"succeeded"`.
1437
+ *
1438
+ * This is a settled decision, not an omission. Recording the reasoning here so the question stops
1439
+ * recurring:
1440
+ *
1441
+ * 1. Two questions, two commands. `import`'s exit code answers "did the import apply what the
1442
+ * translator gave me". `check` and `diff` answer "is this project fully translated", and both
1443
+ * already exit non-zero or report pending in every one of these cases. Folding pending human
1444
+ * work into the import exit code duplicates an existing gate rather than adding one.
1445
+ * 2. It would break the staged handoff. With `de` and `fr` configured and only `de` filled in,
1446
+ * failing on unfilled work makes `fr` fail and takes the whole import with it, even though `de`
1447
+ * imported perfectly. Locale-at-a-time handoff is this feature's most common workflow.
1448
+ * 3. {@link LocaleSummary.malformedRows} structurally cannot signal lost work. The Status column is
1449
+ * the only reachable rejection in the workbook reader; every other field is an unconstrained
1450
+ * string or a tolerant parse. So a row is malformed on its Status cell alone, entirely
1451
+ * independently of whether its Translation cell was filled. An untouched sheet with a mangled
1452
+ * Status column (spreadsheet autocorrect, a CAT-tool round trip) is all-malformed with zero lost
1453
+ * work. A bucket that cannot distinguish dropped work from absent work cannot honestly drive a
1454
+ * failure status.
1455
+ * 4. {@link LocaleSummary.duplicateKeys} never could: the first occurrence wins and the intent is
1456
+ * honoured.
1436
1457
  */
1437
1458
  readonly status: "succeeded" | "partial" | "failed";
1438
1459
  /**
@@ -1513,11 +1534,16 @@ interface LocaleSummary {
1513
1534
  */
1514
1535
  readonly needsReview: readonly NeedsReviewEntry[];
1515
1536
  /**
1516
- * Keys the workbook exported as `changed` (needing an updated translation) that the translator left
1517
- * blank, drifted or not, sorted by key. Pending work made visible: the row is skipped (nothing is
1518
- * written and the prior lock baseline is kept), but the key is surfaced so an unfinished import is
1519
- * never silently reported as done. Only a workbook import populates this; the provider path, which
1520
- * never leaves a candidate key unfilled by a person, always leaves it empty.
1537
+ * Keys the translator left blank that still need a translation, drifted or not, sorted by key.
1538
+ * Pending work made visible: the row is skipped (nothing is written and the prior lock baseline is
1539
+ * kept), but the key is surfaced so an unfinished import is never silently reported as done. Only a
1540
+ * workbook import populates this; the provider path, which never leaves a candidate key unfilled by
1541
+ * a person, always leaves it empty.
1542
+ *
1543
+ * Membership is decided by the import-time diff, not by the `status` the row was exported with: a
1544
+ * never-translated key exports as `new`, and a first handoff is all-new, which is the most common
1545
+ * unfilled case there is. A row exported as `changed` whose key no longer needs work by import time
1546
+ * is correspondingly excluded.
1521
1547
  */
1522
1548
  readonly unfilled: readonly string[];
1523
1549
  /**
@@ -1697,6 +1723,19 @@ interface LocaleFinishedEvent {
1697
1723
  readonly locale: string;
1698
1724
  /** Number of keys accepted (translated) for this locale. In a dry-run, the keys that would be translated. */
1699
1725
  readonly translated: number;
1726
+ /**
1727
+ * Zero-based index of this locale in the run's target-locale order, carrying the same value as the
1728
+ * matching {@link LocaleStartedEvent}. This is a correlation key, so a consumer can pair a finish
1729
+ * with its start without matching on the locale name.
1730
+ *
1731
+ * It is deliberately NOT a progress counter. The worker pool claims indices up front, so this is a
1732
+ * claim ordinal rather than a completion ordinal: under concurrency the locales finish in a
1733
+ * different order, and rendering `localeIndex + 1` of `totalLocales` on a finish produces a
1734
+ * counter that jumps around and never reaches its own total in order. Do not reintroduce one.
1735
+ */
1736
+ readonly localeIndex: number;
1737
+ /** Total number of target locales in this run, mirroring {@link LocaleStartedEvent}. */
1738
+ readonly totalLocales: number;
1700
1739
  }
1701
1740
  /** The run reached the end of its locale loop: how many locales it processed. */
1702
1741
  interface RunFinishedEvent {
@@ -2060,8 +2099,9 @@ interface WatchInput {
2060
2099
  readonly lockAcquireTimeoutMs?: number;
2061
2100
  /**
2062
2101
  * Passed through to every run's {@link TranslateInput.concurrency}: how many locales each cycle may
2063
- * run at once. Defaults to 1 (strictly serial). The per-cycle tracker is fresh, so a budgeted watch
2064
- * with concurrency greater than 1 still fails each run with `CONCURRENCY_BUDGET_CONFLICT`.
2102
+ * run at once. Defaults to 1 (strictly serial). Because the per-cycle budget tracker is fresh, a
2103
+ * value greater than 1 cannot be honored alongside a `maxTokens` budget; {@link watch} refuses that
2104
+ * combination at startup rather than letting every cycle fail on it.
2065
2105
  */
2066
2106
  readonly concurrency?: number;
2067
2107
  /**
@@ -2095,9 +2135,23 @@ interface WatchController {
2095
2135
  * `onRun` as a {@link WatchRunResult} and watching continues. Returns a controller whose `stop()`
2096
2136
  * closes the watcher and awaits the in-flight run.
2097
2137
  *
2138
+ * Both startup refusals are ordered deliberately: `concurrency` is resolved first because it is
2139
+ * decidable from the arguments alone, while the missing-source check is an I/O probe of project
2140
+ * state, so argument validation runs before any I/O and before the watcher exists. The concurrency
2141
+ * resolve passes `dryRun: false` unconditionally, which is exact rather than a simplification: there
2142
+ * is no dry-run watch, so the budget conflict always applies to a session.
2143
+ *
2144
+ * Resolving it here rather than leaving it to each cycle is the point, not an optimization: a watch
2145
+ * session is long-lived, so a per-cycle refusal would fail the initial run and every run after it,
2146
+ * indefinitely, while the session stayed alive and looked healthy.
2147
+ *
2098
2148
  * @param input - The config, optional cwd/debounce, and the `onRun` callback that receives each result.
2099
2149
  * @param deps - Optional composition seams (watcher, run, registry, provider builder, file system) for tests.
2100
2150
  * @returns A {@link WatchController}; call `stop()` to close the watcher and await the in-flight run.
2151
+ * @throws {@link SdkError} `CONCURRENCY_INVALID`: at startup only, when `concurrency` is defined but
2152
+ * not an integer of at least 1.
2153
+ * @throws {@link SdkError} `CONCURRENCY_BUDGET_CONFLICT`: at startup only, when `concurrency` is
2154
+ * greater than 1 while the config sets a `maxTokens` budget.
2101
2155
  * @throws {@link SdkError} `SOURCE_UNREADABLE`: at startup only, when the source locale file is absent.
2102
2156
  * @example
2103
2157
  * ```ts
package/dist/index.d.ts CHANGED
@@ -527,6 +527,7 @@ type GlossaryProvenance = {
527
527
  readonly path: string;
528
528
  };
529
529
 
530
+ /** Where {@link loadConfig} should obtain the configuration. The three sources are alternatives. */
530
531
  interface LoadConfigOptions {
531
532
  /** Directory to start the search from. Defaults to the current working directory. */
532
533
  readonly cwd?: string;
@@ -972,7 +973,7 @@ interface DiffDeps {
972
973
  declare function diff(input: DiffInput, deps?: DiffDeps): Promise<DiffSummary>;
973
974
 
974
975
  /** Why {@link gateCandidateValue} rejected a candidate value. */
975
- type IntegrityGateReason = "placeholder" | "icu" | "degenerate";
976
+ type IntegrityGateReason = "placeholder" | "icu" | "degenerate" | "empty";
976
977
 
977
978
  /** Input for {@link editEntry}: the validated config, exactly one target locale/key pair, and the human-typed replacement value. */
978
979
  interface EditEntryInput {
@@ -1348,7 +1349,7 @@ type RetranslateEntryResult = {
1348
1349
  declare function retranslateEntry(input: RetranslateEntryInput, deps?: RetranslateEntryDeps): Promise<RetranslateEntryResult>;
1349
1350
 
1350
1351
  /** A stable code for an SDK-originated notice (not a provider notice). */
1351
- type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED" | "BLANK_ROW_BASELINE_RETAINED" | "BUDGET_TOKENS_EXCEEDED";
1352
+ type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED" | "BLANK_ROW_BASELINE_RETAINED" | "BUDGET_TOKENS_EXCEEDED" | "CACHE_VERSION_UNRECOGNIZED";
1352
1353
  /** Summed token usage across every provider call in a scope (one locale, or the whole run). */
1353
1354
  interface UsageSummary {
1354
1355
  readonly inputTokens: number;
@@ -1432,7 +1433,27 @@ interface LocaleSummary {
1432
1433
  * {@link LocaleSummary.malformedRows}, and {@link LocaleSummary.duplicateKeys}, deliberately do NOT
1433
1434
  * feed this status: they are surfaced in their own lists (and the CLI) but are not withholdings of
1434
1435
  * an attempted translation, so a locale that dropped a malformed row, collapsed a duplicate key, or
1435
- * left a `changed` row unfilled while accepting its other rows can still be `"succeeded"`.
1436
+ * left a row unfilled while accepting its other rows can still be `"succeeded"`.
1437
+ *
1438
+ * This is a settled decision, not an omission. Recording the reasoning here so the question stops
1439
+ * recurring:
1440
+ *
1441
+ * 1. Two questions, two commands. `import`'s exit code answers "did the import apply what the
1442
+ * translator gave me". `check` and `diff` answer "is this project fully translated", and both
1443
+ * already exit non-zero or report pending in every one of these cases. Folding pending human
1444
+ * work into the import exit code duplicates an existing gate rather than adding one.
1445
+ * 2. It would break the staged handoff. With `de` and `fr` configured and only `de` filled in,
1446
+ * failing on unfilled work makes `fr` fail and takes the whole import with it, even though `de`
1447
+ * imported perfectly. Locale-at-a-time handoff is this feature's most common workflow.
1448
+ * 3. {@link LocaleSummary.malformedRows} structurally cannot signal lost work. The Status column is
1449
+ * the only reachable rejection in the workbook reader; every other field is an unconstrained
1450
+ * string or a tolerant parse. So a row is malformed on its Status cell alone, entirely
1451
+ * independently of whether its Translation cell was filled. An untouched sheet with a mangled
1452
+ * Status column (spreadsheet autocorrect, a CAT-tool round trip) is all-malformed with zero lost
1453
+ * work. A bucket that cannot distinguish dropped work from absent work cannot honestly drive a
1454
+ * failure status.
1455
+ * 4. {@link LocaleSummary.duplicateKeys} never could: the first occurrence wins and the intent is
1456
+ * honoured.
1436
1457
  */
1437
1458
  readonly status: "succeeded" | "partial" | "failed";
1438
1459
  /**
@@ -1513,11 +1534,16 @@ interface LocaleSummary {
1513
1534
  */
1514
1535
  readonly needsReview: readonly NeedsReviewEntry[];
1515
1536
  /**
1516
- * Keys the workbook exported as `changed` (needing an updated translation) that the translator left
1517
- * blank, drifted or not, sorted by key. Pending work made visible: the row is skipped (nothing is
1518
- * written and the prior lock baseline is kept), but the key is surfaced so an unfinished import is
1519
- * never silently reported as done. Only a workbook import populates this; the provider path, which
1520
- * never leaves a candidate key unfilled by a person, always leaves it empty.
1537
+ * Keys the translator left blank that still need a translation, drifted or not, sorted by key.
1538
+ * Pending work made visible: the row is skipped (nothing is written and the prior lock baseline is
1539
+ * kept), but the key is surfaced so an unfinished import is never silently reported as done. Only a
1540
+ * workbook import populates this; the provider path, which never leaves a candidate key unfilled by
1541
+ * a person, always leaves it empty.
1542
+ *
1543
+ * Membership is decided by the import-time diff, not by the `status` the row was exported with: a
1544
+ * never-translated key exports as `new`, and a first handoff is all-new, which is the most common
1545
+ * unfilled case there is. A row exported as `changed` whose key no longer needs work by import time
1546
+ * is correspondingly excluded.
1521
1547
  */
1522
1548
  readonly unfilled: readonly string[];
1523
1549
  /**
@@ -1697,6 +1723,19 @@ interface LocaleFinishedEvent {
1697
1723
  readonly locale: string;
1698
1724
  /** Number of keys accepted (translated) for this locale. In a dry-run, the keys that would be translated. */
1699
1725
  readonly translated: number;
1726
+ /**
1727
+ * Zero-based index of this locale in the run's target-locale order, carrying the same value as the
1728
+ * matching {@link LocaleStartedEvent}. This is a correlation key, so a consumer can pair a finish
1729
+ * with its start without matching on the locale name.
1730
+ *
1731
+ * It is deliberately NOT a progress counter. The worker pool claims indices up front, so this is a
1732
+ * claim ordinal rather than a completion ordinal: under concurrency the locales finish in a
1733
+ * different order, and rendering `localeIndex + 1` of `totalLocales` on a finish produces a
1734
+ * counter that jumps around and never reaches its own total in order. Do not reintroduce one.
1735
+ */
1736
+ readonly localeIndex: number;
1737
+ /** Total number of target locales in this run, mirroring {@link LocaleStartedEvent}. */
1738
+ readonly totalLocales: number;
1700
1739
  }
1701
1740
  /** The run reached the end of its locale loop: how many locales it processed. */
1702
1741
  interface RunFinishedEvent {
@@ -2060,8 +2099,9 @@ interface WatchInput {
2060
2099
  readonly lockAcquireTimeoutMs?: number;
2061
2100
  /**
2062
2101
  * Passed through to every run's {@link TranslateInput.concurrency}: how many locales each cycle may
2063
- * run at once. Defaults to 1 (strictly serial). The per-cycle tracker is fresh, so a budgeted watch
2064
- * with concurrency greater than 1 still fails each run with `CONCURRENCY_BUDGET_CONFLICT`.
2102
+ * run at once. Defaults to 1 (strictly serial). Because the per-cycle budget tracker is fresh, a
2103
+ * value greater than 1 cannot be honored alongside a `maxTokens` budget; {@link watch} refuses that
2104
+ * combination at startup rather than letting every cycle fail on it.
2065
2105
  */
2066
2106
  readonly concurrency?: number;
2067
2107
  /**
@@ -2095,9 +2135,23 @@ interface WatchController {
2095
2135
  * `onRun` as a {@link WatchRunResult} and watching continues. Returns a controller whose `stop()`
2096
2136
  * closes the watcher and awaits the in-flight run.
2097
2137
  *
2138
+ * Both startup refusals are ordered deliberately: `concurrency` is resolved first because it is
2139
+ * decidable from the arguments alone, while the missing-source check is an I/O probe of project
2140
+ * state, so argument validation runs before any I/O and before the watcher exists. The concurrency
2141
+ * resolve passes `dryRun: false` unconditionally, which is exact rather than a simplification: there
2142
+ * is no dry-run watch, so the budget conflict always applies to a session.
2143
+ *
2144
+ * Resolving it here rather than leaving it to each cycle is the point, not an optimization: a watch
2145
+ * session is long-lived, so a per-cycle refusal would fail the initial run and every run after it,
2146
+ * indefinitely, while the session stayed alive and looked healthy.
2147
+ *
2098
2148
  * @param input - The config, optional cwd/debounce, and the `onRun` callback that receives each result.
2099
2149
  * @param deps - Optional composition seams (watcher, run, registry, provider builder, file system) for tests.
2100
2150
  * @returns A {@link WatchController}; call `stop()` to close the watcher and await the in-flight run.
2151
+ * @throws {@link SdkError} `CONCURRENCY_INVALID`: at startup only, when `concurrency` is defined but
2152
+ * not an integer of at least 1.
2153
+ * @throws {@link SdkError} `CONCURRENCY_BUDGET_CONFLICT`: at startup only, when `concurrency` is
2154
+ * greater than 1 while the config sets a `maxTokens` budget.
2101
2155
  * @throws {@link SdkError} `SOURCE_UNREADABLE`: at startup only, when the source locale file is absent.
2102
2156
  * @example
2103
2157
  * ```ts