harness-everything 0.36.1 → 0.36.3
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +17 -0
- package/ci/mechanism-2aw-project-docs-config.test.js +450 -10
- package/ci/mechanism-335-baseline-evidence.test.js +355 -7
- package/docs/doc-evidence-baseline-binding.md +28 -7
- package/docs/document-lifecycle.md +60 -8
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-evidence.js +69 -9
- package/harness-everything/scripts/project-docs-resolver.js +30 -13
- package/multi-agent-workspace/SKILL.md +1 -1
- package/multi-agent-workspace/scripts/project-docs-resolver.js +30 -13
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +69 -9
- package/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js +30 -13
- package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +1 -1
- package/plugins/harness-everything/skills/multi-agent-workspace/scripts/project-docs-resolver.js +30 -13
- package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-spec/scripts/check-project-docs.js +136 -4
- package/plugins/harness-everything/skills/to-spec/scripts/project-docs-resolver.js +30 -13
- package/sbom.cdx.json +7 -7
- package/to-spec/SKILL.md +1 -1
- package/to-spec/scripts/check-project-docs.js +136 -4
- package/to-spec/scripts/project-docs-resolver.js +30 -13
|
@@ -1877,7 +1877,9 @@ checkCase('335 P3 the metadata-validated rename destination digest is carried wi
|
|
|
1877
1877
|
for (const target of [modules, packageModules]) {
|
|
1878
1878
|
const label = target === modules ? 'canonical' : 'packaged';
|
|
1879
1879
|
const ctx = prepare('rename', target);
|
|
1880
|
-
|
|
1880
|
+
// P3(r4321077631): the destination snapshot reads its own digest exactly once, so a getter
|
|
1881
|
+
// that throws after that single valid read is never reached and the captured B still binds.
|
|
1882
|
+
const counted = countingDigestEntry(RENAME_CAPTURED, { validReads: 1, later: 'throw' });
|
|
1881
1883
|
const artifacts = { ...ctx.artifacts, [RENAME_TO]: counted.entry };
|
|
1882
1884
|
const record = execRecord(ctx, 'pair-run', 'doc-change:doc-lint', {
|
|
1883
1885
|
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [], baselinePaths: [RENAME_FROM],
|
|
@@ -1892,12 +1894,74 @@ checkCase('335 P3 the metadata-validated rename destination digest is carried wi
|
|
|
1892
1894
|
if (!result.artifactDigest.includes(`${RENAME_TO}=${RENAME_CAPTURED}`)) {
|
|
1893
1895
|
failures.push(`${label}: audit=${result.artifactDigest}`);
|
|
1894
1896
|
}
|
|
1895
|
-
if (counted.reads() !==
|
|
1897
|
+
if (counted.reads() !== 1) failures.push(`${label}: destination reads=${counted.reads()}`);
|
|
1896
1898
|
}
|
|
1897
1899
|
assert.deepStrictEqual(failures, [],
|
|
1898
1900
|
`the validated destination digest must be carried: ${failures.join('; ')}`);
|
|
1899
1901
|
});
|
|
1900
1902
|
|
|
1903
|
+
// #342 review P3: a destination snapshot rejected during metadata (throwing or unusable own
|
|
1904
|
+
// digest) is still a visited snapshot. The capture and audit lanes must never re-read it: the
|
|
1905
|
+
// single metadata read stays the only digest read, and a stateful getter cannot ship a later
|
|
1906
|
+
// value into the audit identity even though its entry failed.
|
|
1907
|
+
checkCase('#342 P3 a rejected destination snapshot is read exactly once even when its digest getter throws', () => {
|
|
1908
|
+
const failures = [];
|
|
1909
|
+
for (const target of [modules, packageModules]) {
|
|
1910
|
+
const label = target === modules ? 'canonical' : 'packaged';
|
|
1911
|
+
const ctx = prepare('rename', target);
|
|
1912
|
+
const counted = countingDigestEntry(RENAME_CAPTURED, { validReads: 0, later: 'throw' });
|
|
1913
|
+
const artifacts = { ...ctx.artifacts, [RENAME_TO]: counted.entry };
|
|
1914
|
+
const record = execRecord(ctx, 'move-run', 'doc-change:doc-lint', {
|
|
1915
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM]
|
|
1916
|
+
});
|
|
1917
|
+
let out;
|
|
1918
|
+
try {
|
|
1919
|
+
out = runDocLint(ctx, target, { artifacts, records: [record] });
|
|
1920
|
+
} catch (error) { failures.push(`${label}: escaped: ${error.message}`); continue; }
|
|
1921
|
+
const result = resultOf(out, 'doc-change:doc-lint');
|
|
1922
|
+
if (result.outcome !== 'none') failures.push(`${label}: outcome=${result.outcome}`);
|
|
1923
|
+
if (!diagnosticCodes(out).includes('candidate-artifacts-invalid')) {
|
|
1924
|
+
failures.push(`${label}: diagnostics=${diagnosticCodes(out)}`);
|
|
1925
|
+
}
|
|
1926
|
+
if (counted.reads() !== 1) failures.push(`${label}: destination reads=${counted.reads()}`);
|
|
1927
|
+
if (!result.artifactDigest.includes(`${RENAME_TO}=missing`)) {
|
|
1928
|
+
failures.push(`${label}: audit=${result.artifactDigest}`);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
assert.deepStrictEqual(failures, [],
|
|
1932
|
+
`a rejected throwing destination must be read once and never again: ${failures.join('; ')}`);
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
checkCase('#342 P3 an unusable first destination read never ships a later non-empty value into the audit', () => {
|
|
1936
|
+
const failures = [];
|
|
1937
|
+
for (const target of [modules, packageModules]) {
|
|
1938
|
+
const label = target === modules ? 'canonical' : 'packaged';
|
|
1939
|
+
const ctx = prepare('rename', target);
|
|
1940
|
+
// First read: unusable ''. Any later read (which must not happen) returns non-empty
|
|
1941
|
+
// RENAME_LATER, so the audit would describe it if the rejected snapshot were re-read.
|
|
1942
|
+
const counted = countingDigestEntry('', { validReads: 1, later: 'value', laterValue: RENAME_LATER });
|
|
1943
|
+
const artifacts = { ...ctx.artifacts, [RENAME_TO]: counted.entry };
|
|
1944
|
+
const record = execRecord(ctx, 'move-run', 'doc-change:doc-lint', {
|
|
1945
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM]
|
|
1946
|
+
});
|
|
1947
|
+
let out;
|
|
1948
|
+
try {
|
|
1949
|
+
out = runDocLint(ctx, target, { artifacts, records: [record] });
|
|
1950
|
+
} catch (error) { failures.push(`${label}: escaped: ${error.message}`); continue; }
|
|
1951
|
+
const result = resultOf(out, 'doc-change:doc-lint');
|
|
1952
|
+
if (result.outcome !== 'none') failures.push(`${label}: outcome=${result.outcome}`);
|
|
1953
|
+
if (counted.reads() !== 1) failures.push(`${label}: destination reads=${counted.reads()}`);
|
|
1954
|
+
if (result.artifactDigest.includes(RENAME_LATER)) {
|
|
1955
|
+
failures.push(`${label}: audit describes a later value: ${result.artifactDigest}`);
|
|
1956
|
+
}
|
|
1957
|
+
if (!result.artifactDigest.includes(`${RENAME_TO}=missing`)) {
|
|
1958
|
+
failures.push(`${label}: audit=${result.artifactDigest}`);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
assert.deepStrictEqual(failures, [],
|
|
1962
|
+
`an unusable destination read must stay single-read and 'missing': ${failures.join('; ')}`);
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1901
1965
|
checkCase('335 P3 a stale pair plus a destination-only record stays none under descriptor patience', () => {
|
|
1902
1966
|
const failures = [];
|
|
1903
1967
|
for (const target of [modules, packageModules]) {
|
|
@@ -2116,10 +2180,12 @@ checkCase('335 P3 two accepted entries sharing a rename destination must agree o
|
|
|
2116
2180
|
}
|
|
2117
2181
|
}
|
|
2118
2182
|
|
|
2119
|
-
// Dynamic shared destination: D1 for the first entry's
|
|
2183
|
+
// Dynamic shared destination: D1 for the first entry's single-read snapshot, D2 for the
|
|
2184
|
+
// second (the per-entry read advances the getter). P3(r4321077631) made each entry's read
|
|
2185
|
+
// exactly one, so the fixture flips after one valid read.
|
|
2120
2186
|
{
|
|
2121
2187
|
const { ctx } = sharedDestinationContext(target);
|
|
2122
|
-
const dynamic = countingDigestEntry(RENAME_CAPTURED, { validReads:
|
|
2188
|
+
const dynamic = countingDigestEntry(RENAME_CAPTURED, { validReads: 1, later: 'value', laterValue: RENAME_LATER });
|
|
2123
2189
|
const artifacts = { ...ctx.artifacts, [RENAME_TO]: dynamic.entry };
|
|
2124
2190
|
const out = run(artifacts, [
|
|
2125
2191
|
sharedPairRecord(ctx, 'pair-run', RENAME_CAPTURED),
|
|
@@ -2177,6 +2243,69 @@ checkCase('335 P3 two accepted entries sharing a rename destination must agree o
|
|
|
2177
2243
|
`shared rename destinations must agree on the validated digest: ${failures.join('; ')}`);
|
|
2178
2244
|
});
|
|
2179
2245
|
|
|
2246
|
+
// #342 review P3: a shared rename destination whose snapshot is rejected must stay honored.
|
|
2247
|
+
// invalid→valid: the first declaring entry fails its own snapshot; a later declaring entry must
|
|
2248
|
+
// fail closed without reading the destination again (one read total, audit 'missing').
|
|
2249
|
+
// valid→throw: the first entry carries a validated success; the second declaration's read
|
|
2250
|
+
// throws, and the poisoned visited state wins in the audit over the previously seeded success
|
|
2251
|
+
// (one read per declaring entry, two total).
|
|
2252
|
+
checkCase('#342 P3 a rejected shared destination is never read again and keeps audit precedence', () => {
|
|
2253
|
+
const failures = [];
|
|
2254
|
+
for (const target of [modules, packageModules]) {
|
|
2255
|
+
const label = target === modules ? 'canonical' : 'packaged';
|
|
2256
|
+
|
|
2257
|
+
// invalid→valid: first own read unusable, later reads (which must not happen) valid.
|
|
2258
|
+
{
|
|
2259
|
+
const { ctx, obligations, baseline } = sharedDestinationContext(target);
|
|
2260
|
+
const counted = countingDigestEntry('', { validReads: 1, later: 'value', laterValue: RENAME_CAPTURED });
|
|
2261
|
+
// Read counts are scoped to the obligation under test; unrelated obligations keep the
|
|
2262
|
+
// designed legacy candidate read, exactly as runDocLint's single-obligation runs do.
|
|
2263
|
+
const out = target.checkDocObligations({
|
|
2264
|
+
obligations: obligations.filter(item => item.id === 'doc-change:doc-lint'),
|
|
2265
|
+
artifacts: { ...ctx.artifacts, [RENAME_TO]: counted.entry }, baseline,
|
|
2266
|
+
executionRecords: [sharedPairRecord(ctx, 'pair-run', RENAME_CAPTURED)], reviewRecords: []
|
|
2267
|
+
});
|
|
2268
|
+
const result = resultOf(out, 'doc-change:doc-lint');
|
|
2269
|
+
if (result.outcome !== 'none') failures.push(`${label}/invalid-to-valid: outcome=${result.outcome}`);
|
|
2270
|
+
if (!diagnosticCodes(out).includes('baseline-path-invalid')) {
|
|
2271
|
+
failures.push(`${label}/invalid-to-valid: diagnostics=${diagnosticCodes(out)}`);
|
|
2272
|
+
}
|
|
2273
|
+
if (counted.reads() !== 1) failures.push(`${label}/invalid-to-valid: destination reads=${counted.reads()}`);
|
|
2274
|
+
if (result.artifactDigest.includes(RENAME_CAPTURED)) {
|
|
2275
|
+
failures.push(`${label}/invalid-to-valid: audit describes a later value: ${result.artifactDigest}`);
|
|
2276
|
+
}
|
|
2277
|
+
if (!result.artifactDigest.includes(`${RENAME_TO}=missing`)) {
|
|
2278
|
+
failures.push(`${label}/invalid-to-valid: audit=${result.artifactDigest}`);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
// valid→throw: successful first snapshot, then a throwing own read on the second entry.
|
|
2283
|
+
{
|
|
2284
|
+
const { ctx, obligations, baseline } = sharedDestinationContext(target);
|
|
2285
|
+
const counted = countingDigestEntry(RENAME_CAPTURED, { validReads: 1, later: 'throw' });
|
|
2286
|
+
const out = target.checkDocObligations({
|
|
2287
|
+
obligations: obligations.filter(item => item.id === 'doc-change:doc-lint'),
|
|
2288
|
+
artifacts: { ...ctx.artifacts, [RENAME_TO]: counted.entry }, baseline,
|
|
2289
|
+
executionRecords: [sharedPairRecord(ctx, 'pair-run', RENAME_CAPTURED)], reviewRecords: []
|
|
2290
|
+
});
|
|
2291
|
+
const result = resultOf(out, 'doc-change:doc-lint');
|
|
2292
|
+
if (result.outcome !== 'none') failures.push(`${label}/valid-to-throw: outcome=${result.outcome}`);
|
|
2293
|
+
if (!diagnosticCodes(out).includes('candidate-artifacts-invalid')) {
|
|
2294
|
+
failures.push(`${label}/valid-to-throw: diagnostics=${diagnosticCodes(out)}`);
|
|
2295
|
+
}
|
|
2296
|
+
if (counted.reads() !== 2) failures.push(`${label}/valid-to-throw: destination reads=${counted.reads()}`);
|
|
2297
|
+
if (result.artifactDigest.includes(RENAME_CAPTURED)) {
|
|
2298
|
+
failures.push(`${label}/valid-to-throw: audit describes a rejected-state value: ${result.artifactDigest}`);
|
|
2299
|
+
}
|
|
2300
|
+
if (!result.artifactDigest.includes(`${RENAME_TO}=missing`)) {
|
|
2301
|
+
failures.push(`${label}/valid-to-throw: audit=${result.artifactDigest}`);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
assert.deepStrictEqual(failures, [],
|
|
2306
|
+
`a rejected shared destination must stay single-read and audited as 'missing': ${failures.join('; ')}`);
|
|
2307
|
+
});
|
|
2308
|
+
|
|
2180
2309
|
// ---------------------------------------------------------------------------
|
|
2181
2310
|
// P2 applicability-gated destination snapshot (review r4318520818 at 56f3094):
|
|
2182
2311
|
// N/A and unknown obligations never bind evidence, so their destination digest
|
|
@@ -2220,10 +2349,11 @@ checkCase('335 P2 an inapplicable rename never consumes the destination snapshot
|
|
|
2220
2349
|
}
|
|
2221
2350
|
|
|
2222
2351
|
// Positive control: the applicable obligation still captures the destination snapshot
|
|
2223
|
-
// once during metadata and binds against it.
|
|
2352
|
+
// once during metadata and binds against it. P3(r4321077631) made that capture a
|
|
2353
|
+
// single guarded read, so the getter is never consumed beyond its first valid read.
|
|
2224
2354
|
{
|
|
2225
2355
|
const ctx = prepare('rename', target);
|
|
2226
|
-
const counted = countingDigestEntry(RENAME_CAPTURED, { validReads:
|
|
2356
|
+
const counted = countingDigestEntry(RENAME_CAPTURED, { validReads: 1, later: 'throw' });
|
|
2227
2357
|
const artifacts = { ...ctx.artifacts, [RENAME_TO]: counted.entry };
|
|
2228
2358
|
const record = {
|
|
2229
2359
|
...execRecord(ctx, 'pair-run', 'doc-change:doc-lint', {
|
|
@@ -2235,7 +2365,7 @@ checkCase('335 P2 an inapplicable rename never consumes the destination snapshot
|
|
|
2235
2365
|
const out = checkArtifacts(ctx, artifacts, { obligations: [obligation], executionRecords: [record] }, target);
|
|
2236
2366
|
const result = resultOf(out, 'doc-change:doc-lint');
|
|
2237
2367
|
if (result.outcome !== 'pass') failures.push(`${label}/applicable: outcome=${result.outcome}`);
|
|
2238
|
-
if (counted.reads() !==
|
|
2368
|
+
if (counted.reads() !== 1) failures.push(`${label}/applicable: destination getter reads=${counted.reads()}`);
|
|
2239
2369
|
}
|
|
2240
2370
|
}
|
|
2241
2371
|
assert.deepStrictEqual(failures, [],
|
|
@@ -2687,4 +2817,222 @@ checkCase('335 P2 the audit mirror reports the metadata-validated path snapshot,
|
|
|
2687
2817
|
`the audit mirror must use the validated path snapshot: ${failures.join('; ')}`);
|
|
2688
2818
|
});
|
|
2689
2819
|
|
|
2820
|
+
//----------------------------------------------------------------------------
|
|
2821
|
+
// P2(r4321077631/r4321077638) at 7217b3a: whole-baseline identity and rename
|
|
2822
|
+
// destination digests must each be read exactly once — the same scalar is
|
|
2823
|
+
// validated and captured, so a stateful getter cannot pass validation as one
|
|
2824
|
+
// value while evidence binds its later value.
|
|
2825
|
+
//----------------------------------------------------------------------------
|
|
2826
|
+
|
|
2827
|
+
const RENAME_OLD = 'docs/adr/old.md';
|
|
2828
|
+
const RENAME_NEW = 'docs/adr/new.md';
|
|
2829
|
+
const RENAME_BASELINE_DIGEST = sha256('rename whole baseline');
|
|
2830
|
+
const RENAME_OLD_BASELINE = sha256('old document baseline content');
|
|
2831
|
+
const RENAME_DEST_B = sha256('destination content B');
|
|
2832
|
+
const RENAME_DEST_C = sha256('destination content C');
|
|
2833
|
+
|
|
2834
|
+
checkCase('335 P2 the whole-baseline digest is validated and captured from one read', () => {
|
|
2835
|
+
const failures = [];
|
|
2836
|
+
for (const target of [modules, packageModules]) {
|
|
2837
|
+
const label = target === modules ? 'canonical' : 'packaged';
|
|
2838
|
+
const PATH = BASELINE_EXAMPLE;
|
|
2839
|
+
|
|
2840
|
+
function runWith(baseline, recordBaseDigest) {
|
|
2841
|
+
return target.checkDocObligations({
|
|
2842
|
+
obligations: [{
|
|
2843
|
+
id: 'doc-change:doc-lint',
|
|
2844
|
+
capability: 'doc-lint',
|
|
2845
|
+
level: 'MUST',
|
|
2846
|
+
applicability: 'applicable',
|
|
2847
|
+
scope: { paths: [PATH], baselinePaths: [{ path: PATH, reason: 'deleted' }] }
|
|
2848
|
+
}],
|
|
2849
|
+
artifacts: {},
|
|
2850
|
+
baseline,
|
|
2851
|
+
executionRecords: [{
|
|
2852
|
+
id: 'tomb-run',
|
|
2853
|
+
obligationId: 'doc-change:doc-lint',
|
|
2854
|
+
command: 'node ci/mechanism-335-baseline-evidence.test.js # tomb-run',
|
|
2855
|
+
exitCode: 0,
|
|
2856
|
+
provenance: 'baseline identity snapshot regression',
|
|
2857
|
+
baseDigest: recordBaseDigest,
|
|
2858
|
+
paths: [PATH],
|
|
2859
|
+
artifactDigests: {},
|
|
2860
|
+
baselineArtifacts: { [PATH]: BASELINE_EXAMPLE_VALUE }
|
|
2861
|
+
}],
|
|
2862
|
+
reviewRecords: []
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
// Stateful identity getter: validation must use the first read; a record attesting to the
|
|
2867
|
+
// later value cannot pass.
|
|
2868
|
+
{
|
|
2869
|
+
let identityReads = 0;
|
|
2870
|
+
const baseline = {
|
|
2871
|
+
get digest() {
|
|
2872
|
+
identityReads += 1;
|
|
2873
|
+
return identityReads === 1 ? 'baseline-A' : 'baseline-B';
|
|
2874
|
+
},
|
|
2875
|
+
artifacts: { [PATH]: BASELINE_EXAMPLE_VALUE }
|
|
2876
|
+
};
|
|
2877
|
+
let out;
|
|
2878
|
+
try {
|
|
2879
|
+
out = runWith(baseline, 'baseline-B');
|
|
2880
|
+
} catch (error) {
|
|
2881
|
+
failures.push(`${label}/stateful: escaped: ${error.message}`);
|
|
2882
|
+
continue;
|
|
2883
|
+
}
|
|
2884
|
+
const result = out.results.find(item => item.obligationId === 'doc-change:doc-lint');
|
|
2885
|
+
if (identityReads !== 1) failures.push(`${label}/stateful: identity getter reads=${identityReads}`);
|
|
2886
|
+
if (result.outcome === 'pass') failures.push(`${label}/stateful: a record bound to the later value passed`);
|
|
2887
|
+
if (!out.diagnostics.some(item => item.code === 'stale-base-digest')
|
|
2888
|
+
&& !out.diagnostics.some(item => item.code === 'baseline-identity-missing')) {
|
|
2889
|
+
failures.push(`${label}/stateful: diagnostics=${out.diagnostics.map(item => item.code)}`);
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
// Stable controls: the validated identity binds; a different identity does not.
|
|
2894
|
+
{
|
|
2895
|
+
const passing = runWith(
|
|
2896
|
+
{ digest: 'baseline-A', artifacts: { [PATH]: BASELINE_EXAMPLE_VALUE } }, 'baseline-A');
|
|
2897
|
+
if (passing.results.find(item => item.obligationId === 'doc-change:doc-lint').outcome !== 'pass') {
|
|
2898
|
+
failures.push(`${label}/stable: the validated identity did not pass`);
|
|
2899
|
+
}
|
|
2900
|
+
const stale = runWith(
|
|
2901
|
+
{ digest: 'baseline-B', artifacts: { [PATH]: BASELINE_EXAMPLE_VALUE } }, 'baseline-A');
|
|
2902
|
+
const staleResult = stale.results.find(item => item.obligationId === 'doc-change:doc-lint');
|
|
2903
|
+
if (staleResult.outcome !== 'none') failures.push(`${label}/stable: outcome=${staleResult.outcome}`);
|
|
2904
|
+
if (!stale.diagnostics.some(item => item.code === 'stale-base-digest')) {
|
|
2905
|
+
failures.push(`${label}/stable: diagnostics=${stale.diagnostics.map(item => item.code)}`);
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
// A throwing identity getter fails closed as a metadata error instead of escaping.
|
|
2910
|
+
{
|
|
2911
|
+
const baseline = {
|
|
2912
|
+
get digest() {
|
|
2913
|
+
throw new Error('baseline identity getter');
|
|
2914
|
+
},
|
|
2915
|
+
artifacts: { [PATH]: BASELINE_EXAMPLE_VALUE }
|
|
2916
|
+
};
|
|
2917
|
+
let out;
|
|
2918
|
+
try {
|
|
2919
|
+
out = runWith(baseline, 'baseline-A');
|
|
2920
|
+
} catch (error) {
|
|
2921
|
+
failures.push(`${label}/throwing: escaped: ${error.message}`);
|
|
2922
|
+
continue;
|
|
2923
|
+
}
|
|
2924
|
+
const result = out.results.find(item => item.obligationId === 'doc-change:doc-lint');
|
|
2925
|
+
if (result.outcome === 'pass') failures.push(`${label}/throwing: a throwing identity produced a pass`);
|
|
2926
|
+
if (!out.diagnostics.some(item => item.code === 'baseline-identity-missing')) {
|
|
2927
|
+
failures.push(`${label}/throwing: diagnostics=${out.diagnostics.map(item => item.code)}`);
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
assert.deepStrictEqual(failures, [],
|
|
2932
|
+
`the whole-baseline identity must be captured once: ${failures.join('; ')}`);
|
|
2933
|
+
});
|
|
2934
|
+
|
|
2935
|
+
checkCase('335 P2 the rename destination digest is validated and captured from one read', () => {
|
|
2936
|
+
const failures = [];
|
|
2937
|
+
for (const target of [modules, packageModules]) {
|
|
2938
|
+
const label = target === modules ? 'canonical' : 'packaged';
|
|
2939
|
+
const obligation = {
|
|
2940
|
+
id: 'doc-change:doc-lint',
|
|
2941
|
+
capability: 'doc-lint',
|
|
2942
|
+
level: 'MUST',
|
|
2943
|
+
applicability: 'applicable',
|
|
2944
|
+
scope: {
|
|
2945
|
+
paths: [RENAME_OLD, RENAME_NEW],
|
|
2946
|
+
baselinePaths: [{ path: RENAME_OLD, reason: 'rename-from', renameTo: RENAME_NEW }]
|
|
2947
|
+
}
|
|
2948
|
+
};
|
|
2949
|
+
const baseline = {
|
|
2950
|
+
digest: RENAME_BASELINE_DIGEST,
|
|
2951
|
+
artifacts: { [RENAME_OLD]: RENAME_OLD_BASELINE }
|
|
2952
|
+
};
|
|
2953
|
+
|
|
2954
|
+
function runWith(destinationEntry, recordDestinationDigest) {
|
|
2955
|
+
return target.checkDocObligations({
|
|
2956
|
+
obligations: [obligation],
|
|
2957
|
+
artifacts: { [RENAME_NEW]: destinationEntry },
|
|
2958
|
+
baseline,
|
|
2959
|
+
executionRecords: [{
|
|
2960
|
+
id: 'rename-run',
|
|
2961
|
+
obligationId: 'doc-change:doc-lint',
|
|
2962
|
+
command: 'node ci/mechanism-335-baseline-evidence.test.js # rename-run',
|
|
2963
|
+
exitCode: 0,
|
|
2964
|
+
provenance: 'rename destination snapshot regression',
|
|
2965
|
+
baseDigest: RENAME_BASELINE_DIGEST,
|
|
2966
|
+
paths: [RENAME_OLD, RENAME_NEW],
|
|
2967
|
+
artifactDigests: { [RENAME_NEW]: recordDestinationDigest },
|
|
2968
|
+
baselineArtifacts: { [RENAME_OLD]: RENAME_OLD_BASELINE }
|
|
2969
|
+
}],
|
|
2970
|
+
reviewRecords: []
|
|
2971
|
+
});
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
// Stateful destination digest getter: validation must use the first read B; a record
|
|
2975
|
+
// carrying only the later C cannot pass.
|
|
2976
|
+
{
|
|
2977
|
+
let destinationReads = 0;
|
|
2978
|
+
const statefulDestination = {
|
|
2979
|
+
get digest() {
|
|
2980
|
+
destinationReads += 1;
|
|
2981
|
+
return destinationReads === 1 ? RENAME_DEST_B : RENAME_DEST_C;
|
|
2982
|
+
}
|
|
2983
|
+
};
|
|
2984
|
+
let out;
|
|
2985
|
+
try {
|
|
2986
|
+
out = runWith(statefulDestination, RENAME_DEST_C);
|
|
2987
|
+
} catch (error) {
|
|
2988
|
+
failures.push(`${label}/stateful: escaped: ${error.message}`);
|
|
2989
|
+
continue;
|
|
2990
|
+
}
|
|
2991
|
+
const result = out.results.find(item => item.obligationId === 'doc-change:doc-lint');
|
|
2992
|
+
if (destinationReads !== 1) failures.push(`${label}/stateful: destination digest reads=${destinationReads}`);
|
|
2993
|
+
if (result.outcome === 'pass') failures.push(`${label}/stateful: a record bound to the later value passed`);
|
|
2994
|
+
if (!out.diagnostics.some(item => item.code === 'stale-artifact-digest')) {
|
|
2995
|
+
failures.push(`${label}/stateful: diagnostics=${out.diagnostics.map(item => item.code)}`);
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
// Stable controls: the validated destination binds its own value; a different value is stale.
|
|
3000
|
+
{
|
|
3001
|
+
const passing = runWith({ digest: RENAME_DEST_B }, RENAME_DEST_B);
|
|
3002
|
+
if (passing.results.find(item => item.obligationId === 'doc-change:doc-lint').outcome !== 'pass') {
|
|
3003
|
+
failures.push(`${label}/stable: the validated destination did not pass; diagnostics=${passing.diagnostics.map(item => item.code)}`);
|
|
3004
|
+
}
|
|
3005
|
+
const stale = runWith({ digest: RENAME_DEST_B }, RENAME_DEST_C);
|
|
3006
|
+
const staleResult = stale.results.find(item => item.obligationId === 'doc-change:doc-lint');
|
|
3007
|
+
if (staleResult.outcome !== 'none') failures.push(`${label}/stable: outcome=${staleResult.outcome}`);
|
|
3008
|
+
if (!stale.diagnostics.some(item => item.code === 'stale-artifact-digest')) {
|
|
3009
|
+
failures.push(`${label}/stable: diagnostics=${stale.diagnostics.map(item => item.code)}`);
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
// A throwing destination digest getter fails closed instead of escaping.
|
|
3014
|
+
{
|
|
3015
|
+
const throwingDestination = {
|
|
3016
|
+
get digest() {
|
|
3017
|
+
throw new Error('destination digest getter');
|
|
3018
|
+
}
|
|
3019
|
+
};
|
|
3020
|
+
let out;
|
|
3021
|
+
try {
|
|
3022
|
+
out = runWith(throwingDestination, RENAME_DEST_B);
|
|
3023
|
+
} catch (error) {
|
|
3024
|
+
failures.push(`${label}/throwing: escaped: ${error.message}`);
|
|
3025
|
+
continue;
|
|
3026
|
+
}
|
|
3027
|
+
const result = out.results.find(item => item.obligationId === 'doc-change:doc-lint');
|
|
3028
|
+
if (result.outcome === 'pass') failures.push(`${label}/throwing: a throwing destination produced a pass`);
|
|
3029
|
+
if (!out.diagnostics.some(item => item.code === 'candidate-artifacts-invalid')) {
|
|
3030
|
+
failures.push(`${label}/throwing: diagnostics=${out.diagnostics.map(item => item.code)}`);
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
assert.deepStrictEqual(failures, [],
|
|
3035
|
+
`the rename destination digest must be captured once: ${failures.join('; ')}`);
|
|
3036
|
+
});
|
|
3037
|
+
|
|
2690
3038
|
helper.finish();
|
|
@@ -82,10 +82,16 @@ after validation, cannot mute the stale-baseline diagnostic, and cannot drift th
|
|
|
82
82
|
Its first read must therefore be the value every consumer sees; a getter that starts returning a
|
|
83
83
|
different or unusable value later is factored out entirely. A baseline per-path inspection that
|
|
84
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**
|
|
86
|
-
re-read of `baselinePaths[].path`, so a stateful `path` accessor is
|
|
87
|
-
passing result always describes the baseline identity that validated and
|
|
88
|
-
|
|
85
|
+
closed instead of escaping the checker. The audit mirror is likewise keyed by the **validated**
|
|
86
|
+
path snapshot rather than a re-read of `baselinePaths[].path`, so a stateful `path` accessor is
|
|
87
|
+
read exactly once and a passing result always describes the baseline identity that validated and
|
|
88
|
+
bound the evidence.
|
|
89
|
+
|
|
90
|
+
The whole-baseline identity `baseline.digest` follows the same discipline: it is read exactly
|
|
91
|
+
once and that same scalar is validated, so a stateful value cannot be inspected as A while every
|
|
92
|
+
evidence comparison uses B. The rename destination's own digest is also read exactly once during
|
|
93
|
+
its validated snapshot, and the captured value drives the metadata cache, binding, the stale
|
|
94
|
+
diagnostics, and the audit identity.
|
|
89
95
|
|
|
90
96
|
Per-path evidence must be carried explicitly. Inherited properties in `baseline.artifacts` or a
|
|
91
97
|
record's `baselineArtifacts` are not own-key evidence and never satisfy a path; the trusted
|
|
@@ -110,6 +116,11 @@ candidate digest — the same one used by ordinary candidate binding, the audit
|
|
|
110
116
|
stale diagnostics. The destination is therefore never read again through the candidate map during
|
|
111
117
|
capture or binding: a nested `digest` that throws or changes after validation cannot make a stale
|
|
112
118
|
destination pair bind while the audit/stale verdicts still describe the captured value. The same
|
|
119
|
+
read-once guarantee extends to rejected destination snapshots: when the destination's own digest
|
|
120
|
+
read throws or returns an unusable value, that failed snapshot ('') is recorded and all later
|
|
121
|
+
reads for that destination path are suppressed as well — the entry's single metadata read remains
|
|
122
|
+
its only digest read, and the failed path contributes `path=missing` to `artifactDigest` instead
|
|
123
|
+
of a later, different value. The same
|
|
113
124
|
record can cover both endpoints. Destination-only evidence, source-only tombstones, split endpoint
|
|
114
125
|
records, and stale destination digests do not satisfy the source. A deletion tombstone therefore
|
|
115
126
|
cannot stand in for a known rename. This destination validation and the snapshot it produces are
|
|
@@ -129,14 +140,24 @@ obligations. `N/A` and `unknown` obligations never bind evidence and keep the le
|
|
|
129
140
|
for their audit identity (so a candidate `get` that supplies a digest is still reflected there);
|
|
130
141
|
they remain `outcome: 'none'` and do not emit `candidate-artifacts-invalid` from this applicability-gated check.
|
|
131
142
|
|
|
143
|
+
When several `rename-from` entries declare one shared destination, the read-once promise is scoped
|
|
144
|
+
**per declaring entry**: each entry's metadata validation reads the shared destination once (their
|
|
145
|
+
agreement of validated values is what detects a contradictory shared-destination snapshot). A
|
|
146
|
+
poisoned destination — one whose snapshot was rejected — is honored inside the metadata loop: a
|
|
147
|
+
later declaring entry fails closed without reading the destination again, and the failed snapshot
|
|
148
|
+
wins in the audit, overriding any previously validated successful snapshot that an earlier entry
|
|
149
|
+
carried, rather than silently describing a value the getter shipped after the failure.
|
|
150
|
+
|
|
132
151
|
A rename destination's validated own digest is carried too. When more than one accepted
|
|
133
152
|
`rename-from` entry shares one `renameTo`, every entry's validated own digest must agree; a
|
|
134
153
|
conflicting validated digest is a contradictory shared-destination snapshot and fails the whole
|
|
135
154
|
obligation closed (metadata error, `outcome: 'none'`) before any binding. The carried digest must
|
|
136
155
|
be a non-empty string: a stateful `digest` accessor whose value is not a usable string is not
|
|
137
|
-
carried or seeded
|
|
138
|
-
|
|
139
|
-
|
|
156
|
+
carried or seeded — instead the destination path is recorded as a visited failure, and neither the
|
|
157
|
+
capture loop nor the audit identity reads the destination again. Only **applicable** obligations
|
|
158
|
+
validate and carry this destination snapshot; `N/A` and `unknown` obligations keep the legacy
|
|
159
|
+
candidate read for their audit identity and do not consume the destination getter during metadata
|
|
160
|
+
validation.
|
|
140
161
|
|
|
141
162
|
The binding applies to execution and review records alike. Existing review approval, waiver,
|
|
142
163
|
failing-command, requirement-sync authority, and applicability rules are unchanged. A correctly
|
|
@@ -4,16 +4,68 @@ Contract for the documentation family: project docs configuration (D5), document
|
|
|
4
4
|
|
|
5
5
|
Documentation is a **consumer** of the #320 workflow/obligation abstractions. Nothing here is a second router, a standing maintenance loop or a cognitive lock: every check is a pure function that reports diagnostics.
|
|
6
6
|
|
|
7
|
-
## Scope of this slice
|
|
7
|
+
## Scope of this slice (status as of 2026-10-20)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
This table distinguishes merged mechanism code from end-to-end workflow integration. The last reviewed `main` commit is `29b1d65b398e2622af97bf067078373b7fa76b26` (v0.36.2). That release commit carries `[skip ci]` and only bumps package/distribution versions on top of `e5d22584e243d5b5e587ee0279625e97a9033160`, whose full-gate `CI` run [39374474269](https://github.com/dyphn1/Harness-everything/actions/runs/39374474269) completed with every job succeeded. The historical Windows System One resident teardown (`S1-RS15`) was observed in attempt 1 of [run 39175240370](https://github.com/dyphn1/Harness-everything/actions/runs/39175240370) on `main@75e04359ae5bf11a41739f8eb04d25a76ee8f3a7`; that run completed successfully on attempt 2 and says nothing about `29b1d65`. Treat the resident teardown as a known platform limitation only when a current run on the exact SHA reproduces it, and require an all-green full-gate run for that SHA before calling any baseline green.
|
|
10
|
+
|
|
11
|
+
| Phase / prerequisite | Verified implementation and remaining work |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| 0 — contract + RED fixtures | Agreed decisions in this document, #325 and #327; fixtures under `ci/fixtures/doc-lifecycle/`; preserve earlier design history |
|
|
14
|
+
| 1a — `project-docs.json` loader | **Merged** in #327; host-only malformed manifest protection remains a separate correction in #331 / draft PR #333 |
|
|
15
|
+
| 2 — roles, IDs, aliases, legacy, supersession, index, D6.9 PRD revisions | **Merged** in #327; do not rebuild this lifecycle logic for Phase 3 |
|
|
16
|
+
| Step-type prerequisite (#326) | **Complete:** PR #328 merged; Harness-owned `doc-change` is explicitly declared, has no implicit TDD binding, and is not auto-inserted by router/System One |
|
|
17
|
+
| 3a — document facts, obligations and evidence producer | **Merged** in #334: `harness-everything/scripts/doc-obligations.js` and `doc-evidence.js`. H3/H4 have *partial deterministic mechanism* coverage, not integrated workflow evidence. Known limitations: caller-attested envelope; `configDigest` presence-only; `candidateDigest` restricted to `docs/prd/`; missing independent task-scope binding. Deleted and old rename paths are bound to trusted per-path baseline evidence ([doc-evidence-baseline-binding.md](doc-evidence-baseline-binding.md), #335 fixed by #342), with workflow-disposition reconciliation left out of scope |
|
|
18
|
+
| 3b — pilot-local provider suggestions and workflow evidence reconciliation | **Next implementation slice, not started; #341-gated.** Provider metadata, adapter/interface design and RED fixtures may proceed in parallel with #341 Phase 0A–0C; **runtime** Evidence Reconciliation must not merge or activate before **#341 E0** is accepted (see the entry-gates bullet in the 3b contract below). Implement only a thin consumer adapter; do not wait for a generic #320 registry/CDG. Includes the minimum trusted producer for baseline/candidate/config/task-scope/reviewer identity, fail-closed negative cases, doc-governed gating on `step`/`obligation` writes at the verification/completion boundary, and freshness re-check at completion. The #335 baseline-bound deletion/rename evidence has shipped (#342); claiming complete integration still requires the reconciliation gate above. Preserve existing required-binding compatibility |
|
|
19
|
+
| 3c — H1–H4 and paired-task evaluation | **Not started.** Mechanism tests alone are insufficient for architecture promotion; compare frozen baseline and treatment on real bounded tasks, success/failure first, then conditional cost and latency |
|
|
20
|
+
| 1b — shared documentation craft and family metadata | **Splittable.** Shared references and thin skill consumers can proceed independently. A small optional logical-family metadata seam may be piloted; physical skill directory moves remain blocked on #320 identity/path separation |
|
|
21
|
+
| 4 — derived-view generators and `describes:` drift | **Not started.** Preserve generator authorization, deterministic regenerate-equals and scoped reminder semantics |
|
|
22
|
+
|
|
23
|
+
### Phase 3b — narrow integration contract (planned, not shipped)
|
|
24
|
+
|
|
25
|
+
- **Inputs:** explicit active `doc-change` step, D6.5 diff facts, Phase 3a obligations/evidence, available provider *metadata*, and host capabilities.
|
|
26
|
+
- **Entry and activation gates (#341):** parallel preparatory work — provider metadata, adapter/interface design and RED fixtures — may proceed while #341 Phase 0A–0C runs, but the **runtime** Evidence Reconciliation must not merge or activate until **#341 E0** is reviewed and accepted (initial evidence read/binding/outcome conformance, mutation and read-order compatibility, source/package parity, zero false MUST PASS on applicable adversarial cases, and applicable green CI including Windows). After E0, only the **bounded, opt-in, reversible** documentation consumer is eligible for runtime integration at the existing verification/disposition path (no second router, forced provider, new lock, or weakened authorization, with a documented rollback), and it supplies the observed exposure for **#341 E1** (initial target 2–3 genuine development/review cycles with sufficient case mix, not elapsed time). **#341 E1 GO / REVISE / NO-GO gates expansion beyond the limited pilot, not the pilot's own observation** (missing or unrepresentative exposure is `UNCHECKED`, never an automatic GO). The H1–H4 paired-task architectural gate below stays required for any #320 promotion in addition to E1. Shared evidence-read/binding/decision defects route to #341 (fixture-first correction, regression replay, re-observation); documentation policy and the trusted host/VCS producer remain #325 deliverables here.
|
|
27
|
+
- **Advisory provider suggestions:** `suggestDocProviders({activeStep, neededCapabilities, availableProviderMetadata, hostCapabilities})` yields candidate providers and unavailable reasons without altering the MUST/SHOULD obligation set, granting tool access, or eagerly reading unrelated `SKILL.md`. Existing binding resolution handles an explicitly chosen provider. No new automatic step selection.
|
|
28
|
+
- **Evidence reconciliation boundary and credit order:** the pilot wires Phase 3a results into the non-Fable typed-step path only. `hooks/scripts/workflow-disposition.js` delegates `step` to `updateStep()` and `binding` to `updateBinding()` in `hooks/scripts/lib/workflow-obligations.js`. Today `updateStep()` persists `step.status = 'pass'` when every required binding is `loaded` and `--evidence` is non-empty; `syncTypedAggregateObligations()` then derives aggregate `execute`/`verify` PASS from the persisted step statuses. An authored `doc-change` step is authored work, not verification: it may advance under its existing binding contract **without claiming** any doc MUST as verified. The doc-MUST reconciliation gate belongs at the authorized `verification` step and the final-completion boundary, matching D6.6 ("All events are checked at the existing workflow's `verification` step"). Phase 3b must evaluate `checkDocObligations()` for the active scope before aggregate `verify` PASS is persisted and before the workflow reports completion; an aggregate `execute` may still reflect authored step passes, but neither aggregate may stand as completion while an applicable doc MUST is unsatisfied, and `unresolvedObligations()` must surface the unsatisfied results (`pending`/`unknown`/`unavailable`/`failed`). Ordered `doc-change → review → verification` topologies stay reachable: evidence supplied by the later `review`/`verification` steps counts at its own stage, so no stage is forced to pass early and a normative doc change cannot deadlock. Non-document steps, persisted plans, existing bindings and authorization stay unchanged.
|
|
29
|
+
- **Typed aggregate writes and freshness (doc-governed scope):** aggregate PASS is not derived exclusively from persisted step passes today. `updateObligation()` accepts a direct `obligation --obligation-id execute|verify --disposition pass --evidence <text>` write for **all** non-Fable typed plans, including workflows with no applicable document MUSTs. Phase 3b's guard is scoped to typed plans/steps with an applicable, doc-governed scope: there, a direct typed `execute`/`verify` PASS must be rejected or ignored unless it was derived/reconciled from satisfied doc evidence, and the `obligation` command joins `step` in the negative tests. Non-document typed workflows and persisted plans retain today's behavior unless a separately authorized compatibility decision changes them; an already-persisted plan that gains a doc-governed scope is validated against that scope and cannot complete on a stale direct aggregate PASS. Verified doc evidence must also stay bound to the persisted candidate, task and config identity: `unresolvedObligations()` and the final completion path re-check freshness, so evidence that was valid before a later candidate edit cannot remain a completion proof solely because `status: pass` was persisted.
|
|
30
|
+
- **Minimum trusted producer (3b deliverable):** Phase 3b ships the pilot-local adapter that derives the VCS baseline and candidate identity, active task scope, parsed config bytes (`configDigest` bound rather than presence-checked), PRD root, execution/verification provenance and independent reviewer identity from host state (see *Phase 3b minimum trust boundary* below). `checkDocObligations()` only proves agreement with caller-supplied values; without this producer, a 3b PASS would promote caller-attested claims. Any input the producer cannot establish stays `unknown`/`unavailable` and MUST NOT pass. Phase 3c broadens adversarial H3 validation and adds paired architectural evaluation; it does not supply the minimum producer.
|
|
31
|
+
- **Pilot topology and unsupported paths:** the pilot is limited to non-Fable typed-step workflows (`stepBindingsEnabled === true`, set when typed steps exist and the strategy is not `fable-*`). Fable stage-contract paths (`fable-staged`, `fable-parallel`, `fable-multi-agent-workspace`) and workflows without the typed-step contract are explicitly **not integrated / unavailable** for doc-MUST reconciliation: a `stage-binding` disposition is never doc verification, and no fallback may mark a doc MUST satisfied. Unsupported paths report that state rather than silently passing.
|
|
32
|
+
- **Negative acceptance case:** for a doc-governed typed plan whose applicable MUST is failing or unknown, no boundary may report PASS: `step --disposition pass` on the verification step, `obligation --obligation-id execute|verify --disposition pass --evidence 'done'`, and final completion must all fail closed, report the unsatisfied obligation ids, and leave aggregate `verify` pending. A loaded skill plus free-text evidence is not verification. A RED fixture covers ordered `doc-change → review → verification` with reviewer/command evidence missing first and valid later: the authored `doc-change` step advances, aggregate/final PASS stays blocked until the later evidence satisfies the MUST, and neither a premature PASS nor a deadlock occurs.
|
|
33
|
+
- **Compatibility:** preserve existing non-document step bindings, authorization/Rule-of-3 semantics, old persisted plans and no new Stop/tool lock. Source and installed plugin copies must behave consistently.
|
|
34
|
+
- **Pre-integration work:** the baseline-bound tombstone/rename-pair evidence required by #335 shipped in #342; the remaining pre-integration work is the reconciliation contract above. #331 / draft PR #333 is a parallel host-config safety correction; neither requires the broad #320 migration.
|
|
35
|
+
|
|
36
|
+
### Phase 3c — architecture promotion gate for #320 (planned, not shipped)
|
|
37
|
+
|
|
38
|
+
| Hypothesis | Objective acceptance evidence |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| **H1 Provider independence** | Swap/omit suggestions without changing workflow obligations; valid alternate execution evidence can satisfy an applicable MUST; no evidence cannot |
|
|
41
|
+
| **H2 Active-step progressive disclosure** | Captured step output / provider metadata access shows only active-step relevance and no eager unrelated `SKILL.md` content or future-step loading |
|
|
42
|
+
| **H3 Evidence reconciliation** | Adversarial replay of fabricated completion, missing verifier, stale digest/base, scope substitution, forged identity, unsupported PRD roots, contradictory/failed results and unsupported waiver yields **zero false PASS** for MUSTs, with the fixture matrix below as the observable trust boundary |
|
|
43
|
+
| **H4 Task-shaped composition** | Docs-only, docs+code, REQ-citing assertions, typo-only changes, code-only uncertainty, missing independent approval and waivers produce the expected obligation union and explicit unknown/unavailable outcomes |
|
|
44
|
+
|
|
45
|
+
### Phase 3b minimum trust boundary and Phase 3c H3 validation (planned, not shipped)
|
|
46
|
+
|
|
47
|
+
Phase 3b delivers the minimum producer and identity/scope binding needed for a trustworthy PASS; Phase 3c extends the same seam with adversarial H3 validation and paired architectural evaluation.
|
|
48
|
+
|
|
49
|
+
- **Trusted producer, not the checker:** a pilot-local adapter is a **3b deliverable**; it derives the VCS baseline and candidate identity, active task scope, project-docs config identity (parsed config bytes, not presence) and execution/review provenance from host state. `checkDocObligations()` proves only that records agree with those caller-supplied values; matching a caller-provided digest is not independent validation of the input.
|
|
50
|
+
- **Identity and scope requirements:** `baseline.digest` and `baseline.artifacts[path]` come from the same recorded baseline tree; candidate artifact digests come from the candidate tree actually under review; `configDigest` is bound to the parsed config bytes rather than checked for presence; the obligation's PRD root and active task scope must match the adapter's scope, and an unsupported custom PRD root is `unavailable`.
|
|
51
|
+
- **Provenance and reviewer identity:** execution and verification records need the command, non-empty provenance and integer exit code; review approval needs an independent reviewer identity distinct from the executor/author. A free-text claim, self-review, missing verifier or replayed record never satisfies a MUST.
|
|
52
|
+
- **Dispositions:** absent, untrusted, stale, replayed, contradictory, malformed or unsupported inputs resolve to `unknown`/`unavailable`/failed, never PASS. The 3b fail-closed negative cases cover missing producer identity, scope substitution, unbound config bytes and stale candidate evidence; 3c adds the full adversarial fixture matrix below.
|
|
53
|
+
|
|
54
|
+
| H3 fixture | Expected disposition |
|
|
10
55
|
| --- | --- |
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
56
|
+
| Fabricated completion claim / no record | `unknown`; MUST unsatisfied |
|
|
57
|
+
| Forged baseline or candidate digest | rejected as stale/unknown; MUST unsatisfied |
|
|
58
|
+
| Scope substitution across tasks or PRD roots | `unknown`; MUST unsatisfied |
|
|
59
|
+
| Unsupported custom PRD root | `unavailable`; MUST unsatisfied |
|
|
60
|
+
| Replayed earlier-candidate record | stale; MUST unsatisfied |
|
|
61
|
+
| Missing, non-independent or mismatched reviewer identity | MUST unsatisfied |
|
|
62
|
+
| Unsupported waiver (no reason, no cited observed record, or self-bound scope) | `unsupported-waiver`; MUST unsatisfied |
|
|
63
|
+
| Contradictory or failed command results | failed; not PASS |
|
|
64
|
+
| Positive control: current, independently produced matching evidence | pass |
|
|
65
|
+
|
|
66
|
+
Two *separate* gates apply. The **functional gate** requires contract tests, negative cases, source/package parity, host/platform compatibility and an all-green applicable CI baseline. The **architectural gate** additionally requires H1–H4 and independently reviewed paired real-task outcomes. Freeze task fixtures, acceptance rubrics, model/effort, host permissions, comparison order and thresholds **after baseline measurement but before treatment outcomes**. Compare task correctness and failures first; report successful-task token/time/tool cost and failed-run overhead separately, with uncertainty. A small inconclusive sample is not approval. Broader #320 CDG/registry compilation and physical skill moves need **explicit owner sign-off on each specific validated interface**; retain the legacy path until then.
|
|
67
|
+
|
|
68
|
+
**Cross-contract acceptance assertion:** the #341 E0/E1 gates in the Phase 3b entry-gates bullet above, [#341](https://github.com/dyphn1/Harness-everything/issues/341) and #325's phase table must stay mutually consistent: a change to one gate's semantics updates the other two records, replays affected #341 fixtures first, and only then re-collects affected 3b observations. E0 enables only the bounded, reversible, doc-only runtime consumer; E1 gates expansion beyond the limited pilot; neither approves generic #320 CDG/registry promotion, which additionally requires the H1–H4 paired real-task evidence and explicit owner sign-off above.
|
|
17
69
|
|
|
18
70
|
## Configuration (D5)
|
|
19
71
|
|