@rulvar/core 1.110.0 → 1.112.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.
package/dist/index.d.ts CHANGED
@@ -824,6 +824,15 @@ interface EntryBillingFold {
824
824
  * semantics. False folds the aggregate slices, the historical basis.
825
825
  */
826
826
  fullyAttributed: boolean;
827
+ /**
828
+ * The models this fold priced per call: record sums equal slice sums
829
+ * counter for counter under the symmetric per-model key (RV604).
830
+ * Published so a row builder can honor the same decision (RV703): a
831
+ * covered model's rows are exactly its records, so no per-slice
832
+ * remainder may be fabricated for it; recomputing coverage elsewhere
833
+ * is how the phantom-remainder skew was born.
834
+ */
835
+ coveredModels: ReadonlySet<ModelRef>;
827
836
  }
828
837
  /**
829
838
  * The billing fold over one terminal entry (RV504), shared by the
@@ -3160,6 +3169,15 @@ declare function quotaRuleKey(rule: QuotaRule): string;
3160
3169
  * so ordinary JavaScript after the constructor (a pushed rule, a
3161
3170
  * reassigned cap) can no longer change a decision, a bucket key, or a
3162
3171
  * recorded fingerprint.
3172
+ *
3173
+ * A set containing two rules with the same canonical content key is
3174
+ * refused typed (RV704): the memory reference buckets by rule INDEX
3175
+ * (each copy counts independently, the full cap admits) while the
3176
+ * store references bucket by rule KEY (one shared bucket is debited
3177
+ * once per matching copy, half the cap admits), so the same duplicated
3178
+ * configuration admitted differently per storage. Refusing it at the
3179
+ * shared construction chokepoint is what keeps equal configurations
3180
+ * equal on every storage.
3163
3181
  */
3164
3182
  declare function snapshotQuotaRules(rules: readonly QuotaRule[], site?: string): readonly QuotaRule[];
3165
3183
  /** True when every dimension the rule pins matches the request. */
package/dist/index.js CHANGED
@@ -2532,7 +2532,8 @@ function priceEntryBilling(entry, priceUsd) {
2532
2532
  servedBy,
2533
2533
  usage
2534
2534
  })),
2535
- fullyAttributed
2535
+ fullyAttributed,
2536
+ coveredModels: covered
2536
2537
  };
2537
2538
  }
2538
2539
  /**
@@ -8542,16 +8543,22 @@ function costReportFromJournal(entries, priceUsd) {
8542
8543
  * and per-row `allocatedUsd` is the additive column whose flat sum
8543
8544
  * reproduces `totalUsd` exactly in every case.
8544
8545
  *
8545
- * Coverage is loss-free by construction: an entry whose records do not
8546
- * cover its usage total (a resume restored from a checkpoint written
8547
- * before the ledger shipped) contributes an `unattributed` remainder
8548
- * row, and an entry with no records at all (written before the ledger
8549
- * shipped, or a fully replayed invocation) contributes one
8550
- * `unattributed` row per usage slice. Missing provider ids are marked,
8551
- * never dropped: a finished call without one reconciles as
8552
- * `missing-provider-id`, a failed or severed call without one as
8553
- * `unconfirmed` (the provider may or may not have billed it; there is
8554
- * no id to match).
8546
+ * Coverage is loss-free by construction: a model whose records do not
8547
+ * cover its usage (a resume restored from a checkpoint written before
8548
+ * the ledger shipped) contributes an `unattributed` remainder row per
8549
+ * slice, and an entry with no records at all (written before the
8550
+ * ledger shipped, or a fully replayed invocation) contributes one
8551
+ * `unattributed` row per usage slice. A COVERED model contributes no
8552
+ * remainder rows at all (RV703): its rows are exactly its records, the
8553
+ * same per-model decision the billing fold makes, so a role mismatch
8554
+ * between records and slices (the schema-extract default splits one
8555
+ * model's usage by role while the record carries one role, or none)
8556
+ * can no longer fabricate a phantom row that breaks the
8557
+ * `rowUsdNonAdditive: false` promise and siphons allocation from the
8558
+ * real call. Missing provider ids are marked, never dropped: a
8559
+ * finished call without one reconciles as `missing-provider-id`, a
8560
+ * failed or severed call without one as `unconfirmed` (the provider
8561
+ * may or may not have billed it; there is no id to match).
8555
8562
  *
8556
8563
  * Pricing happens at fold time from the table you pass, exactly like
8557
8564
  * CostReport. For historical stability against price-table updates,
@@ -8577,6 +8584,12 @@ const USAGE_FIELDS = [
8577
8584
  * model: the whole-entry remainder was published under `entry.servedBy`,
8578
8585
  * so a slice with no records left its allocation pool rowless and the
8579
8586
  * dust pass moved its dollars onto another model's row.
8587
+ *
8588
+ * Consulted only for UNCOVERED models (RV703): coverage is a per-model
8589
+ * decision, so a covered model's slices never reach this arithmetic.
8590
+ * The per-role subtraction here against the per-model coverage key was
8591
+ * exactly the mismatch that fabricated a phantom remainder whenever a
8592
+ * covered model's record roles differed from its slice roles.
8580
8593
  */
8581
8594
  function sliceRemainder(slice, records) {
8582
8595
  const remainder = {
@@ -8686,7 +8699,8 @@ function invoiceFromJournal(entries, priceUsd, options) {
8686
8699
  let everyEntryFullyAttributed = true;
8687
8700
  for (const entry of entries) {
8688
8701
  if (entry.status === "running" || entry.usage === void 0) continue;
8689
- if (!priceEntryBilling(entry, priceUsd).fullyAttributed) everyEntryFullyAttributed = false;
8702
+ const billing = priceEntryBilling(entry, priceUsd);
8703
+ if (!billing.fullyAttributed) everyEntryFullyAttributed = false;
8690
8704
  const abandoned = entry.kind !== "resolution" && entry.kind !== "abandon" && abandonFold.isAbandoned(entry.ref ?? entry.seq);
8691
8705
  const base = {
8692
8706
  entrySeq: entry.seq,
@@ -8737,6 +8751,7 @@ function invoiceFromJournal(entries, priceUsd, options) {
8737
8751
  }
8738
8752
  let remainderOrdinal = records.length + 1;
8739
8753
  for (const slice of entryUsageSlices(entry)) {
8754
+ if (billing.coveredModels.has(slice.servedBy)) continue;
8740
8755
  const remainder = sliceRemainder(slice, records);
8741
8756
  if (remainder === void 0) continue;
8742
8757
  const usd = rowUsd(priceUsd, slice.servedBy, remainder, entry.seq);
@@ -9271,9 +9286,25 @@ function quotaRuleKey(rule) {
9271
9286
  * so ordinary JavaScript after the constructor (a pushed rule, a
9272
9287
  * reassigned cap) can no longer change a decision, a bucket key, or a
9273
9288
  * recorded fingerprint.
9289
+ *
9290
+ * A set containing two rules with the same canonical content key is
9291
+ * refused typed (RV704): the memory reference buckets by rule INDEX
9292
+ * (each copy counts independently, the full cap admits) while the
9293
+ * store references bucket by rule KEY (one shared bucket is debited
9294
+ * once per matching copy, half the cap admits), so the same duplicated
9295
+ * configuration admitted differently per storage. Refusing it at the
9296
+ * shared construction chokepoint is what keeps equal configurations
9297
+ * equal on every storage.
9274
9298
  */
9275
9299
  function snapshotQuotaRules(rules, site = "quota rules") {
9276
9300
  validateQuotaRules(rules, site);
9301
+ const firstIndexByKey = /* @__PURE__ */ new Map();
9302
+ rules.forEach((rule, index) => {
9303
+ const key = quotaRuleKey(rule);
9304
+ const first = firstIndexByKey.get(key);
9305
+ if (first !== void 0) throw new ConfigError(`${site}[${String(index)}] duplicates ${site}[${String(first)}] (rule key ${key}): identical rules occupy independent buckets in memory but share one key-debited bucket on keyed storage, so one configuration would admit differently per store; delete the duplicate`);
9306
+ firstIndexByKey.set(key, index);
9307
+ });
9277
9308
  return Object.freeze(rules.map((rule) => Object.freeze({
9278
9309
  ...rule.provider === void 0 ? {} : { provider: rule.provider },
9279
9310
  ...rule.model === void 0 ? {} : { model: rule.model },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/core",
3
- "version": "1.110.0",
3
+ "version": "1.112.0",
4
4
  "description": "Rulvar core: L0 contracts, journal kernel, ctx primitives, agent runtime, model router, tool system, dynamic orchestrator, InMemory and JSONL stores, event stream.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",