harness-everything 0.36.0 → 0.36.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.
@@ -0,0 +1,249 @@
1
+ # Baseline-bound document evidence (#335)
2
+
3
+ Additive evidence contract for deleted files and the source endpoint of a rename. This extends
4
+ the phase 3a `doc-change` / `requirement-sync` producer in
5
+ [`document-lifecycle.md`](document-lifecycle.md). It does not reconcile evidence with workflow
6
+ disposition (#325 phase 3b/3c), add a lock, or add a router/provider path.
7
+
8
+ ## Data shape
9
+
10
+ The trusted baseline keeps its existing whole-tree identity and may add per-path SHA256 values:
11
+
12
+ ```js
13
+ baseline = {
14
+ digest: '<whole-baseline identity>',
15
+ artifacts: {
16
+ 'docs/adr/GRPH-008.md': '<sha256 of the trusted baseline file>'
17
+ }
18
+ }
19
+ ```
20
+
21
+ `digest` is still required for every record, including baseline-only evidence. `artifacts` is
22
+ optional; when a scope declares baseline-only paths, the matching per-path values are required.
23
+
24
+ Derivation keeps every original `scope.paths` entry and adds an annotation for paths that cannot
25
+ have a candidate artifact:
26
+
27
+ ```js
28
+ scope = {
29
+ paths: ['docs/reference/guide.md', 'docs/reference/old-guide.md'],
30
+ baselinePaths: [
31
+ { path: 'docs/reference/old-guide.md', reason: 'rename-from', renameTo: 'docs/reference/guide.md' }
32
+ ]
33
+ }
34
+ ```
35
+
36
+ - `reason: 'deleted'` describes a path present in the trusted baseline and absent from the
37
+ candidate. A deleted entry must not carry `renameTo` at all; any own `renameTo` property,
38
+ including an explicitly present `undefined`, is malformed and fails the obligation closed.
39
+ - `reason: 'rename-from'` describes the old endpoint of an identified content-identical move.
40
+ The destination is named by `renameTo` and must itself be a member of the same
41
+ `scope.paths`. A `renameTo` outside the scope is malformed and fails the obligation closed.
42
+ - Every `baselinePaths` entry's fields (`path`, `reason`, and the own-key `renameTo` presence) are
43
+ snapshotted through one guarded inspection, and each field result is carried exactly once. An
44
+ entry whose field getter or descriptor trap throws is a metadata error (`baseline-path-invalid`)
45
+ and fails the entry closed instead of escaping the checker; a field accessor that throws only on
46
+ a later read cannot abort the audit mirror; the mirror is built from the metadata-validated
47
+ snapshot (validated path and captured digest) and is never a second read of the raw entry
48
+ - A path without a `baselinePaths` entry remains a normal candidate path and keeps the existing
49
+ exact candidate-digest requirement.
50
+ - `scope.paths` is never filtered to hide a baseline-only path. If the annotation is absent, the
51
+ old fail-closed behavior remains: the path still needs candidate evidence and cannot pass.
52
+
53
+ ## Record binding
54
+
55
+ Records keep `paths`, `artifactDigests`, and `baseDigest`. They may add
56
+ `baselineArtifacts`:
57
+
58
+ ```js
59
+ {
60
+ id, obligationId, command, exitCode, provenance,
61
+ baseDigest: '<whole-baseline identity>',
62
+ paths: ['docs/reference/old-guide.md', 'docs/reference/guide.md'],
63
+ artifactDigests: { 'docs/reference/guide.md': '<candidate sha256>' },
64
+ baselineArtifacts: { 'docs/reference/old-guide.md': '<trusted baseline sha256>' }
65
+ }
66
+ ```
67
+
68
+ A deleted path is satisfied only when one observed record:
69
+
70
+ 1. carries the exact whole-baseline `baseDigest`;
71
+ 2. lists the path in `paths`;
72
+ 3. carries the exact trusted `baseline.artifacts[path]` value as an **own-key** entry in
73
+ `baselineArtifacts`;
74
+ 4. carries **no** `artifactDigests` entry for that path, and the candidate artifact map has **no
75
+ own-key entry** for that path.
76
+
77
+ The trusted per-path value is validated once during metadata validation and then **captured**.
78
+ The audit mirror, the stale-baseline diagnostics, and baseline binding compare against that
79
+ captured value and never re-read the caller-owned `baseline.artifacts` map during evidence
80
+ evaluation: a stateful own getter cannot satisfy a record with a value it started returning only
81
+ after validation, cannot mute the stale-baseline diagnostic, and cannot drift the audit mirror.
82
+ Its first read must therefore be the value every consumer sees; a getter that starts returning a
83
+ different or unusable value later is factored out entirely. A baseline per-path inspection that
84
+ throws during validation is a `baseline-artifact-invalid` metadata error and fails the path
85
+ closed. The audit mirror is likewise keyed by the **validated** path snapshot rather than a
86
+ re-read of `baselinePaths[].path`, so a stateful `path` accessor is read exactly once and a
87
+ passing result always describes the baseline identity that validated and bound the evidence.
88
+ closed instead of escaping the checker.
89
+
90
+ Per-path evidence must be carried explicitly. Inherited properties in `baseline.artifacts` or a
91
+ record's `baselineArtifacts` are not own-key evidence and never satisfy a path; the trusted
92
+ baseline entry itself must also be an own-key entry. Null-prototype dictionaries remain valid
93
+ when they carry own entries.
94
+
95
+ Presence is own-key membership, not digest truthiness: a candidate map entry such as `{}`,
96
+ `{ digest: '' }`, or `null` still means the path is present, so it is contradictory path state,
97
+ not a deletion. Equally, a record with an otherwise correct `baselineArtifacts` entry that also
98
+ carries an `artifactDigests` entry for the deleted path (a real, empty, or fake digest) is
99
+ contradictory and does not bind that path; it is reported as `baseline-path-candidate-digest`.
100
+ A valid alternate record may still satisfy the obligation.
101
+
102
+ A rename source is satisfied only by a **single** record that also lists the current
103
+ `renameTo` path, carries its current candidate digest as an **own-key** entry in
104
+ `artifactDigests`, carries no `artifactDigests` entry for the old path, and has no own-key
105
+ candidate artifact map entry for the old path. The candidate destination must itself be an
106
+ own-key entry in the candidate artifact map whose artifact entry declares its own `digest`; an
107
+ inherited nested digest is not explicit evidence. The own digest validated by that metadata check
108
+ is carried into the captured snapshot, and the record binds the destination against that captured
109
+ candidate digest — the same one used by ordinary candidate binding, the audit identity, and the
110
+ stale diagnostics. The destination is therefore never read again through the candidate map during
111
+ capture or binding: a nested `digest` that throws or changes after validation cannot make a stale
112
+ destination pair bind while the audit/stale verdicts still describe the captured value. The same
113
+ record can cover both endpoints. Destination-only evidence, source-only tombstones, split endpoint
114
+ records, and stale destination digests do not satisfy the source. A deletion tombstone therefore
115
+ cannot stand in for a known rename. This destination validation and the snapshot it produces are
116
+ **applicability-gated**: `N/A` and `unknown` (and obligations without an applicability) never bind
117
+ evidence, so they do not consume the destination digest getter during metadata validation; their
118
+ audit identity keeps the un-gated legacy candidate read, and a stateful accessor therefore reports
119
+ the same first legacy value to them that it would report without baseline binding.
120
+
121
+ Metadata validation is the single candidate snapshot for a baseline-only path. Once a declared
122
+ baseline path has been confirmed to have **no** own-key candidate entry, that validated absence is
123
+ carried on the entry; binding and the `artifactDigest` audit identity reuse it instead of
124
+ re-inspecting the candidate map, so a `Proxy` that answers own-property or `get` inspection
125
+ consistently during validation cannot later abort the check or change the audit key. A
126
+ baseline-only path therefore contributes `path=missing` to `artifactDigest` from the validated
127
+ proof rather than from a fresh candidate read. This shortcut applies only to **applicable**
128
+ obligations. `N/A` and `unknown` obligations never bind evidence and keep the legacy candidate read
129
+ for their audit identity (so a candidate `get` that supplies a digest is still reflected there);
130
+ they remain `outcome: 'none'` and do not emit `candidate-artifacts-invalid` from this applicability-gated check.
131
+
132
+ A rename destination's validated own digest is carried too. When more than one accepted
133
+ `rename-from` entry shares one `renameTo`, every entry's validated own digest must agree; a
134
+ conflicting validated digest is a contradictory shared-destination snapshot and fails the whole
135
+ obligation closed (metadata error, `outcome: 'none'`) before any binding. The carried digest must
136
+ be a non-empty string: a stateful `digest` accessor whose value is not a usable string is not
137
+ carried or seeded. Only **applicable** obligations validate and carry this destination snapshot;
138
+ `N/A` and `unknown` obligations keep the legacy candidate read for their audit identity and do not
139
+ consume the destination getter during metadata validation.
140
+
141
+ The binding applies to execution and review records alike. Existing review approval, waiver,
142
+ failing-command, requirement-sync authority, and applicability rules are unchanged. A correctly
143
+ bound command that fails still produces an observed failure, not a pass.
144
+
145
+ ## Trust limits
146
+
147
+ - `baseline.artifacts` is trusted caller/VCS input. The checker proves that evidence agrees with
148
+ the declared baseline values; it does not read Git or recompute those bytes. A validated
149
+ per-path value is read and captured once, so the agreement proof is against a single validated
150
+ snapshot rather than the value a stateful getter returns at later moments.
151
+ - The whole-baseline `digest` remains the identity anchor. A per-path digest without the exact
152
+ whole-baseline identity is insufficient.
153
+ - Candidate evidence remains authoritative for every present, new, or edited path. A baseline
154
+ digest alone never satisfies such a path.
155
+ - Path membership is explicit. A record cannot bind a path only by carrying its digest.
156
+ - Candidate presence is own-key membership in the candidate artifact map. A present entry whose
157
+ digest is absent, empty, or not a usable value is still present; it is never silently
158
+ reinterpreted as a deletion or as a rename source.
159
+ - For an applicable obligation that declares baseline-only paths, the top-level candidate
160
+ `artifacts` argument must itself be a non-null object, not an array, whose prototype is
161
+ `Object.prototype` or `null`. An omitted, `undefined`, `null`, array, string, `Map`, `Date`,
162
+ `RegExp`, class instance, custom-prototype object, or other malformed top-level value is
163
+ **unknown inventory** and fails closed with `candidate-artifacts-invalid`; an explicit `{}` or
164
+ an empty null-prototype dictionary remains the legitimate empty deletion inventory. This
165
+ provenance check is limited to obligations that actually declare baseline paths, and it does
166
+ not change the shared `isPlainObject` normalizer used by callers without
167
+ `scope.baselinePaths`. The prototype is read only for those applicable baseline-only
168
+ obligations; a value whose prototype inspection throws (for example a `Proxy` with a throwing
169
+ `getPrototypeOf` trap) is likewise unknown inventory, and unrelated non-baseline or
170
+ inapplicable obligations never trigger the read. The same boundary is exception-safe for
171
+ own-property inspection: a throwing property-descriptor trap or a throwing nested `digest`
172
+ accessor encountered during metadata validation or the pre-binding capture is classified as
173
+ unknown inventory (`candidate-artifacts-invalid` for applicable baseline-only obligations)
174
+ instead of escaping the checker. Inspecting a `baselinePaths` entry itself is guarded the same
175
+ way: an entry whose `path`, `reason`, or `renameTo` field getter or descriptor trap throws is a
176
+ `baseline-path-invalid` metadata error, not an escape; the audit mirror is built from the
177
+ validated snapshot and contains no second read of the raw entry, so an accessor that only
178
+ misbehaves after the guarded snapshot is never reached. Inspection is not repeated after
179
+ validation for a validated baseline-only path or a rename destination, so a trap that only
180
+ starts throwing on a later re-inspection is never reached. `N/A` and `unknown` obligations never
181
+ emit that diagnostic.
182
+ - A mixed scope can combine baseline-only paths with ordinary candidate paths (for example one
183
+ document deleted and another edited in the same change set). For an applicable obligation
184
+ that declares baseline paths, every ordinary scoped candidate digest is captured once before
185
+ evidence binding; binding, the `artifactDigest` audit identity, and the stale-digest
186
+ diagnostics all use that same captured snapshot, so a nested digest accessor whose later read
187
+ throws or returns a different value cannot make the audit key or a stale verdict disagree with
188
+ the digest that bound evidence. This includes the rename destination: after metadata proves the
189
+ destination is an own-key inventory entry with its own digest, the captured snapshot is seeded
190
+ with that validated own digest and the destination candidate map is not read again during
191
+ capture, so a stale pair plus a destination-only record cannot jointly pass. Declared
192
+ baseline-only paths were validated absent during metadata and are not read again either. A
193
+ throwing accessor on one of the remaining ordinary paths during capture also fails the candidate
194
+ inventory closed. Callers without `scope.baselinePaths` keep the legacy candidate-binding
195
+ behavior unchanged, including the legacy `candidateBound` read path.
196
+
197
+ ## Fail-closed cases
198
+
199
+ The result is not a pass when any of the following is observed:
200
+
201
+ - absent whole-baseline identity, absent `baseline.artifacts`, or an invalid/missing per-path
202
+ baseline value for a declared baseline-only path;
203
+ - a baseline-only entry that is missing from `scope.paths`, is duplicated, has an unknown reason,
204
+ has a malformed `renameTo` (empty, equal to the source, outside `scope.paths`, or — for an
205
+ applicable obligation — without an own-key candidate destination artifact that declares its own
206
+ `digest`), or whose field inspection fails (a `path`, `reason`, or `renameTo` getter or
207
+ descriptor trap that throws during the guarded entry snapshot);
208
+ - a deleted entry that carries a `renameTo` property at all, including an explicitly present
209
+ `undefined`;
210
+ - a present `scope.baselinePaths` annotation that is not an array, including an explicitly
211
+ present `undefined`; absence of the property remains backward-compatible;
212
+ - an applicable obligation that declares baseline-only paths but receives an absent or malformed
213
+ top-level candidate `artifacts` map (omitted, `undefined`, `null`, array, string, or an
214
+ object-like value that is not a plain object or null-prototype dictionary, including `Map`,
215
+ `Date`, `RegExp`, class instances, objects whose prototype cannot be inspected, and inventories
216
+ whose own-property descriptors or nested digest accessors throw); an explicit `{}` or
217
+ null-prototype dictionary is valid;
218
+ - a mixed baseline+ordinary scope whose ordinary candidate digest accessor throws while the
219
+ candidate inventory is captured before binding;
220
+ - a rename pair whose destination digest differs from the captured destination candidate digest,
221
+ including a stale pair that a separate destination-only record would otherwise leave jointly
222
+ passing;
223
+ - a rename destination whose validated own digest conflicts with another accepted entry that
224
+ shares the same `renameTo`, or whose captured own value is not a usable non-empty string;
225
+ - a baseline-only path with an own-key candidate artifact map entry, even when that entry has no
226
+ usable digest (`{}`, `{ digest: '' }`, or `null`);
227
+ - a record that carries an `artifactDigests` entry (real, empty, or fake) for a baseline-only
228
+ path;
229
+ - a rename destination with no candidate artifact (applicable obligations only; `N/A` and
230
+ `unknown` obligations keep the legacy candidate read for their audit identity);
231
+ - a record with the wrong baseline identity, the wrong per-path baseline digest, an absent or
232
+ empty `baselineArtifacts` entry, or stale/missing endpoint evidence — where "wrong" is judged
233
+ against the validated baseline value captured during metadata, not a live re-read;
234
+ - a baseline per-path digest whose inspection throws (a getter or descriptor trap), which is a
235
+ `baseline-artifact-invalid` metadata error rather than an escape;
236
+ - a stateful `baselinePaths[].path` accessor whose later read names a different path: the audit
237
+ mirror is keyed by the validated path snapshot, so a pass can never describe a different
238
+ baseline identity than the one that validated the evidence;
239
+ - an unsupported review kind, an unapproved review, or an incomplete execution record.
240
+
241
+ These cases are visible in diagnostics and remain `outcome: 'none'` unless the failure is a
242
+ correctly bound command, which remains `outcome: 'fail'` as before.
243
+
244
+ ## Audit output
245
+
246
+ Results keep the existing `artifactDigest`, `baseDigest`, `provenance`, and `recordRefs` fields.
247
+ Obligations that declare baseline paths additionally expose `baselineArtifacts`, mapping each
248
+ validated baseline-only path to the trusted baseline value used for the check. The field is
249
+ additive; callers that do not use baseline binding see unchanged output.
@@ -11,8 +11,8 @@ Documentation is a **consumer** of the #320 workflow/obligation abstractions. No
11
11
  | 0 — contract + RED fixtures | This file; fixtures under `ci/fixtures/doc-lifecycle/` |
12
12
  | 1a — `project-docs.json` loader | `multi-agent-workspace/scripts/project-docs-resolver.js` |
13
13
  | 2 — roles, IDs, aliases, legacy, supersession, index, D6.9 PRD revisions | `harness-everything/scripts/doc-lifecycle.js` |
14
- | 3a — `doc-change` / `requirement-sync` pure derivation and evidence producer | `harness-everything/scripts/doc-obligations.js` (facts and obligations), `harness-everything/scripts/doc-evidence.js` (`checkDocObligations`). Pilot-local: not wired into workflow disposition yet. H3 and H4 have deterministic tests; H1 and H2 are deferred to 3c. Known limits: the attested-envelope fields are caller-asserted (`configDigest` is checked for presence only, not against the parsed config); `candidateDigest` covers only `docs/prd/`, so custom PRD roots and task-scope binding are not yet verified; deleted and renamed paths fail closed (#335) |
15
- | 1b, 3b–3c (provider suggestions, reconciliation, H1–H2 evaluation) | Not started: blocked by #320 family metadata, D1 obligation schema and D3 evidence states. Known gap: deleted and old rename paths cannot get candidate-bound evidence, so their MUSTs fail closed |
14
+ | 3a — `doc-change` / `requirement-sync` pure derivation and evidence producer | `harness-everything/scripts/doc-obligations.js` (facts and obligations), `harness-everything/scripts/doc-evidence.js` (`checkDocObligations`). Pilot-local: not wired into workflow disposition yet. H3 and H4 have deterministic tests; H1 and H2 are deferred to 3c. Known limits: the attested-envelope fields are caller-asserted (`configDigest` is checked for presence only, not against the parsed config); `candidateDigest` covers only `docs/prd/`, so custom PRD roots and task-scope binding are not yet verified; deleted and old rename paths are bound to trusted per-path baseline evidence ([doc-evidence-baseline-binding.md](doc-evidence-baseline-binding.md), #335), with reconciliation left out of scope |
15
+ | 1b, 3b–3c (provider suggestions, reconciliation, H1–H2 evaluation) | Not started: blocked by #320 family metadata, D1 obligation schema and D3 evidence states. Deleted and old rename paths now use trusted baseline-bound evidence (#335); reconciliation with workflow disposition remains open |
16
16
  | 4 — derived-view generators, `describes:` drift | Not started |
17
17
 
18
18
  ## Configuration (D5)
@@ -4,7 +4,7 @@ description: "Route software/project work through Harness: classify tier/topolog
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.35.0
7
+ version: 0.36.1
8
8
  ---
9
9
 
10
10
  # Harness Everything