plumb-line-provenance 0.2.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/README.md +27 -0
- package/audit.mjs +74 -0
- package/index.mjs +3 -0
- package/marked.mjs +49 -0
- package/package.json +43 -0
- package/provenance.mjs +126 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# plumb-line-provenance (JavaScript)
|
|
2
|
+
|
|
3
|
+
A conservative provenance / confidence / lineage envelope with a
|
|
4
|
+
taint-propagation combination law: once any input is mock or low-confidence,
|
|
5
|
+
every value derived from it inherits that taint automatically — there is no
|
|
6
|
+
escape hatch that silently clears the flag.
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
import { mark, derive, metaOf, auditMeta } from "plumb-line-provenance";
|
|
10
|
+
|
|
11
|
+
const base = mark(1000, { source: "real", confidence: "high" });
|
|
12
|
+
const rate = mark(1.25, { source: "mock", confidence: "low" });
|
|
13
|
+
const total = derive([base, rate], (a, r) => a * r);
|
|
14
|
+
|
|
15
|
+
total.derivedFromMock; // true — inherited from rate, cannot be cleared
|
|
16
|
+
total.confidence; // 'low' — only as certain as the weakest input
|
|
17
|
+
auditMeta(metaOf(total)); // [] — internally consistent
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
You can also copy the `.mjs` files directly into a project and import them
|
|
21
|
+
relatively; both styles work.
|
|
22
|
+
|
|
23
|
+
- **Specification:** [`SPEC.md`](https://github.com/effythealien/plumb-line/blob/main/primitives/SPEC.md) (envelope schema version 1)
|
|
24
|
+
- **Model, law, examples:** [`README.md`](https://github.com/effythealien/plumb-line/blob/main/primitives/README.md)
|
|
25
|
+
- **License:** Apache-2.0
|
|
26
|
+
|
|
27
|
+
Python parity package: `plumb-line-provenance` on PyPI.
|
package/audit.mjs
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// audit.mjs — runtime consistency checker for provenance metadata.
|
|
2
|
+
import {
|
|
3
|
+
CONFIDENCE,
|
|
4
|
+
STATUS,
|
|
5
|
+
weakestConfidence,
|
|
6
|
+
weakestSource,
|
|
7
|
+
isScore,
|
|
8
|
+
} from "./provenance.mjs";
|
|
9
|
+
|
|
10
|
+
const CLEAN_SOURCES = ["real", "semiReal", "fallback"];
|
|
11
|
+
|
|
12
|
+
export function auditMeta(meta) {
|
|
13
|
+
if (!meta) return ["missing meta"];
|
|
14
|
+
const issues = [];
|
|
15
|
+
const lineage = Array.isArray(meta.lineage) ? meta.lineage : [];
|
|
16
|
+
|
|
17
|
+
if (CLEAN_SOURCES.includes(meta.source) && meta.derivedFromMock === true) {
|
|
18
|
+
issues.push(
|
|
19
|
+
`laundering: clean source '${meta.source}' but derivedFromMock is true`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const lineageConfidences = lineage
|
|
24
|
+
.map((s) => s?.confidence)
|
|
25
|
+
.filter((c) => CONFIDENCE.includes(c));
|
|
26
|
+
if (lineageConfidences.length > 0) {
|
|
27
|
+
const weakest = weakestConfidence(...lineageConfidences);
|
|
28
|
+
if (CONFIDENCE.indexOf(meta.confidence) > CONFIDENCE.indexOf(weakest)) {
|
|
29
|
+
issues.push(
|
|
30
|
+
`over-claiming: confidence '${meta.confidence}' exceeds weakest lineage confidence '${weakest}'`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Numeric over-claiming — the higher-resolution analog of the ordinal check.
|
|
36
|
+
if (isScore(meta.confidenceScore)) {
|
|
37
|
+
const lineageScores = lineage
|
|
38
|
+
.map((s) => s?.confidenceScore)
|
|
39
|
+
.filter((c) => isScore(c));
|
|
40
|
+
if (lineageScores.length > 0) {
|
|
41
|
+
const weakest = Math.min(...lineageScores);
|
|
42
|
+
if (meta.confidenceScore > weakest) {
|
|
43
|
+
issues.push(
|
|
44
|
+
`over-claiming: confidenceScore ${meta.confidenceScore} exceeds weakest lineage score ${weakest}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Source over-claim — weakestSource cannot look cleaner than the lineage proves.
|
|
51
|
+
if (STATUS.includes(meta.weakestSource)) {
|
|
52
|
+
const actual = weakestSource(...lineage.map((s) => s?.source));
|
|
53
|
+
if (actual && STATUS.indexOf(meta.weakestSource) > STATUS.indexOf(actual)) {
|
|
54
|
+
issues.push(
|
|
55
|
+
`source over-claim: weakestSource '${meta.weakestSource}' is cleaner than lineage's '${actual}'`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const lineageTainted = lineage.some(
|
|
61
|
+
(s) => Boolean(s?.derivedFromMock) || s?.source === "mock",
|
|
62
|
+
);
|
|
63
|
+
if (lineageTainted && meta.derivedFromMock === false) {
|
|
64
|
+
issues.push(
|
|
65
|
+
"taint dropped: lineage contains a tainted step but derivedFromMock is false",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (meta.source === "derived" && lineage.length === 0) {
|
|
70
|
+
issues.push("unreproducible: derived value has no lineage");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return issues;
|
|
74
|
+
}
|
package/index.mjs
ADDED
package/marked.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// marked.mjs — thin wrapper sugar over the provenance law. The law lives in provenance.mjs.
|
|
2
|
+
import { combineProvenance, makeMeta } from "./provenance.mjs";
|
|
3
|
+
|
|
4
|
+
const META_KEYS = [
|
|
5
|
+
"source",
|
|
6
|
+
"confidence",
|
|
7
|
+
"confidenceScore",
|
|
8
|
+
"derivedFromMock",
|
|
9
|
+
"lineage",
|
|
10
|
+
"weakestSource",
|
|
11
|
+
"basis",
|
|
12
|
+
"adapter",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
// Only these keys may be supplied as overrides to derive(). lineage and
|
|
16
|
+
// weakestSource always come from the computed combineProvenance result;
|
|
17
|
+
// derivedFromMock taint cannot be cleared through an override.
|
|
18
|
+
const OVERRIDE_KEYS = ["source", "confidence", "confidenceScore", "basis", "adapter"];
|
|
19
|
+
|
|
20
|
+
export function mark(value, metaInput = {}) {
|
|
21
|
+
return { value, ...makeMeta(metaInput) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function unwrap(marked) {
|
|
25
|
+
return marked?.value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function metaOf(marked) {
|
|
29
|
+
const meta = {};
|
|
30
|
+
for (const key of META_KEYS)
|
|
31
|
+
if (key in (marked || {})) meta[key] = marked[key];
|
|
32
|
+
return meta;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function derive(inputs, fn, metaOverride = {}) {
|
|
36
|
+
const value = fn(...inputs.map(unwrap));
|
|
37
|
+
const combined = combineProvenance(...inputs.map(metaOf));
|
|
38
|
+
const safeOverride = {};
|
|
39
|
+
for (const key of OVERRIDE_KEYS) {
|
|
40
|
+
if (key in metaOverride) safeOverride[key] = metaOverride[key];
|
|
41
|
+
}
|
|
42
|
+
const merged = {
|
|
43
|
+
...combined,
|
|
44
|
+
...safeOverride,
|
|
45
|
+
derivedFromMock:
|
|
46
|
+
combined.derivedFromMock || Boolean(metaOverride.derivedFromMock),
|
|
47
|
+
};
|
|
48
|
+
return { value, ...merged };
|
|
49
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "plumb-line-provenance",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Conservative provenance/confidence/lineage envelope with a taint-propagation combination law. Mock or low-confidence data cannot launder itself clean.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.mjs",
|
|
12
|
+
"provenance.mjs",
|
|
13
|
+
"marked.mjs",
|
|
14
|
+
"audit.mjs",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"prepublishOnly": "vitest run"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"provenance",
|
|
23
|
+
"lineage",
|
|
24
|
+
"confidence",
|
|
25
|
+
"epistemic-honesty",
|
|
26
|
+
"reproducibility",
|
|
27
|
+
"data-quality"
|
|
28
|
+
],
|
|
29
|
+
"author": "Aoife Okonedo Martin",
|
|
30
|
+
"license": "Apache-2.0",
|
|
31
|
+
"homepage": "https://github.com/effythealien/plumb-line/tree/main/primitives",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/effythealien/plumb-line.git",
|
|
35
|
+
"directory": "primitives/js"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=16"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"vitest": "^2.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/provenance.mjs
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// provenance.mjs — the provenance/lineage law (single source).
|
|
2
|
+
|
|
3
|
+
// Schema version of the provenance metadata envelope (Principle 7). Declared so
|
|
4
|
+
// consumers can pin to a shape; embedding it per-meta and validating against it
|
|
5
|
+
// is planned.
|
|
6
|
+
export const PROVENANCE_VERSION = 1;
|
|
7
|
+
|
|
8
|
+
export const STATUS = [
|
|
9
|
+
"unavailable",
|
|
10
|
+
"mock",
|
|
11
|
+
"fallback",
|
|
12
|
+
"semiReal",
|
|
13
|
+
"derived",
|
|
14
|
+
"real",
|
|
15
|
+
];
|
|
16
|
+
export const CONFIDENCE = ["none", "low", "medium", "high"];
|
|
17
|
+
|
|
18
|
+
export function isScore(x) {
|
|
19
|
+
return typeof x === "number" && Number.isFinite(x) && x >= 0 && x <= 1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function makeMeta({
|
|
23
|
+
source = "derived",
|
|
24
|
+
confidence = "none",
|
|
25
|
+
confidenceScore,
|
|
26
|
+
derivedFromMock,
|
|
27
|
+
lineage = [],
|
|
28
|
+
weakestSource,
|
|
29
|
+
basis,
|
|
30
|
+
adapter,
|
|
31
|
+
} = {}) {
|
|
32
|
+
const meta = {
|
|
33
|
+
source,
|
|
34
|
+
confidence,
|
|
35
|
+
derivedFromMock:
|
|
36
|
+
derivedFromMock === undefined
|
|
37
|
+
? source === "mock"
|
|
38
|
+
: Boolean(derivedFromMock),
|
|
39
|
+
lineage: Array.isArray(lineage) ? [...lineage] : [],
|
|
40
|
+
};
|
|
41
|
+
// Optional numeric confidence — a finer-grained companion to the ordinal
|
|
42
|
+
// `confidence`, never a replacement. Stored only when it is a valid score.
|
|
43
|
+
if (isScore(confidenceScore)) meta.confidenceScore = confidenceScore;
|
|
44
|
+
// Computed-only resolution beyond the derivedFromMock boolean; passed through
|
|
45
|
+
// here so chained derives carry it, but never settable as a derive override.
|
|
46
|
+
if (STATUS.includes(weakestSource)) meta.weakestSource = weakestSource;
|
|
47
|
+
if (basis !== undefined) meta.basis = basis;
|
|
48
|
+
if (adapter !== undefined) meta.adapter = adapter;
|
|
49
|
+
return meta;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function weakestConfidence(...levels) {
|
|
53
|
+
if (levels.length === 0) return "none";
|
|
54
|
+
let minIdx = CONFIDENCE.length - 1;
|
|
55
|
+
for (const level of levels) {
|
|
56
|
+
const idx = CONFIDENCE.indexOf(level);
|
|
57
|
+
minIdx = Math.min(minIdx, idx === -1 ? 0 : idx);
|
|
58
|
+
}
|
|
59
|
+
return CONFIDENCE[minIdx];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function taints(meta) {
|
|
63
|
+
return Boolean(meta?.derivedFromMock) || meta?.source === "mock";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Least-trustworthy source among the given values, ranked by STATUS. Unknown
|
|
67
|
+
// values are ignored; returns undefined when nothing is rankable.
|
|
68
|
+
export function weakestSource(...sources) {
|
|
69
|
+
let minIdx = STATUS.length;
|
|
70
|
+
for (const s of sources) {
|
|
71
|
+
const idx = STATUS.indexOf(s);
|
|
72
|
+
if (idx !== -1) minIdx = Math.min(minIdx, idx);
|
|
73
|
+
}
|
|
74
|
+
return minIdx === STATUS.length ? undefined : STATUS[minIdx];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Conservative numeric floor: the minimum, but only when every input carries a
|
|
78
|
+
// score. A missing score is "unknown" and cannot be dropped from a min, so any
|
|
79
|
+
// gap yields undefined (omit) rather than an over-claim.
|
|
80
|
+
export function combineConfidenceScore(scores) {
|
|
81
|
+
if (scores.length === 0 || !scores.every(isScore)) return undefined;
|
|
82
|
+
return Math.min(...scores);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let __stepCounter = 0;
|
|
86
|
+
export function __resetStepCounter() {
|
|
87
|
+
__stepCounter = 0;
|
|
88
|
+
}
|
|
89
|
+
function nextStepId() {
|
|
90
|
+
__stepCounter += 1;
|
|
91
|
+
return `step-${__stepCounter}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function combineProvenance(...metas) {
|
|
95
|
+
const derivedFromMock = metas.some((m) => taints(m));
|
|
96
|
+
const confidence = weakestConfidence(...metas.map((m) => m?.confidence));
|
|
97
|
+
const confidenceScore = combineConfidenceScore(
|
|
98
|
+
metas.map((m) => m?.confidenceScore),
|
|
99
|
+
);
|
|
100
|
+
const priorLineage = metas.flatMap((m) =>
|
|
101
|
+
Array.isArray(m?.lineage) ? m.lineage : [],
|
|
102
|
+
);
|
|
103
|
+
const inputSteps = metas.map((m) => {
|
|
104
|
+
const step = {
|
|
105
|
+
id: nextStepId(),
|
|
106
|
+
of: "input",
|
|
107
|
+
source: m?.source,
|
|
108
|
+
confidence: m?.confidence,
|
|
109
|
+
derivedFromMock: taints(m),
|
|
110
|
+
};
|
|
111
|
+
// Record the numeric score too when the input carries one, so the numeric
|
|
112
|
+
// over-claim audit works on real derive output, not just hand-built metas.
|
|
113
|
+
if (isScore(m?.confidenceScore)) step.confidenceScore = m.confidenceScore;
|
|
114
|
+
return step;
|
|
115
|
+
});
|
|
116
|
+
const lineage = [...priorLineage, ...inputSteps];
|
|
117
|
+
return makeMeta({
|
|
118
|
+
source: "derived",
|
|
119
|
+
confidence,
|
|
120
|
+
confidenceScore,
|
|
121
|
+
derivedFromMock,
|
|
122
|
+
lineage,
|
|
123
|
+
// Weakest source anywhere in the ancestry, read off the full lineage.
|
|
124
|
+
weakestSource: weakestSource(...lineage.map((s) => s?.source)),
|
|
125
|
+
});
|
|
126
|
+
}
|