ID-109 PRODUCT — internal-function holder attribution (Option C, extraction-side tag)
ID-109 — internal-function holder attribution (PRODUCT)
Section titled “ID-109 — internal-function holder attribution (PRODUCT)”Spec chain:
{109.1}RESEARCH (done, ratified S348) →{109.2}PRODUCT (this document) →{109.3}TECH →{109.4}PLAN (TECH/PLAN pending PRODUCT ratification). Artefact kind:{N.2}PRODUCT — defines desired behaviour and numbered, testable invariants; mechanism choice and implementation live in{109.3}TECH. Provenance: promoted frombl-297(S345); supersedes ID-101{101.11}(cancelled, re-homed here). Product call ratified by Liam S348 (recorded in the{109.1}ledger journal): adopt Option C (attribute internal-function certs to self via an extraction-side tag), disclaimer-dominant + abstain-on-ambiguity, explicit-possessive-only trigger, keepholder_basis=internal_function, lean on disclaimer + possessive signals, dedicatedsource_scopefield.
Summary
Section titled “Summary”When a Knowledge Hub document states that the author organisation’s own internal
function holds a certification — e.g. “Our internal IT team is compliant to ISO 27001” —
the certification is attributed to the client (holder=self) rather than left silently
unattributed. The classifier tags the holder relationship with an explicit
internal-function signal at extraction; the holder rule keys off that tag to stamp
{ holder: "self", holder_basis: "internal_function" }. The rule is supplier-disclaimer-
dominant (a disclaimer to a third party always wins), fires only on explicit
first-person possessive framing, and abstains to holder=null whenever the signal is
absent or ambiguous — closing the 7e511dbc-class coverage gap without re-introducing the
false-self-attribution risk that a naïve string match would carry.
Problem
Section titled “Problem”Unqualified internal-function compliance statements currently yield holder=null after
the {101.9}/{101.10} alias fix. “Internal IT is compliant to ISO 27001” is captured as
a certification mention but receives no holder signal, so a genuine client
certification is silently unattributed in every downstream consumer (“what certs does the
client hold?” returns nothing for it). This is a genuine coverage gap — a class of
subject the holder rule was never designed to attribute — and is distinct from the
{101.9} resolution bug (there the subject was the client but its short name failed to
resolve; here the subject is an internal function deliberately excluded from
organisation extraction, so no alias resolution can ever help). Full mechanics, options
A–D, and the disambiguation-signal analysis live in the ratified {109.1}
RESEARCH.md; this PRODUCT defines the behaviour for the ratified Option C.
Code-intelligence orientation (cited)
Section titled “Code-intelligence orientation (cited)”GitNexus (repo: knowledge-hub, branch canonical-pipeline-setup) was run against the
ID-109 domain vocabulary; ast-dataflow is TS-only, so the Python pipeline surfaces were
covered by grep over scripts/cocoindex_pipeline/*.py and supabase/migrations/*.sql.
gitnexus_context({name: 'deriveHolderMetadata'})— verbatim: symbolFunction:lib/ai/classify.ts:deriveHolderMetadata(lines 524–595); incoming.calls:classifyContent(sole caller,lib/ai/classify.ts); outgoing.calls:resolveAlias(lib/entities/entity-aliases.ts),canonicalise(lib/entities/entity-dedup.ts);processes: [](the holder rule is a pure function with no indexed execution-flow process). This is the TS holder-rule edit locus; the Python port isscripts/cocoindex_pipeline/holder_rule.pyderive_holder_metadata.gitnexus_query({query: 'holder attribution certification compliance internal function self supplier'})—processes: [](no indexed call-chain),definitionssurfaced (all verbatim from the query): the cross-path evalscripts/eval-holder-rule-ts.ts(classifyHolderDivergence833–867,fetchLegacyHolderStates1413–1436); the parity testsscripts/tests/test_parity_driver.py(test_build_holder_states_no_signal_cert_absent161–165,test_build_holder_diagnostics_only_keys_certifications210–221); the prompt-contract testsscripts/tests/test_cocoindex_prompts.py(TestRelationshipPromptHolderRules277–335, asserting the verbatim disclaimer paragraph- supplier-attribution example are present in the Python prompt); the write-path tests
scripts/tests/test_cocoindex_flow_write_path.py(TestHolderStampWiring3400–3622,test_cert_metadata_merges_span_and_holder_keys3466–3508,test_supplier_cert_carries_supplier_name3510–3549). These are the suites the new internal-function cases must extend.
- supplier-attribution example are present in the Python prompt); the write-path tests
- Downstream consumer types (OUT of scope — read-only, confirm additive-safety only):
CertificationMetadata(components/dashboard/certification-summary-card.tsx:29–39) currently typesholder?: 'self' | 'supplier';CertificationReportEntry(lib/mcp/formatters/entities.ts:132–141). The newholder_basiskey is an additive optional metadata field — it must not break these read sites (no enum value is removed fromholder;holder_basisis new and ignorable). No consumption change is in scope (see Non-goals). - grep sweep (cited file:line). TS holder rule
lib/ai/classify.ts:HOLDS_SYNONYMS(522),deriveHolderMetadata(524–595), client orgBRANDING.organisationName.toLowerCase()(529), Pass-2 synonym gatesourceIsClientOrg/sourceIsExtractedOrg(570–572), self/supplier stamp (583–590). Python holder rulescripts/cocoindex_pipeline/holder_rule.py: Pass-2 gatesource_is_client_org/source_is_extracted_org(164–171), Inv-10 absent-on-no-signal (if holds_source is None: continue, 181), self/supplier stamp (187–193). Prompt surfaces — TSlib/ai/skills/classification.mdholder-disambiguation block (719–778), “Internal departments” extraction exclusion (390); Pythonscripts/cocoindex_pipeline/prompts.pyholder-disambiguation block (226–272),ENTITY_MENTION_PROMPT(139). Confirmed latent divergence: the PythonENTITY_MENTION_PROMPTcarries no “Internal departments” exclusion line analogous toclassification.md:390— TECH must align this (the internal-function subject must remain a non-organisationmention on both paths; the new signal rides on the relationship, not on promoting “Internal IT” to an org).source_scopeandholder_basisare net-new fields — repo-wide grep returns zero existing references for either, so both are greenfield additions to the relationship contract / mention metadata respectively.
Goals / Non-goals
Section titled “Goals / Non-goals”Goals. Attribute a certification to holder=self when the document author’s own
internal function holds it, declared via explicit first-person possessive, with no
supplier-disclaimer in scope — at byte-stable TS↔Python prompt parity and identical
holder-rule behaviour on both extraction paths.
Non-goals (explicitly out of scope).
- Consumption / KG. No change to how
metadata.holderis read downstream — no edits toget_certification_status/ MCP tools, the compliance dashboard (components/dashboard/compliance-status-section.tsx), the entity-management UI, or any knowledge-graph projection. This Task changes only what gets stamped at capture. - Promoting internal functions to organisation mentions. “Internal IT” stays excluded
from
organisationextraction on both paths; the signal rides on the relationship. - Bare / non-possessive internal-department attribution. Bare “Internal IT” or “the IT team” with no first-person possessor does not fire (see invariant 6); the authorship-is-client prior alone is not a sufficient trigger (ratified OQ-3/OQ-5).
- Mixed-provenance ingest robustness. The rule is specified for the client’s own corpus (document author = client). Hardening for ingested RFP responses / due-diligence packs that quote suppliers is deferred (see Open question 1).
Behavior
Section titled “Behavior”The certification holder pipeline has two stages — extraction (the LLM emits entity
mentions + relationship triples) and the holder rule (a pure function that stamps
metadata.holder per certification mention). Both stages are duplicated TS (classifyContent
→ deriveHolderMetadata) and Python (extraction → derive_holder_metadata) and MUST
behave identically. The invariants below are written from the perspective of the holder-rule
consumer (the code/operator reading entity_mentions.metadata.holder after capture) and the
extraction-contract consumer (the holder rule reading the relationship triples).
The internal-function trigger
Section titled “The internal-function trigger”-
Positive case — internal-function self-attribution. When a certification’s holder is the document author’s own internal function, declared with explicit first-person possessive framing (e.g. “our internal IT team”, “our in-house security team”, “we maintain ISO 27001 through our internal IT”), and no supplier/third-party disclaimer is in scope for that certification, the certification mention is stamped
{ holder: "self", holder_basis: "internal_function" }. This is the 7e511dbc-class case (“Our internal IT team is compliant to ISO 27001 / Cyber Essentials Plus”) that today yieldsholder=null. -
The signal is produced at extraction, on the relationship. The classifier emits the internal-function signal as an explicit field on the
holds/complies_with/evidencesrelationship whosesourceis the internal function — a dedicatedsource_scopefield with value"internal"(ratified mechanism OQ-6; the exact wire shape — field name, enum values, schema/Zod surface — is a TECH decision, but it MUST be a dedicated field, not a reserved/sentinelsourcestring value). When the source is an ordinary third party or the named client org,source_scopeis"external"or absent/null. The internal function (“Internal IT”) is not emitted as anorganisationentity mention — it remains excluded from organisation extraction on both paths. -
The holder rule consumes the tag as a third stamp branch, before the supplier branch. In both
deriveHolderMetadata(lib/ai/classify.ts:579–593) andderive_holder_metadata(scripts/cocoindex_pipeline/holder_rule.py:176–194), when the resolved holder source for a certification target carriessource_scope="internal"(and the supplier-disclaimer guard did not scope the certification to a third party — see invariant 4), the mention is stamped{ holder: "self", holder_basis: "internal_function" }. Otherwise the existing self/supplier logic is unchanged: a source equal to the client org →{ holder: "self" }(noholder_basis); any other resolved org source →{ holder: "supplier", supplier_name: <source> }.
Supplier-disclaimer dominance (the dominant guard)
Section titled “Supplier-disclaimer dominance (the dominant guard)”-
A supplier/third-party disclaimer always wins over the internal-function tag. When a certification falls within the scope of an existing supplier-disclaimer signal — the trigger phrases and disclaimer paragraphs already defined in the holder-disambiguation block (
lib/ai/skills/classification.md:719–778/scripts/cocoindex_pipeline/prompts.py:226–272), e.g. “held by [party]”, “managed by [party]”, or a “…are held by [party], not [author]” disclaimer paragraph — the certification is attributed to that named third party (holder=supplier,supplier_name=<party>), and the internal-function tag MUST NOT fire, even if the same sentence/section also uses “internal” phrasing. The disclaimer establishessource_scope="external"(or names a third-party source), and the internal branch is never reached. This preserves the existing supplier-disclaimer behaviour exactly. -
Named third-party internal functions are external, not self. When the internal function belongs to a named third party rather than the document author — e.g. “Example Datacentre’s internal security team is compliant to ISO 27001” — the certification is attributed to that third party (
holder=supplier,supplier_name=example datacentre) or, if the third party is not resolvable as a holder under the existing rules, leftholder=null. It is neverholder=self. A possessor other than the document author (“their”, a named org’s possessive) disqualifies the internal-function trigger.
Abstention (abstain-to-null on ambiguity)
Section titled “Abstention (abstain-to-null on ambiguity)”-
Bare / non-possessive internal-department phrasing abstains to
null. “Internal IT is compliant to ISO 27001”, “the IT team holds ISO 27001”, or any internal-department subject without an explicit first-person possessive (“our”/“we”/“our own”) does not fire the internal-function trigger and stamps no holder signal →holder=null. The authorship-is-client prior alone is deliberately not sufficient (ratified OQ-3/OQ-5): the precision bar requires explicit possessive framing. This is a deliberate recall sacrifice for precision. -
Ambiguous or conflicting signals abstain to
null. When the internal-vs-external signal is absent, ambiguous, or in conflict (e.g. first-person possessive present but a third-party party is also named in scope; or the disclaimer scope is unclear), the holder rule stamps no signal →holder=null. Abstention — never defaulting toselfon a missing or uncertain signal — is mandatory and is what makes this rule safe where a naïve string match is not. -
Inv-10 preserved — no-signal certifications remain unstamped. A certification mention with no resolved holder source (no
holds/synonym relationship, or a relationship whose source the holder rule rejects) is absent from the holder map and keeps its span-only metadata:metadata.holderis unset (null). The new internal branch MUST NOT change this — it adds a stamp only for the explicit internal-tagged + disclaimer-free case; it never converts a previously-nullcertification toselfexcept via that explicit path. The existing guard (if holds_source is None: continue— “never default to self”) stays in force. -
Inv-14 preserved — only certification mentions are stamped. The holder rule stamps
metadata.holderonly on mentions whoseentity_type == "certification". The internal-function branch does not stamp non-certification mentions.
Provenance and consumer safety
Section titled “Provenance and consumer safety”-
holder_basisis an additive provenance breadcrumb. Internal-function self-attribution stampsholder_basis: "internal_function"alongsideholder: "self"(ratified OQ-4). This distinguishes internal-function self from name-resolved self for downstream audit and lets a future consumption Task treat them differently.holder_basisis a new, optional metadata key: name-resolved self (source == client org) and supplier stamps do not carry it, and the addition MUST be non-breaking for existingmetadata.holderread sites (theholdervalue space{self, supplier, <absent=null>}is unchanged; consumers that do not readholder_basisare unaffected). -
holder=selfis the same observable holder value for both self paths. A name-resolved-self certification and an internal-function-self certification both reportholder: "self"to any consumer that reads onlyholder. The distinction lives solely in the optionalholder_basisbreadcrumb. No consumer is required to special-case internal-function self to obtain correct holder attribution.
Parity and composition invariants
Section titled “Parity and composition invariants”-
TS↔Python parity is mandatory. The internal-function signal MUST be emitted identically by both prompt surfaces (
classification.md+prompts.py, byte-stable mirrors of the holder-disambiguation block) and consumed identically by both holder rules (deriveHolderMetadata+derive_holder_metadata). For every input, the two paths MUST produce the sameholder/holder_basis/supplier_nameoutcome. A divergence between paths MUST be caught by the cross-path parity harness (scripts/eval-holder-rule-ts.ts+scripts/cocoindex_pipeline/parity_driver.py) and fail CI. The latentENTITY_MENTION_PROMPTdivergence (Python lacks the “Internal departments” exclusion thatclassification.md:390carries) MUST be resolved as part of this change so the internal-function subject stays a non-organisationmention on both paths. -
Composition with the PI-15 fail-closed alias model — no regression. Internal-function self-attribution does not depend on the client name appearing in the document (that is the point), so it neither relies on nor regresses the
{101.9}/{101.10}alias-driven self-match. The internal tag is consumed after canonicalisation and does not read or write the alias snapshot. The PI-15 fail-closed deploy gate (flow.py_generate_client_alias_snapshot: configured client + zeroprovenance='client'rows →RuntimeError, deploy fails) stays untouched. The new rule MUST NOT add any path that, on a missing or ambiguous signal, silently self-attributes — abstain-to-null(invariants 6–8) is the only permitted no-signal behaviour. The R4 fail-fast (unsetPIPELINE_CLIENT_ORG→ValueError,holder_rule.py:124–131) continues to hold; the internal branch runs after the client-org guard, not around it. -
Short-term
holder=nullstays until shipped on both paths. Until the Option-C rule lands on both extraction paths with parity proven, internal-function compliance continues to stampholder=null. Behaviour MUST NOT flip partially on one path only — no interim state where TS attributes internal-function self but Python does not (or vice versa). The honest interim signal isnull.
Worked examples (de-identified)
Section titled “Worked examples (de-identified)”-
The following synthetic, de-identified examples (placeholders
{CLIENT_ORGANISATION_NAME}, “Example Datacentre”, “Internal IT”) are normative for the invariants above:- Fires (self). “Our internal IT team is compliant to ISO 27001.” →
holder: "self",holder_basis: "internal_function". (Invariant 1.) - Disclaimer wins (supplier). “Note: the certifications below are held by Example
Datacentre, not {CLIENT_ORGANISATION_NAME}. ISO 27001 is maintained by our internal
team.” →
holder: "supplier",supplier_name: "example datacentre"— the disclaimer scopes the cert; the internal phrasing does not override it. (Invariant 4.) - Named third party (not self). “Example Datacentre’s internal security team is
compliant to ISO 27001.” → not
self;supplier/nullper existing rules. (Invariant 5.) - Bare internal department (abstain). “Internal IT is compliant to ISO 27001.” →
holder: null(no explicit possessive). (Invariant 6.) - Ambiguous (abstain). “The IT team holds ISO 27001” with no possessive, no disclaimer,
no authorship cue →
holder: null. (Invariant 7.) - Name-resolved self (unchanged). “{CLIENT_ORGANISATION_NAME} is certified to ISO 9001.”
→
holder: "self", noholder_basis. (Invariant 11.)
- Fires (self). “Our internal IT team is compliant to ISO 27001.” →
Open questions
Section titled “Open questions”These are residual product questions surfaced for Liam by this PRODUCT pass. None block TECH authoring on the ratified Option-C behaviour above; they refine edges.
-
Mixed-provenance ingest (deferred scope confirmation). The “document author = client” prior is safe for the client’s own corpus but weaker for ingested RFP responses / due-diligence packs that quote suppliers. This PRODUCT specifies the client-corpus case and defers mixed-provenance hardening (Non-goals). Confirm mixed-provenance ingest is out of scope for ID-109 (the explicit-possessive + disclaimer-dominant rule already de-risks it, but a named-third-party-in-scope-plus-our-possessive sentence abstains to
nullper invariant 7 rather than guessing — confirm that abstention is the desired behaviour for such mixed sentences, vs attributing to the named third party). -
Possessive surface breadth (extraction-prompt tuning, informs TECH). Invariant 1 lists “our internal IT team”, “our in-house security team”, “we maintain … through our internal IT” as positive triggers. Confirm the possessive surface to instruct the LLM on: is “our” / “we” / “our own” the complete trigger set, or should “in-house” alone (without “our”) count as first-person possessive? This is a precision/recall dial on the prompt wording; TECH will encode whatever set is confirmed. Default if unanswered: require an explicit first-person possessor (“our”/“we”/“our own”); treat bare “in-house” as abstain-to-
null.