plumb-line-provenance 0.3.1 → 0.4.1

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/audit.mjs CHANGED
@@ -9,8 +9,21 @@ import {
9
9
 
10
10
  const CLEAN_SOURCES = ["real", "semiReal", "fallback"];
11
11
 
12
+ /**
13
+ * Checks a provenance metadata envelope for internal consistency.
14
+ * Returns an empty array when the envelope is consistent; otherwise returns
15
+ * one string per issue found. Issue prefixes:
16
+ * - `"laundering:"` — a clean source combined with mock taint
17
+ * - `"over-claiming:"` — confidence or confidenceScore higher than lineage supports
18
+ * - `"source over-claim:"` — weakestSource cleaner than lineage proves
19
+ * - `"taint dropped:"` — a tainted lineage step but derivedFromMock is false
20
+ * - `"unreproducible:"` — source is "derived" but lineage is empty
21
+ * - `"missing meta"` — input is not a plain object (null, undefined, or a non-object)
22
+ * @param {object|null|undefined} meta - Envelope to audit
23
+ * @returns {string[]} List of issue descriptions; empty means consistent
24
+ */
12
25
  export function auditMeta(meta) {
13
- if (!meta) return ["missing meta"];
26
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) return ["missing meta"];
14
27
  const issues = [];
15
28
  const lineage = Array.isArray(meta.lineage) ? meta.lineage : [];
16
29
 
@@ -77,3 +90,34 @@ export function auditMeta(meta) {
77
90
 
78
91
  return issues;
79
92
  }
93
+
94
+ // The four required fields (SPEC §1) and their type predicates, in the order
95
+ // they appear in the envelope table.
96
+ const REQUIRED_FIELDS = [
97
+ ["source", (v) => typeof v === "string", "a string"],
98
+ ["confidence", (v) => typeof v === "string", "a string"],
99
+ ["derivedFromMock", (v) => typeof v === "boolean", "a boolean"],
100
+ ["lineage", (v) => Array.isArray(v), "an array"],
101
+ ];
102
+
103
+ // validateEnvelope — the *structural* checker, complementary to auditMeta.
104
+ // auditMeta verifies logical consistency among the fields that ARE present and
105
+ // tolerates absence as "unknown" (SPEC §2); it therefore passes a structurally
106
+ // empty `{}`. validateEnvelope verifies the four required fields (SPEC §1) are
107
+ // present and well-typed. Like auditMeta it is total: it returns a list of issue
108
+ // strings (empty = structurally valid) and never throws.
109
+ export function validateEnvelope(meta) {
110
+ if (meta === null || meta === undefined) return ["missing meta"];
111
+ if (typeof meta !== "object" || Array.isArray(meta)) {
112
+ return ["not an envelope object"];
113
+ }
114
+ const issues = [];
115
+ for (const [name, ok, typeLabel] of REQUIRED_FIELDS) {
116
+ if (!(name in meta)) {
117
+ issues.push(`missing required field: ${name}`);
118
+ } else if (!ok(meta[name])) {
119
+ issues.push(`field '${name}' must be ${typeLabel}`);
120
+ }
121
+ }
122
+ return issues;
123
+ }
package/marked.mjs CHANGED
@@ -17,14 +17,33 @@ const META_KEYS = [
17
17
  // derivedFromMock taint cannot be cleared through an override.
18
18
  const OVERRIDE_KEYS = ["source", "confidence", "confidenceScore", "basis", "adapter"];
19
19
 
20
+ /**
21
+ * Wraps a value with provenance metadata, producing a marked value object.
22
+ * The returned object is frozen; its `value` property holds the original value
23
+ * and the remaining properties are the metadata envelope fields.
24
+ * @param {*} value - Any value to track
25
+ * @param {object} [metaInput={}] - Initial metadata; same options as {@link makeMeta}
26
+ * @returns {Readonly<{value: *, source: string, confidence: string, derivedFromMock: boolean, lineage: object[]}>}
27
+ */
20
28
  export function mark(value, metaInput = {}) {
21
29
  return Object.freeze({ value, ...makeMeta(metaInput) });
22
30
  }
23
31
 
32
+ /**
33
+ * Extracts the raw value from a marked object.
34
+ * @param {object} marked - A value produced by {@link mark} or {@link derive}
35
+ * @returns {*} The unwrapped value
36
+ */
24
37
  export function unwrap(marked) {
25
38
  return marked?.value;
26
39
  }
27
40
 
41
+ /**
42
+ * Extracts the provenance metadata from a marked object as a plain object.
43
+ * Only the known envelope keys are included; the `value` field is excluded.
44
+ * @param {object} marked - A value produced by {@link mark} or {@link derive}
45
+ * @returns {object} Metadata envelope
46
+ */
28
47
  export function metaOf(marked) {
29
48
  const meta = {};
30
49
  for (const key of META_KEYS)
@@ -32,6 +51,18 @@ export function metaOf(marked) {
32
51
  return meta;
33
52
  }
34
53
 
54
+ /**
55
+ * Derives a new marked value from one or more marked inputs.
56
+ * The combination law is applied automatically: mock taint and the weakest
57
+ * confidence propagate to the result and cannot be overridden.
58
+ * @param {object[]} inputs - Marked values produced by {@link mark} or {@link derive}
59
+ * @param {Function} fn - Pure function applied to the unwrapped input values
60
+ * @param {object} [metaOverride={}] - Optional overrides for `source`, `confidence`,
61
+ * `confidenceScore`, `basis`, or `adapter`; `derivedFromMock` cannot be cleared.
62
+ * By convention `basis` is an operation label naming the transform `fn`
63
+ * (e.g. `"pricing.applyFx@v3"`) — lineage records input states, not `fn`. See SPEC §4.
64
+ * @returns {Readonly<{value: *, source: string, confidence: string, derivedFromMock: boolean, lineage: object[]}>}
65
+ */
35
66
  export function derive(inputs, fn, metaOverride = {}) {
36
67
  const value = fn(...inputs.map(unwrap));
37
68
  const combined = combineProvenance(...inputs.map(metaOf));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plumb-line-provenance",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Conservative provenance/confidence/lineage envelope with a taint-propagation combination law. Mock or low-confidence data cannot launder itself clean.",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
@@ -41,6 +41,7 @@
41
41
  "node": ">=16"
42
42
  },
43
43
  "devDependencies": {
44
+ "fast-check": "^4.8.0",
44
45
  "vitest": "^4.1.9"
45
46
  }
46
47
  }
package/provenance.mjs CHANGED
@@ -15,10 +15,28 @@ export const STATUS = [
15
15
  ];
16
16
  export const CONFIDENCE = ["none", "low", "medium", "high"];
17
17
 
18
+ /**
19
+ * Returns true when x is a finite number in [0, 1].
20
+ * @param {*} x
21
+ * @returns {boolean}
22
+ */
18
23
  export function isScore(x) {
19
24
  return typeof x === "number" && Number.isFinite(x) && x >= 0 && x <= 1;
20
25
  }
21
26
 
27
+ /**
28
+ * Constructs a frozen provenance metadata envelope.
29
+ * @param {object} [opts]
30
+ * @param {string} [opts.source="derived"] - One of {@link STATUS}
31
+ * @param {string} [opts.confidence="none"] - One of {@link CONFIDENCE}
32
+ * @param {number} [opts.confidenceScore] - Numeric precision in [0, 1]; omitted when invalid
33
+ * @param {boolean} [opts.derivedFromMock] - Defaults to `source === "mock"`
34
+ * @param {object[]} [opts.lineage=[]] - Prior lineage steps; each step is frozen
35
+ * @param {string} [opts.weakestSource] - Lowest-ranked source in ancestry; one of {@link STATUS}
36
+ * @param {*} [opts.basis] - Arbitrary domain metadata (passed through unchanged)
37
+ * @param {*} [opts.adapter] - Adapter identifier (passed through unchanged)
38
+ * @returns {Readonly<object>} Frozen envelope
39
+ */
22
40
  export function makeMeta({
23
41
  source = "derived",
24
42
  confidence = "none",
@@ -57,6 +75,12 @@ export function makeMeta({
57
75
  return Object.freeze(meta);
58
76
  }
59
77
 
78
+ /**
79
+ * Returns the weakest (lowest-ranked) confidence level among the given values.
80
+ * Unknown values are treated as `"none"`. Returns `"none"` when called with no arguments.
81
+ * @param {...string} levels - Values from {@link CONFIDENCE}
82
+ * @returns {string} Weakest confidence level
83
+ */
60
84
  export function weakestConfidence(...levels) {
61
85
  if (levels.length === 0) return "none";
62
86
  let minIdx = CONFIDENCE.length - 1;
@@ -67,12 +91,22 @@ export function weakestConfidence(...levels) {
67
91
  return CONFIDENCE[minIdx];
68
92
  }
69
93
 
94
+ /**
95
+ * Returns true when the envelope carries mock taint
96
+ * (either `derivedFromMock` is truthy or `source` is `"mock"`).
97
+ * @param {object|null|undefined} meta
98
+ * @returns {boolean}
99
+ */
70
100
  export function taints(meta) {
71
101
  return Boolean(meta?.derivedFromMock) || meta?.source === "mock";
72
102
  }
73
103
 
74
- // Least-trustworthy source among the given values, ranked by STATUS. Unknown
75
- // values are ignored; returns undefined when nothing is rankable.
104
+ /**
105
+ * Returns the least-trustworthy source among the given values, ranked by {@link STATUS}.
106
+ * Unknown values are ignored. Returns `undefined` when nothing is rankable.
107
+ * @param {...string} sources - Values from {@link STATUS}
108
+ * @returns {string|undefined}
109
+ */
76
110
  export function weakestSource(...sources) {
77
111
  let minIdx = STATUS.length;
78
112
  for (const s of sources) {
@@ -82,9 +116,13 @@ export function weakestSource(...sources) {
82
116
  return minIdx === STATUS.length ? undefined : STATUS[minIdx];
83
117
  }
84
118
 
85
- // Conservative numeric floor: the minimum, but only when every input carries a
86
- // score. A missing score is "unknown" and cannot be dropped from a min, so any
87
- // gap yields undefined (omit) rather than an over-claim.
119
+ /**
120
+ * Returns the minimum of an array of numeric confidence scores,
121
+ * but only when every element is a valid score. Returns `undefined` if any
122
+ * element is missing or invalid — a gap is "unknown", not zero.
123
+ * @param {number[]} scores
124
+ * @returns {number|undefined}
125
+ */
88
126
  export function combineConfidenceScore(scores) {
89
127
  if (scores.length === 0 || !scores.every(isScore)) return undefined;
90
128
  return Math.min(...scores);
@@ -95,6 +133,17 @@ export function combineConfidenceScore(scores) {
95
133
  // shared state to reset between runs. Safe to delete from call sites.
96
134
  export function __resetStepCounter() {}
97
135
 
136
+ /**
137
+ * Applies the taint-propagation combination law to one or more metadata envelopes
138
+ * and returns a new derived envelope. This is the core invariant: mock taint
139
+ * propagates forward and cannot be cleared.
140
+ *
141
+ * Calling with zero arguments returns an `"unavailable"` envelope (not `"derived"`),
142
+ * because a value derived from nothing has no honest provenance.
143
+ *
144
+ * @param {...object} metas - Provenance envelopes produced by {@link makeMeta}
145
+ * @returns {Readonly<object>} Combined envelope with `source: "derived"`
146
+ */
98
147
  export function combineProvenance(...metas) {
99
148
  // A value combined from no inputs is derived from nothing — honestly
100
149
  // 'unavailable', not 'derived'. Returning 'derived' with an empty lineage