docs: add ADR for evidence fabric boundaries and contract ownership - #13
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new Architecture Decision Record (ADR) to formalize/“freeze” the boundary between the umbrella SourceOS metadata-plane contracts and a separate operational evidence-fabric contract family (including ownership and naming/discriminator rules).
Changes:
- Introduces an ADR describing evidence-fabric boundaries, reserved names, and the
kindvstypediscriminator rule. - Documents repo ownership split across SourceOS-Linux, SocioProphet, and SociOS-Linux.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -0,0 +1,123 @@ | |||
| # ADR: Evidence Fabric Boundaries and Contract Ownership (v0.1) | |||
There was a problem hiding this comment.
ADR filenames and titles in this repo follow the numbered pattern (e.g., docs/adr/0005-... with a # ADR-0005: ... heading). Consider renaming this to the next available number (currently 0006-...) and updating the H1 to match, so it stays consistent with the existing ADR index/sorting.
| # ADR: Evidence Fabric Boundaries and Contract Ownership (v0.1) | |
| # ADR-0006: Evidence Fabric Boundaries and Contract Ownership (v0.1) |
There was a problem hiding this comment.
Rejected — the premise does not hold.
The suggestion is to renumber this ADR to match a numbered convention and take "the next available number (currently 0006)". There is no such convention in docs/adr/ to match. Actual state of the directory:
| number | times used |
|---|---|
0001 |
8 |
0012 |
3 |
0002, 0006, 0009, 0010 |
2 each |
Three different header styles are also in use (# ADR-0001:, # ADR 0001 —, and ## Status sections), including in 0000-template.md's own neighbours. Renaming this one file would not make the collection uniform; it would just add a ninth 000x collision. 0006 in particular is already taken twice — 0006-evidence-fabric-boundaries.md and ADR-0006-polycentric-observer-schema-family.md — so the specific number suggested here is not available.
There is a real problem next to this one that the review did not name: 0006-evidence-fabric-boundaries.md now exists, with the conventional header, covering the same decision as this file, with different content. Two ADRs for one decision is worth resolving; renaming either of them is not, and choosing which is canonical is an editorial call rather than a cleanup, so I am flagging it rather than doing it silently.
Closing this thread as answered, not as done.
| ## Status | ||
|
|
||
| Proposed. | ||
|
|
There was a problem hiding this comment.
The ADR header metadata format here diverges from the project ADR template: other ADRs use a top metadata block like **Status:** ..., **Date:** ... (and sometimes **Deciders:** ...) followed by a --- divider. Updating this ADR to match the template will keep the collection uniform and easier to scan.
| ## Status | |
| Proposed. | |
| **Status:** Proposed | |
| --- |
There was a problem hiding this comment.
Rejected — the premise does not hold.
The suggestion is to renumber this ADR to match a numbered convention and take "the next available number (currently 0006)". There is no such convention in docs/adr/ to match. Actual state of the directory:
| number | times used |
|---|---|
0001 |
8 |
0012 |
3 |
0002, 0006, 0009, 0010 |
2 each |
Three different header styles are also in use (# ADR-0001:, # ADR 0001 —, and ## Status sections), including in 0000-template.md's own neighbours. Renaming this one file would not make the collection uniform; it would just add a ninth 000x collision. 0006 in particular is already taken twice — 0006-evidence-fabric-boundaries.md and ADR-0006-polycentric-observer-schema-family.md — so the specific number suggested here is not available.
There is a real problem next to this one that the review did not name: 0006-evidence-fabric-boundaries.md now exists, with the conventional header, covering the same decision as this file, with different content. Two ADRs for one decision is worth resolving; renaming either of them is not, and choosing which is canonical is an editorial call rather than a cleanup, so I am flagging it rather than doing it silently.
Closing this thread as answered, not as done.
… governance substrate (task #13) (#250) Language regulates state, so an approved term must be usable to reason about governance: CAPTURED (ontology class) + VECTOR-ALIGNED (NP↔VP link in the sovereign 768 space) + IMPLEMENTED (estate entity/service). Extends the existing GlossaryTerm (optional, backward- compatible) with partOfSpeech (NP/VP, Tesnière), status, a 3-method alignment block, and typed relations over a CLOSED RDF/RDFS/SKOS/FOAF predicate set (is-type/has-type, is-a, has-a, is-member/has-member, skos:broader/narrower/related, foaf:member, has-datatype). Fail-closed drift-guard (validate_glossary_alignment_examples.py): an approved term missing any alignment method is a governance hole; NP→entity, VP→action/service; vector links reciprocal; relation inverse-pairs reciprocate; skos:related symmetric; is-a subsumption ACYCLIC (so a reasoner can derive dependencies+constraints). Teeth-verified (approve-unaligned, broken reciprocity, broken symmetry, is-a cycle all fail). Dogfood: the draft example is SP-GATE-004's release_gates.* new vocab, unaligned. make validate ok; dup-$id 345. Follow-ups: LSA↔LDA currency detection; agreement test vs the blast-radius graph + neurosymbolic domain.
…erned (task #13) (#255) * feat(vocab): LIVE vocab-currency GovernedLoop — LSA(glossary)↔LDA(corpus) currency (task #13) Makes the vocabulary-currency loop live and GOVERNED by the merged GovernedLoop contract. The approved glossary is the fixed set (LSA), the corpus token distribution is the open set (LDA), and uncovered probability mass is the currency signal (the LSI drive — new fields that became vocab but aren't connected). Remediation connects the highest-mass uncovered term, strictly decreasing divergence (monotone-decrease is real). The runner reads bound + tolerance + onNonConvergence + admission FROM the contract and obeys them. Fail-closed teeth (asserted by validate-vocab-currency-loop, wired into make validate): - convergent corpus reaches currency (ok); - a domain the vocab can't name within the bound ESCALATES-human, never silently passes, never spins past maxIterations; - a loop with no superconscious admission is REFUSED (loops don't self-authorize). Emits candidateNewVocab for routing into ontogenesis/prophet-ontology. Dependency-free. * fix(vocab): rename misleading --emit-candidates to --trace (own review) The flag gated the per-iteration trace, not the candidate list (candidateNewVocab is always emitted), so the name misdescribed its effect. Renamed to --trace. * fix(vocab): remediate Copilot review on #255 — measure guard + null tolerance + conformance-first - run_loop now REFUSES a contract whose convergence.measure this runner doesn't implement (only monotone-decrease/error-below-tolerance; a 'fixpoint' contract is refused rather than run with mismatched semantics — don't honor a guarantee you don't provide). - convergence.tolerance may be null per schema; treat null as 0.0 (must fully cover) instead of crashing on 'D <= None'. - the CI harness now schema-validates the shipped GovernedLoop contract FIRST (like the repo's other validators), so contract drift fails with a clear conformance error not a KeyError. - added two teeth: contract:conforms and unsupported-measure:refused. All 5 pass.
…op (task #13) (#256) * feat(vocab): emit conformant draft GlossaryTerms from the currency loop (task #13) Closes the currency→proposal lifecycle. Each remediation step now emits a status:draft GlossaryTerm artifact (result.proposedTerms) for the token it connects — directly ingestible by ontogenesis for the 3-method alignment pass. The loop PROPOSES only: partOfSpeech, alignment, and approval are downstream; status stays draft (the loop never self-approves). The CI harness refuses any proposed term that doesn't conform to GlossaryTerm.json or that isn't status:draft, so the loop can't push malformed or self-approved vocab into the glossary. 6 teeth. * fix(vocab): use .get('id') in self-approved check so a malformed proposal reports, not crashes (Copilot #256) A proposal missing 'id' is exactly what the conformance check should surface; using t['id'] (computed before the 'if bad' branch) would KeyError-crash first. Use t.get('id').
… vocab (task #13) (#257) * feat(vocab): LIVE dogfood of the currency loop on the estate's own vocab (task #13) tools/dogfood_vocab_currency.py runs the governed loop over THIS repo's real vocabulary — every approved GlossaryTerm in examples/ (fixed/LSA) vs specs/*.md (open/LDA) — answering honestly whether our approved vocab is current with our specs. It is not: 2 approved terms vs 14 specs, divergence ~0.98, and the governed loop escalates-human (can't reach currency in its 8-iteration bound) with concrete draft proposals rather than pretending currency. Writes each proposed draft GlossaryTerm to build/vocab-currency-proposals/<slug>.json (gitignored) — the artifacts ontogenesis ingests. Informational (make vocab-currency-dogfood, exit 0, NOT in the validate gate); corpus is Markdown-cleaned + generic-prose stoplisted so domain terms surface (contract/runtime/schema/ policy/device/mpcc/slot). Enforcement stays on fixtures in validate-vocab-currency-loop. * fix(vocab): remediate Copilot review on #257 — robustness + de-hardcode metrics - real_corpus() now skips an unreadable/undecodable spec with a warning instead of crashing. - real_glossary() requires type==GlossaryTerm (or untyped bundle entry), so a doc merely carrying a glossary-namespaced id but typed otherwise isn't miscounted as a term. - reworded the 'always exits 0' claim to its honest meaning: it doesn't gate on the currency OUTCOME (exit 0 whether current or escalated); a genuine IO error still surfaces. - softened volatile hard-coded metrics (term/spec counts, ~0.98 divergence) in the spec + CHANGELOG to qualitative statements marked point-in-time.
…od alignment pass (task #13) (#258) * feat(vocab): governed draft->approved glossary promotion — the 3-method alignment pass (task #13) Closes the vocabulary lifecycle. tools/promote_glossary_term.py promotes a draft GlossaryTerm to approved ONLY when the 3-method alignment holds as a fail-closed MEET: capture (ontologyClassRef) AND vector-align (vectorLink pinned to the sovereign nomic/768 space AND reciprocated by the named peer) AND implement (estateBinding entity/service/action). It recomputes the meet (never trusts a declared flag), refuses on any missing/non-reciprocal/off-space method (term stays draft — no governance hole), and its approved output PASSES the #250 alignment drift-guard by construction. validate-glossary-promotion (in make validate) teeth: full alignment promotes; missing-implement, non-reciprocal peer, and off-sovereign-space embedding are each refused; promoted output passes the reused #250 check_alignment. Example: governed-loop <-> operational-dag reciprocal pair. * fix(vocab): guard promotion input — refuse non-term + already-approved (own review) promote() now refuses a non-GlossaryTerm input and an already-approved term (no silent mutation), and uses .get('id') so malformed input can't KeyError. Two teeth added; 7 total. * fix(vocab): remediate Copilot review on #258 — cosine + schema-valid output + guard filter - promote() now requires vectorLink.cosine (number in [-1,1], schema-required) AND validates the minted approved term against GlossaryTerm.json — it can never produce a schema-invalid approved term (fail-closed on the output, not just the alignment content). - the #250-guard teeth keyed the terms dict by 'approved'/'peer', but check_alignment prefixes messages with the dict key, so a real failure (e.g. non-reciprocal) whose message lacked the term id could slip the filter. Now keyed by id; filter catches any failure prefixed with the id. - added teeth: missing-cosine refused + a regression test proving the #250-guard filter catches a holey approved term. 9 teeth total.
…h (task #13) (#259) * feat(vocab): agreement test — glossary relations vs blast-radius graph (task #13) The neurosymbolic check that the vocabulary tells the truth about structure. tools/agreement_test.py projects the glossary's composition relations (has-a/has-member) onto the estate via alignment.estateBinding and compares them to a CONSUMED blast-radius/dependency graph (GBRG owns that graph; this only compares). Fail-closed on OVERCLAIM (a declared dependency with no observed edge = governance hole); reports DRIFT (an observed edge no relation names) as a remediation candidate (a proposed has-a relation), same treatment as the vocab-currency loop's candidate terms. Only composition predicates imply runtime dependencies; is-a/skos:* excluded. validate-agreement (in make validate) teeth: aligned agrees; overclaim refused; drift reported as candidate. * fix(vocab): agreement test only holds APPROVED terms (own review) A draft term hasn't passed the alignment gate and doesn't regulate state, so its relations must not overclaim the estate. Restrict declared edges + bindings to status==approved terms. * fix(vocab): fail-closed on ambiguous bindings + malformed graph edges (Copilot #259) - two approved terms binding the SAME estate entity made drift attribution ambiguous (dict silently dropped a duplicate). Now surfaced as bindingConflicts and fails closed; only unambiguously-bound entities participate in drift. - a graph edge missing from/to would KeyError-crash. Now malformed edges are collected into malformedEdges and fail closed instead of crashing. Two teeth added (ambiguous-binding, malformed-edge); 5 total.
…ask #13) (#260) * feat(vocab): stopword deviation analysis — audit the words we DROP (task #13) The stoplist is itself ungoverned vocabulary: a word that is filler in one domain (set, class, state, required) is a real term in another. tools/stopword_analysis.py audits the dropped words across domains using TWO signals — cross-domain deviation (concentration) AND compositional density (repeated-collocation rate) — because frequency alone can't tell a domain term from a stylistic quirk (e.g. 'and' is concentrated in chatty prose but forms no repeated collocations). Surfaces term-candidate / stylistic / noise; term-candidates are un-stoplist proposals (a remediation signal like the currency loop's candidates and the agreement test's drift). validate-stopword-analysis teeth: domain terms surfaced; a stylistically-concentrated word NOT promoted (the trap frequency alone falls into); a uniform word is noise. stopword-analysis-live audits the shipped STOP over specs/*.md (currently clean: 0 candidates / 38 noise / 9 insufficient). Compositional density is the bigram floor of the k-gram TF-IDF/LSA differential (3..7) to follow. * fix(vocab): remediate Copilot review on #260 - compare UNROUNDED compositional density to the threshold (rounding could flip a boundary verdict); round only for reporting. - precompute a Counter per domain instead of t.count(w) per (word, domain) — O(tokens + words*domains). - expose the three verdicts distinctly (stylisticWords/uniformNoiseWords + counts); keep the flattened noiseWords (their union) for existing consumers. - validator: require >=1 term-candidate before the all(...) concentration check (all([]) is vacuously True and would mask an empty result). * fix(vocab): restore repeated_adj line dropped in the prior remediation edit The previous commit accidentally removed the repeated_adj assignment while switching to the unrounded-density comparison, causing a NameError. Restored; all teeth + make validate green.
…opword candidates (task #13) (#261) * feat(vocab): k-gram TF-IDF/LSA differential (orders 3-7) — confirm stopword candidates The confirmation stage for the stopword deviation analysis. tools/kgram_tfidf_differential.py measures a candidate word's domain-specificity across n-gram orders 3..7: per order it builds the n-gram x domain TF-IDF matrix (n-grams over raw tokens, so phrase-embedded stopwords are captured), takes the LSA (truncated SVD) top component as the cross-domain axis, and scores the candidate by its strongest domain-specific n-gram DISCOUNTED by intrinsic unigram specificity (so 'the' can't borrow 'the state machine' specificity). The differential across orders is the discriminator: a true term PERSISTS (confirmed-term); a concentrated-but-diffuse word ('and') or a borrowed-signal word ('the') stays unconfirmed. validate-kgram-differential teeth: set/class/state confirmed across 3..7; 'and' unconfirmed; 'the' stripped by the unigram discount; orders are exactly [3,4,5,6,7]. Uses numpy (installed in the make recipe). Closes the two-stage stopword design. * fix(vocab): remediate Copilot review on #261 — LSA in verdict, perf, <2-domain guard - confirmation now uses BOTH signals of the differential (docstring promised it): TF-IDF must persist across orders AND the word must load on the dominant cross-domain LSA axis (lsaParticipates). Matches 'tf-idf lsa differential'; 'and' now fails both signals. - precompute candidate->row indices in the single pass over vocab instead of rescanning the whole vocab per candidate (O(vocab) once, not O(vocab*candidates) per order). - differential() raises on <2 domains (TF-IDF/LSA are cross-domain measures — fail fast, don't emit a meaningless result). Teeth: single-domain rejected. 6 teeth total.
Summary
Adds an ADR that freezes the boundary between the umbrella SourceOS metadata plane and the planned operational evidence fabric.
What this PR adds
docs/adr/ADR-evidence-fabric-boundaries-v0-1.mdWhy
The current ecosystem now has enough moving pieces that repo and contract ownership needs to be explicit before evidence-plane repos are created and implemented.
This ADR locks the following points:
sourceos-specremains the umbrella metadata-plane and governance contract layerRunRecordkindrather thantypeCurrent upstream posture
This PR was rebased onto the current visible
mainbase exposed by the connector (1dfa4b6067904fc1ebd5c708b3dcd8c379659159).Follow-on work
sourceos-specsourceos-spec