plumb-line-provenance 0.2.0 → 0.3.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
@@ -20,9 +20,14 @@ export function auditMeta(meta) {
20
20
  );
21
21
  }
22
22
 
23
+ // An unknown confidence on a step is laundering, not "no signal": treat it as
24
+ // the `none` floor (mirroring weakestConfidence), so audit is never laxer than
25
+ // the combination law. A step that records *no* confidence is still skipped —
26
+ // absence is genuinely unrankable and must not manufacture a false over-claim.
23
27
  const lineageConfidences = lineage
24
28
  .map((s) => s?.confidence)
25
- .filter((c) => CONFIDENCE.includes(c));
29
+ .filter((c) => c != null)
30
+ .map((c) => (CONFIDENCE.includes(c) ? c : "none"));
26
31
  if (lineageConfidences.length > 0) {
27
32
  const weakest = weakestConfidence(...lineageConfidences);
28
33
  if (CONFIDENCE.indexOf(meta.confidence) > CONFIDENCE.indexOf(weakest)) {
package/marked.mjs CHANGED
@@ -18,7 +18,7 @@ const META_KEYS = [
18
18
  const OVERRIDE_KEYS = ["source", "confidence", "confidenceScore", "basis", "adapter"];
19
19
 
20
20
  export function mark(value, metaInput = {}) {
21
- return { value, ...makeMeta(metaInput) };
21
+ return Object.freeze({ value, ...makeMeta(metaInput) });
22
22
  }
23
23
 
24
24
  export function unwrap(marked) {
@@ -39,11 +39,15 @@ export function derive(inputs, fn, metaOverride = {}) {
39
39
  for (const key of OVERRIDE_KEYS) {
40
40
  if (key in metaOverride) safeOverride[key] = metaOverride[key];
41
41
  }
42
- const merged = {
42
+ // Route the override through makeMeta so derive is never weaker than the
43
+ // constructor: an out-of-range confidenceScore (or unrankable weakestSource)
44
+ // is dropped by the same validation, not stored raw. derivedFromMock is
45
+ // force-OR'd *before* the call, so taint still cannot be cleared (the one law).
46
+ const merged = makeMeta({
43
47
  ...combined,
44
48
  ...safeOverride,
45
49
  derivedFromMock:
46
50
  combined.derivedFromMock || Boolean(metaOverride.derivedFromMock),
47
- };
48
- return { value, ...merged };
51
+ });
52
+ return Object.freeze({ value, ...merged });
49
53
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plumb-line-provenance",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",
@@ -28,6 +28,9 @@
28
28
  ],
29
29
  "author": "Aoife Okonedo Martin",
30
30
  "license": "Apache-2.0",
31
+ "bugs": {
32
+ "url": "https://github.com/effythealien/plumb-line/issues"
33
+ },
31
34
  "homepage": "https://github.com/effythealien/plumb-line/tree/main/primitives",
32
35
  "repository": {
33
36
  "type": "git",
@@ -38,6 +41,6 @@
38
41
  "node": ">=16"
39
42
  },
40
43
  "devDependencies": {
41
- "vitest": "^2.0.0"
44
+ "vitest": "^4.1.9"
42
45
  }
43
46
  }
package/provenance.mjs CHANGED
@@ -36,7 +36,15 @@ export function makeMeta({
36
36
  derivedFromMock === undefined
37
37
  ? source === "mock"
38
38
  : Boolean(derivedFromMock),
39
- lineage: Array.isArray(lineage) ? [...lineage] : [],
39
+ // Each meta owns a *frozen copy* of its lineage. Steps are cloned then
40
+ // frozen so (a) an envelope's recorded history can't be rewritten in place,
41
+ // and (b) a step shared across parent/child metas can't leak a mutation from
42
+ // one into the other — the audit trail an auditMeta() trusts stays intact.
43
+ lineage: Object.freeze(
44
+ (Array.isArray(lineage) ? lineage : []).map((s) =>
45
+ s && typeof s === "object" ? Object.freeze({ ...s }) : s,
46
+ ),
47
+ ),
40
48
  };
41
49
  // Optional numeric confidence — a finer-grained companion to the ordinal
42
50
  // `confidence`, never a replacement. Stored only when it is a valid score.
@@ -46,7 +54,7 @@ export function makeMeta({
46
54
  if (STATUS.includes(weakestSource)) meta.weakestSource = weakestSource;
47
55
  if (basis !== undefined) meta.basis = basis;
48
56
  if (adapter !== undefined) meta.adapter = adapter;
49
- return meta;
57
+ return Object.freeze(meta);
50
58
  }
51
59
 
52
60
  export function weakestConfidence(...levels) {
@@ -82,16 +90,24 @@ export function combineConfidenceScore(scores) {
82
90
  return Math.min(...scores);
83
91
  }
84
92
 
85
- let __stepCounter = 0;
86
- export function __resetStepCounter() {
87
- __stepCounter = 0;
88
- }
89
- function nextStepId() {
90
- __stepCounter += 1;
91
- return `step-${__stepCounter}`;
92
- }
93
+ // Deprecated no-op, kept for import compatibility. Step IDs are now assigned by
94
+ // a counter local to each combineProvenance call (see below), so there is no
95
+ // shared state to reset between runs. Safe to delete from call sites.
96
+ export function __resetStepCounter() {}
93
97
 
94
98
  export function combineProvenance(...metas) {
99
+ // A value combined from no inputs is derived from nothing — honestly
100
+ // 'unavailable', not 'derived'. Returning 'derived' with an empty lineage
101
+ // would contradict auditMeta's "derived value has no lineage" check
102
+ // (SPEC §3 vs §5). See #25.
103
+ if (metas.length === 0) {
104
+ return makeMeta({
105
+ source: "unavailable",
106
+ confidence: "none",
107
+ derivedFromMock: false,
108
+ lineage: [],
109
+ });
110
+ }
95
111
  const derivedFromMock = metas.some((m) => taints(m));
96
112
  const confidence = weakestConfidence(...metas.map((m) => m?.confidence));
97
113
  const confidenceScore = combineConfidenceScore(
@@ -102,7 +118,6 @@ export function combineProvenance(...metas) {
102
118
  );
103
119
  const inputSteps = metas.map((m) => {
104
120
  const step = {
105
- id: nextStepId(),
106
121
  of: "input",
107
122
  source: m?.source,
108
123
  confidence: m?.confidence,
@@ -113,7 +128,16 @@ export function combineProvenance(...metas) {
113
128
  if (isScore(m?.confidenceScore)) step.confidenceScore = m.confidenceScore;
114
129
  return step;
115
130
  });
116
- const lineage = [...priorLineage, ...inputSteps];
131
+ // Renumber the *entire* output lineage from a combine-local counter, so step
132
+ // IDs are unique-within-output (SPEC §4) for every input shape — two
133
+ // independently-built inputs each start at step-1, so seeding past the prior
134
+ // length alone wouldn't stop their inherited steps from colliding. No
135
+ // module-level state means concurrent combines can't collide either. IDs are
136
+ // thus a pure function of output structure, not creation order. See #23.
137
+ const lineage = [...priorLineage, ...inputSteps].map((s, i) => ({
138
+ ...s,
139
+ id: `step-${i + 1}`,
140
+ }));
117
141
  return makeMeta({
118
142
  source: "derived",
119
143
  confidence,