From 4dfd637e48f7cf25d9ecdf35fb0e781648460970 Mon Sep 17 00:00:00 2001 From: Sage Date: Wed, 22 Jul 2026 01:18:26 -0500 Subject: [PATCH] fix(validator): fail-closed property-coverage attribution (N-7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The coverage display filtered on hardcoded field names, so violations with unmatched fields — approval_tiers..blocks, TOML syntax, schema failures, SOUL/IDENTITY content, cross-file mismatches — flipped no property and a FAIL run could print all-green coverage (PR #4 review N-7). Exit codes were always correct; display-only. Replace the filters with per-violation attribution: every violation marks >=1 property; unrecognized fields mark every property their source file underwrites (unknown sources mark all five). Verified across all 42 fixtures + 2 examples: 36/36 negatives flip >=1 property, 8/8 positives all-green, suite READY. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- validators/README.md | 2 ++ validators/validate.js | 54 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/validators/README.md b/validators/README.md index 37d4dcb..4c155de 100644 --- a/validators/README.md +++ b/validators/README.md @@ -44,6 +44,8 @@ node validators/validate.js --help ## Output +The Property Coverage block attributes every violation to the five properties fail-closed: a FAIL run always marks at least one property ✗, and a violation no attribution rule recognizes marks every property its source file underwrites (e.g. an unreadable `ward.toml` marks both Bounded Authority and Human Belonging). + ``` familiar-contract validator v0.4.0 Checking: /path/to/familiar diff --git a/validators/validate.js b/validators/validate.js index b8470fe..ee34824 100755 --- a/validators/validate.js +++ b/validators/validate.js @@ -420,16 +420,50 @@ ${bold('Exit codes:')} allViolations.push(...soulViolations, ...identityViolations, ...wardViolations, ...crossViolations); allViolations.push(...memoryViolations); - // Property coverage report - const propertyCoverage = { - 'Named Identity': soulViolations.filter(v => ['name', 'file'].includes(v.field)).length === 0 - && identityViolations.filter(v => ['name', 'file'].includes(v.field)).length === 0, - 'Defined Purpose': soulViolations.filter(v => ['purpose', 'core_work', 'what_i_am_not'].includes(v.field)).length === 0, - 'Bounded Authority': soulViolations.filter(v => v.field === 'boundaries').length === 0 - && wardViolations.filter(v => ['[protected]', 'protected.files', '[editable]', 'editable.paths', '[approval_tiers]'].includes(v.field)).length === 0, - 'Persistent Memory': memoryViolations.length === 0, - 'Human Belonging': wardViolations.filter(v => ['meta.person', 'protected.invariants'].includes(v.field)).length === 0, - }; + // Property coverage report. + // Attribution is fail-closed (N-7): every violation must mark at least one of + // the five properties as failing; a violation that no rule recognizes marks + // every property it could plausibly belong to via its source file's default. + const PROPERTIES = ['Named Identity', 'Defined Purpose', 'Bounded Authority', 'Persistent Memory', 'Human Belonging']; + + function propertiesFor(v) { + if (v.file === 'SOUL.md') { + if (v.field === 'name') return ['Named Identity']; + if (['purpose', 'core_work', 'what_i_am_not'].includes(v.field)) return ['Defined Purpose']; + if (v.field === 'boundaries') return ['Bounded Authority']; + // file/content/unknown: SOUL.md carries all three of its properties + return ['Named Identity', 'Defined Purpose', 'Bounded Authority']; + } + if (v.file === 'IDENTITY.md') { + // name/file/creature/purpose/content all serve the machine-readable identity record + return ['Named Identity']; + } + if (v.file === 'ward.toml') { + if (['meta.person', 'protected.invariants'].includes(v.field)) return ['Human Belonging']; + if (v.field.startsWith('approval_tiers.') || v.field === '[approval_tiers]' + || v.field.startsWith('schema /approval_tiers') || v.field.startsWith('schema /editable') + || v.field.startsWith('schema /protected') + || ['[protected]', 'protected.files', '[editable]', 'editable.paths'].includes(v.field)) { + return ['Bounded Authority']; + } + if (v.field.startsWith('schema /meta')) return ['Human Belonging']; + // file/syntax/content/root-schema failures: ward.toml underwrites both properties + return ['Bounded Authority', 'Human Belonging']; + } + if (v.file === 'MEMORY.md') return ['Persistent Memory']; + if (v.file === 'cross-file') return ['Named Identity']; + // Unknown source: fail closed across the board + return PROPERTIES; + } + + const failedProperties = new Set(); + for (const v of allViolations) { + for (const prop of propertiesFor(v)) failedProperties.add(prop); + } + + const propertyCoverage = Object.fromEntries( + PROPERTIES.map(prop => [prop, !failedProperties.has(prop)]) + ); console.log(bold('Property Coverage:')); for (const [prop, pass] of Object.entries(propertyCoverage)) {