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 +45 -1
- package/marked.mjs +31 -0
- package/package.json +2 -1
- package/provenance.mjs +54 -5
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
|
+
"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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|