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 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
@@ -0,0 +1,3 @@
1
+ export * from "./provenance.mjs";
2
+ export * from "./marked.mjs";
3
+ export * from "./audit.mjs";
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
+ }