Skip to content

ID-109 RESEARCH — internal-function holder attribution (Option C feasibility)

ID-109 — internal-function holder attribution (RESEARCH)

Section titled “ID-109 — internal-function holder attribution (RESEARCH)”

Spec chain: {109.1} RESEARCH (this document){109.2} PRODUCT → {109.3} TECH → {109.4} PLAN (all pending the product call this document surfaces). Artefact kind: {N.1} RESEARCH. Investigation only — recommends, does not decide. The product call (“is internal-function self-attribution desired behaviour at all?”) belongs to Liam and is surfaced as an Open Question, not resolved here. Provenance: promoted from bl-297 (“internal-fn holder, Option C”, S345); supersedes ID-101 {101.11} (“Internal-function holder coverage gap (7e511dbc-class)”, cancelled and re-homed here so ID-101 closed on {101.9} parity alone, S348).

Unqualified internal-function compliance statements — e.g. “Internal IT is compliant to ISO 27001 / Cyber Essentials Plus” (the 7e511dbc-class eval item) — currently yield holder=null after the {101.9}/{101.10} alias fix. The certification is captured as a mention, but no holder signal is stamped, so the cert is silently unattributed downstream. This is a genuine coverage gap (a class of subject the rule was never designed to attribute), not a resolution bug (where the subject is the client but the name fails to resolve — that was {101.9}).

The Task favours Option C: the classifier emits an explicit internal/external signal on the compliance subject, and holder_rule keys off the signal — robust against the false-self-attribution risk that the brittle string-match Option B carries. This RESEARCH investigates whether Option C is feasible across both extraction paths (TS classifyContent oracle + Python cocoindex pipeline), how the signal integrates with the existing holder-derivation passes, and how it composes with the PI-15 fail-closed alias model (ID-95) without re-introducing the silent-degrade-to-baseline failure mode.

Feasibility verdict (preview): Option C is feasible but its reliability hinges entirely on the disambiguation signal — distinguishing “internal function of the client” from “a third party’s / supplier’s internal function”. The recommended rule is a possessive-/authorship-scoped internal-subject tag emitted at the relationship source, consumed by holder_rule as a third self/supplier/internal branch, defaulting to null (not self) whenever the internal-vs-external signal is absent or ambiguous. Scope is attribution-rule + capture only; consumption/KG is explicitly out of scope.

  • In scope: the attribution rule (where the internal/external signal is produced) and capture (how holder_rule stamps entity_mentions.metadata.holder).
  • OUT of scope: how metadata.holder is later consumed — downstream MCP tools (get_certification_status), the entity-management UI, the compliance dashboard (components/dashboard/compliance-status-section.tsx), and any knowledge-graph projection. Those read the stamped metadata; this Task changes only what gets stamped. Consumption changes (if any) are a separate Task.

Problem — the 7e511dbc-class gap, precisely

Section titled “Problem — the 7e511dbc-class gap, precisely”

Current behaviour: holder=null for internal-function compliance

Section titled “Current behaviour: holder=null for internal-function compliance”

“Internal IT is compliant to ISO 27001 / Cyber Essentials Plus” is captured as a certification mention but receives no holder signal. The mechanics, verified against the live code in this worktree:

  1. The subject is never emitted as an organisation mention. lib/ai/skills/classification.md:390 lists “Internal departments: IT Department, HR Team, the project team, senior management” as an explicit extraction exclusion. So “Internal IT” is never an organisation entity mention — there is no client-org-named subject for the cert.

  2. The synonym fallback rejects the non-org source. Even when the classifier emits a complies_with/evidences relationship with source “Internal IT”, holder_rule’s Pass 2 accepts a synonym only when the source is the client org or an extracted organisation mention:

    • Python scripts/cocoindex_pipeline/holder_rule.py:159-171:
      source_is_client_org = source_c == client_org_lower
      source_is_extracted_org = source_c in org_sources
      if not source_is_client_org and not source_is_extracted_org:
      continue # "Internal IT" is neither → synonym rel DROPPED
    • TS oracle lib/ai/classify.ts:570-572 is structurally identical (sourceIsClientOrg / sourceIsExtractedOrg, if (!… && !…) continue;).

    “Internal IT” canonicalises to neither the configured client org nor any extracted organisation mention (point 1 guarantees the latter), so the synonym rel is dropped.

  3. No holds_by_target entry → no holder signal → holder=null. The stamp loop (holder_rule.py:176-194; TS classify.ts:579-593) only stamps certs with a resolved holder source. A cert with no entry is absent from the returned map — Inv-10’s deliberate “never default to self”:

    if holds_source is None:
    continue # Inv-10: no signal → leave untouched (never default 'self').

    So the cert mention keeps span-only metadata; metadata.holder is unset (null).

  4. Self-match can never fire for an internal-function subject. Self-attribution is holds_source == client_org_lower (holder_rule.py:187; TS classify.ts:583). client_org_lower is the relationship-canonicalised PIPELINE_CLIENT_ORG env knob (Python; R4 fail-fast at holder_rule.py:124-131) or BRANDING.organisationName.toLowerCase() (TS, classify.ts:529). An internal-function subject carries no client name or alias, so it never equals client_org_lower — self-match structurally cannot fire.

Net: internal-function compliance carries no client name/alias, never self-matches the client org, and the synonym fallback rejects its non-org source → holder=null.

Why this is DISTINCT from the {101.9} alias fix

Section titled “Why this is DISTINCT from the {101.9} alias fix”

{101.9}/{101.10} fixed a resolution bug: a short client name (“Acme”) that failed to resolve to the full registered form (“Acme Holdings Limited”) in the registered DB alias table, so the holder self-match (holder_rule.py:187) failed and the client’s own cert was mis-stamped holder='supplier' against a phantom supplier bearing the client’s short name (documented verbatim at canonicalisation.py:126-128). There, the subject WAS the client — it was simply unresolved; the {101.10} fail-closed DB-alias snapshot fixed resolution.

The internal-function class is different in kind: the subject is not the client at all (it is an internal function, deliberately excluded from organisation extraction). No amount of alias resolution helps, because there is no client-org name to resolve. This is a coverage gap — a class of subject the rule was never designed to attribute — confirmed by the {101.11} journal: “Coverage gap distinct from the DB-alias fix: internal-function subjects carry no client name/alias so never self-match the client org.”

A client document that says “Internal IT is compliant to ISO 27001” almost certainly means the client holds ISO 27001 via its own internal IT function. Today that cert lands with holder=null, so it is silently unattributed in every downstream consumer (compliance status, certification-status MCP tool, entity UI). A genuine client certification is effectively invisible to “what certs does {CLIENT_ORGANISATION_NAME} hold?” queries.


Per the binding pre-spec-write orientation step. gitnexus_query /gitnexus_context were run against repo: knowledge-hub (canonical-pipeline-setup); for the Python pipeline files (ast-dataflow does not cover Python) grep sweeps were run over scripts/cocoindex_pipeline/*.py.

gitnexus_context({name: 'deriveHolderMetadata'}) — verbatim

Section titled “gitnexus_context({name: 'deriveHolderMetadata'}) — verbatim”
  • Symbol: Function:lib/ai/classify.ts:deriveHolderMetadata, lines 524-595.
  • incoming.calls: classifyContent (lib/ai/classify.ts) — the sole caller.
  • outgoing.calls: resolveAlias (lib/entities/entity-aliases.ts), canonicalise (lib/entities/entity-dedup.ts).

This confirms the TS path’s canonicalisation is resolveAlias(canonicalise(...)) (the baseline + DB alias map), distinct from the Python port’s canonicalise_for_relationship.

gitnexus_query({query: 'holder attribution certification compliance internal'}) — verbatim definitions

Section titled “gitnexus_query({query: 'holder attribution certification compliance internal'}) — verbatim definitions”

The query returned no execution processes (the holder rule is a pure function with no indexed call-chain process), but surfaced these definitions:

  • scripts/cocoindex_pipeline/holder_rule.pyderive_holder_metadata (the Python port).
  • scripts/cocoindex_pipeline/parity_driver.pybuild_holder_states (120-142), build_holder_diagnostics (145-198) (cross-path parity harness).
  • scripts/tests/test_holder_rule_parity.pytest_self_attribution (68-75), test_supplier_disclaimer_attribution (81-95) (the six-case parity suite).
  • scripts/tests/test_holder_rule_alias_resolution.pytest_holder_self_with_short_name_alias (114-132), test_baseline_only_misattributes_short_name (138-166) (the {101.9} fix tests).
  • scripts/eval-holder-rule-ts.tsresolveClientOrgLower (110-113), the TS eval harness.
  • components/dashboard/compliance-status-section.tsxComplianceStatusSection (42-191), CertificationReport (19-30) (downstream consumer — OUT of scope, but confirms the metadata.holder read site).

grep sweep — Python prompt + holder surfaces (cited file:line)

Section titled “grep sweep — Python prompt + holder surfaces (cited file:line)”
  • Exclusion source (TS skill): lib/ai/skills/classification.md:390 — “Internal departments: IT Department, HR Team, the project team, senior management”.
  • Python prompt surface: scripts/cocoindex_pipeline/prompts.pyENTITY_MENTION_PROMPT (139), RELATIONSHIP_PROMPT (194). The holder-disambiguation block is prompts.py:226-272. Finding: the Python ENTITY_MENTION_PROMPT does not carry an explicit “Internal departments” exclusion line (the TS classification.md:390 exclusion has no exact Python prompt analogue) — a latent cross-path divergence noted for the PRODUCT/TECH phases.
  • Holder rule (Python): holder_rule.pyHOLDS_SYNONYMS (96), Pass-1 holds (137-141), Pass-2 synonym gate (159-171), self-match stamp (187-193), Inv-10 absent-on-no-signal (181-182), R4 fail-fast (124-131).
  • Holder rule (TS oracle): classify.tsHOLDS_SYNONYMS (522), deriveHolderMetadata (525), client org via BRANDING.organisationName.toLowerCase() (529), Pass-1 (536-542), Pass-2 gate (563-576), self-match (583-589).
  • PI-15 fail-closed boot: flow.py:3656-3730 _generate_client_alias_snapshot (the {101.10} deploy gate), prime_alias_cache_from_db_rows import at flow.py:100, holder-rule call inside the Inv-15 best-effort try/except at flow.py:2240-2262.
  • Canonicalisation: canonicalisation.pycanonicalise_for_relationship, prime_alias_cache_from_db_rows, _get_alias_map (325-336, baseline overlaid by DB snapshot), graceful baseline-only degrade in the file loader (_load_db_entity_aliases, 294-322) which is the dev/CI fallback only — the runtime fail-closed gate (RuntimeError predicate) is flow.py:3697-3708.
document content
→ [extraction] entity mentions + relationship triples (LLM)
TS: classifyContent → lib/ai/skills/classification.md prompt
Python: extraction.py → prompts.py (ENTITY_MENTION_PROMPT, RELATIONSHIP_PROMPT)
→ [holder rule] derive holder per cert from holds/synonym rels
TS: deriveHolderMetadata (classify.ts:525)
Python: derive_holder_metadata (holder_rule.py:99)
→ [stamp] metadata.holder ∈ {self, supplier(+supplier_name), <absent=null>}
written onto entity_mentions.metadata jsonb

The internal/external signal, if introduced, must be produced in the extraction stage (the only stage that sees document content and authorship context) and consumed in the holder rule stage. Both stages are duplicated TS↔Python and must stay at parity.


Candidate attribution rules with trade-offs

Section titled “Candidate attribution rules with trade-offs”

Option A — status quo (leave holder=null)

Section titled “Option A — status quo (leave holder=null)”

Do nothing; internal-function compliance stays unattributed.

  • When acceptable: if the product call is that internal-function self-attribution is not desired (e.g. false-positive cost outweighs the recall gain), null is the correct, honest signal — Inv-10’s “never default to self” is doing its job.
  • Cost: genuine client certs expressed via internal functions are invisible downstream (the user-facing impact above). This is the current state and the documented gap.

Option B — extraction-time string-match on internal-function phrases → attribute self

Section titled “Option B — extraction-time string-match on internal-function phrases → attribute self”

At extraction or holder-rule time, match phrases like “Internal IT”, “our IT team”, “in-house”, “internal team” and attribute the cert to self.

  • Pro: simple; no schema/prompt-contract change; localisable to one matcher.

  • FALSE-SELF-ATTRIBUTION risk (the disqualifier): a brittle phrase match cannot tell whose internal function it is. Consider:

    • A supplier’s internal function described in the client’s document: “Our datacentre partner’s internal security team is compliant to ISO 27001” — “internal … team” matches, but the holder is the supplier, not the client. Mis-attributed to self.
    • A disclaimer-scoped third party: a cert under a “held by Example Datacentre, not {CLIENT_ORGANISATION_NAME}” disclaimer (classification.md:738-747) that happens to use “internal” phrasing would be yanked back to self, regressing the existing supplier-disclaimer rule.
    • Possessive ambiguity: “internal IT” with no possessive (“our”) is genuinely ambiguous — string-match has no signal to resolve it.

    Because this risks false attribution of supplier certs to the client — the exact failure mode the existing supplier-disclaimer disambiguation (classification.md:719-778) was built to prevent — Option B is not recommended. The Task description names it as the brittle alternative for precisely this reason.

Option C — classifier emits an internal/external tag; holder_rule keys off the tag (TASK-FAVOURED)

Section titled “Option C — classifier emits an internal/external tag; holder_rule keys off the tag (TASK-FAVOURED)”

The classifier, which sees the full document and authorship context, tags the compliance subject internal-vs-external at extraction; holder_rule consumes the tag.

  • Where the tag is emitted (the key design question — three sub-options):

    • C1 — relationship-level field on source. The relationship extractor already decides the source of a holds/complies_with rel and already runs the supplier-disclaimer disambiguation. Add an optional field (e.g. source_scope: "internal" | "external" | null, or a sentinel canonical source value like "__internal__") that marks “this holds/complies_with source is the document author’s own internal function”. This lives exactly where the holder decision is already made, composes naturally with the supplier-disclaimer rule (the disclaimer sets external; an unqualified “our internal IT” sets internal), and holder_rule reads it as a third branch (internal → self).
    • C2 — mention-level tag. Tag the certification mention itself (internal_function: true). Weaker: the mention has no authorship context and no link to whose function — it re-introduces the Option-B ambiguity at mention level.
    • C3 — new internal_function pseudo-entity. Emit “Internal IT” as a tagged pseudo-organisation scoped to the author. Heavier (new entity-type semantics, dedup, storage) and out of proportion to the gap.

    C1 is the recommended locus — see Recommended rule.

  • How holder_rule consumes it: a third branch in the stamp loop. Today (holder_rule.py:187-193): self if source == client_org_lower, else supplier. With C1, before the supplier branch: if the resolved source is tagged internal (and the supplier-disclaimer did NOT mark it external), stamp {"holder": "self", "holder_basis": "internal_function"} (a provenance breadcrumb for auditability). Absent tag → unchanged behaviour (Inv-10 null preserved).

  • Composition with PI-15: the internal tag is orthogonal to alias resolution. An internal-function self-attribution does not depend on the client name appearing in the text (that is the whole point), so it neither relies on nor regresses the {101.9}/{101.10} alias-driven self-match. Crucially, the new rule must not widen the fail-closed surface: PI-15’s _generate_client_alias_snapshot gate (flow.py:3690-3708) stays as-is; the internal tag is consumed after canonicalisation and does not touch the alias snapshot. (See “Composition constraints” below.)

  • Pro: robust — the disambiguation lives in the LLM stage that has the context to make it; composes with the existing supplier-disclaimer rule; preserves Inv-10 (null when no tag). Con: requires a prompt-contract change at both paths (TS classification.md

    • Python prompts.py) and a schema/field addition with cross-language parity — the bulk of the implementation cost.

Option D — author-org default within a disclaimer-free scope (variant worth noting)

Section titled “Option D — author-org default within a disclaimer-free scope (variant worth noting)”

A narrower form of C1: when a cert has a complies_with/evidences rel whose source is an internal-function phrase and the document is within a disclaimer-free scope and the possessive framing is first-person (“our”/“we”), default the holder to the document author org (= client, since KH ingests the client’s own corpus). This avoids a new tag by leaning on the existing supplier-disclaimer scoping plus possessive detection.

  • Pro: smaller prompt change. Con: relies on “document author = client” always holding (true for the client’s own corpus, but fragile if mixed-provenance docs are ever ingested); the possessive/disclaimer heuristics are softer than an explicit tag. Treat as a fallback if Option C’s tag proves too costly.

The single hardest question, on which Option C’s reliability rests: what signal reliably distinguishes “internal function OF THE CLIENT” from “a third party’s / supplier’s internal function”? Candidate signals, ranked by reliability:

  1. Supplier-disclaimer scoping (strongest, already exists). The existing rule (classification.md:719-778 / prompts.py:226-272) already attributes certs under a “held by [party], not [author]” disclaimer to the named third party. An internal-function tag should fire only when no supplier/disclaimer signal is in scope — the disclaimer wins. This reuses a battle-tested rule and prevents the Option-B regression.
  2. Possessive / first-person framing (“our IT”, “our internal team”, “we maintain”). First-person possessive strongly implies the document author (= client). “the internal IT team” (definite article, no possessor) is weaker; a named third party’s internal function (“Datacentre-X’s internal team”) is clearly external.
  3. Document authorship / provenance. KH ingests the client’s own corpus, so the document author is the client by default. This is a strong prior but not a guarantee for mixed-provenance documents (RFP responses quoting suppliers, due-diligence packs). Reliable as a default, not as a sole signal.
  4. Absence of a named third party in the cert’s sentence/section. “Internal IT is compliant to ISO 27001” with no named external party nearby implies the author’s own function. Useful as a corroborating signal, weak alone.
  5. Section context. A “Our certifications” / “Compliance” section vs a “Subcontractors” / “Supply chain” section. Useful but inconsistently present.

Concrete reliability call: signals (1)+(2) together — fire internal→self only when (a) no supplier-disclaimer is in scope for the cert AND (b) the subject is first-person possessive or an unqualified internal-department phrase with no named third party nearby — give a high-precision rule. Signal (3) (authorship = client) is the safety net that makes “internal IT” (without explicit possessive) resolvable to self in the common case, while (1) prevents the supplier-cert false positive. When (1) and (2)/(3) conflict or are both absent, the rule must abstainnull (Inv-10 honoured). This abstention is what makes Option C safe where Option B is not.


Adopt Option C, sub-option C1 (relationship-source internal scope tag), with the supplier-disclaimer rule as the dominant guard and null-on-ambiguity abstention.

  1. Where the signal is produced — the LLM extraction stage, at the relationship source. Extend the holder-disambiguation block in both prompt surfaces (lib/ai/skills/classification.md:719-778 and scripts/cocoindex_pipeline/prompts.py:226-272) to instruct: when a certification’s holder is the document author’s own internal function (first-person possessive, or an unqualified internal-department subject with no supplier/disclaimer signal in scope), emit the holds/complies_with rel with a marked-internal source. Mechanism choice (a dedicated source_scope field vs a reserved canonical source sentinel) is a TECH decision; PRODUCT defines the behaviour. The supplier-disclaimer rule takes precedence: if a disclaimer scopes the cert to a third party, the source is that third party (external), never internal.
  2. How holder_rule integrates it — a third stamp branch, before the supplier branch. In both derive_holder_metadata (holder_rule.py:176-194) and deriveHolderMetadata (classify.ts:579-593): if the resolved holder source is tagged internal → stamp {"holder": "self", "holder_basis": "internal_function"}. Else fall through to the existing self/supplier logic. No-tag certs stay absent from the map → holder=null unchanged (Inv-10 preserved). The holder_basis breadcrumb distinguishes name-resolved self from internal-function self for auditing (and lets a future consumption Task treat them differently if desired).
  3. Cross-language (TS + Python) parity — mandatory. The signal must be emitted identically by both prompt surfaces and consumed identically by both holder rules. The existing parity harness (parity_driver.py build_holder_states/build_holder_diagnostics, the eval-holder-rule cross-path eval, and test_holder_rule_parity.py) must be extended with internal-function cases on both sides. Also resolve the latent prompt divergence found in orientation: the Python ENTITY_MENTION_PROMPT lacks the “Internal departments” exclusion line that classification.md:390 has — the PRODUCT/TECH phases must decide whether to align them (the internal-function subject must remain a non-organisation, so the exclusion should hold on both paths; the new signal rides on the relationship, not on promoting “Internal IT” to an org mention).
  4. Short-term holder=null stays until shipped. Until the C1 rule lands on both paths with parity proven, internal-function compliance continues to stamp holder=null (no behaviour change is forced before the full parity slice is complete). This is the honest interim state and matches Inv-10; nothing in this Task should flip behaviour partially on one path only.

Why C1 over the alternatives: the relationship source is where the holder decision is already made and where the supplier-disclaimer guard already lives — so the internal tag composes with the existing guard for free, keeps “Internal IT” out of organisation extraction (no entity-type churn), and preserves Inv-10’s null-on-no-signal contract. C2 re-introduces ambiguity; C3 is over-engineered; B is unsafe.


Regression-test shape (de-identified / synthetic)

Section titled “Regression-test shape (de-identified / synthetic)”

Mirror the existing {101.8}/{101.9} synthetic-fixture pattern in scripts/tests/test_holder_rule_parity.py (duck-typed _Mention/_Rel @dataclass stand-ins; _CLIENT_ORG = canonicalise_for_relationship("Knowledge Hub Ltd"); pure-function asserts, no LLM/DB). All fixtures synthetic and de-identified — real client snapshots are gitignored (entity_aliases_snapshot.json), only synthetic de-id’d tests are committed. Use the placeholder {CLIENT_ORGANISATION_NAME}, “Internal IT”, and generic certs (ISO 27001 / Cyber Essentials Plus / ISO 9001).

Positive case (the gap closes):

  • internal-function → self: an internal-tagged holds/complies_with rel for “Internal IT compliant to ISO 27001”, no supplier-disclaimer in scope → stamp {"holder": "self", "holder_basis": "internal_function"}. (7e511dbc-class.)

Negative cases that MUST stay null/supplier (regression guards — the Option-B failures):

  • supplier-disclaimer-scoped cert stays supplier: cert under “held by Example Datacentre, not {CLIENT_ORGANISATION_NAME}” that also uses “internal” phrasing → holder=supplier, supplier_name=example datacentre (disclaimer wins; internal tag must NOT fire).
  • named third-party internal function stays supplier/null: “Datacentre-X’s internal team is compliant to ISO 27001” → NOT self (external named party).
  • ambiguous / no signal stays null: “the IT team holds ISO 27001” with no possessive, no disclaimer, no authorship cue → holder=null (Inv-10 abstention).
  • non-cert never stamped and no-signal cert absent from map — existing Inv-10/Inv-14 guards must continue to hold with the new branch added.

Cross-language golden parity: the same fixture matrix must pass on both the Python derive_holder_metadata (scripts/tests/test_holder_rule_parity.py) and the TS deriveHolderMetadata (the vitest/eval counterpart used by eval-holder-rule-ts.ts and the {101.9} comparator suite). Add an internal-function row to the cross-path comparator so a divergence between paths fails CI.

Prompt-contract tests (if the field is structured): if a source_scope field is added, extend the extraction-output schema/Zod validation tests and a golden prompt-output fixture proving the LLM emits the internal tag for the canonical 7e511dbc-class input and withholds it under a disclaimer.

Test file locations: scripts/tests/test_holder_rule_parity.py (extend the existing six-case suite) + the TS counterpart alongside the existing holder-rule vitest tests; the cross-path comparator in/around scripts/cocoindex_pipeline/parity_driver.py’s test suite (scripts/tests/test_parity_driver.py).


Composition constraints (PI-15 — must not regress)

Section titled “Composition constraints (PI-15 — must not regress)”
  • The internal tag is consumed after canonicalisation and does not read or write the alias snapshot — so it neither depends on nor weakens the {101.9}/{101.10} alias-driven self-match.
  • Must NOT re-introduce silent-degrade-to-baseline. PI-15’s fail-closed gate (flow.py:3690-3708: configured client + zero provenance='client' rows → RuntimeError, deploy fails) stays untouched. The new rule must not add a code path that, on missing signal, silently self-attributes (that would be Option B’s failure mode wearing Option C’s clothes) — null-on-ambiguity is mandatory.
  • The R4 fail-fast (holder_rule.py:124-131, unset PIPELINE_CLIENT_ORGValueError) must continue to hold; the internal branch runs after the client-org guard, not around it.

Open questions for Liam (product calls — do NOT block authoring)

Section titled “Open questions for Liam (product calls — do NOT block authoring)”
  1. Is internal-function self-attribution the desired product behaviour at all? This is the core call. If “Internal IT is compliant to ISO 27001” should be attributed to the client → adopt Option C. If the false-positive risk is judged unacceptable for the knowledge-base’s trust bar → Option A (keep null) is the right answer and this Task closes as “won’t fix, documented”.
  2. False-positive tolerance. Option C abstains (null) on ambiguity by design. Is the recall gain (capturing genuine internal-function client certs) worth the residual risk of a rare mis-attribution when authorship/possessive signals mislead? Where is the precision/recall line?
  3. Confidence threshold / explicitness bar. Should the internal tag fire only on explicit first-person possessive (“our internal IT”), or also on bare “Internal IT” leaning on the authorship-is-client prior (signal 3)? The latter raises recall and risk.
  4. holder_basis provenance breadcrumb — keep it? Stamping holder_basis: "internal_function" distinguishes internal-function self from name-resolved self for downstream consumers/audit. Worth the extra metadata field, or stamp a plain {"holder": "self"} identical to name-resolved self?
  5. Mixed-provenance documents. The “document author = client” prior (signal 3) is safe for the client’s own corpus but weaker for ingested RFP responses / due-diligence packs that quote suppliers. Is mixed-provenance ingest in scope for KH now or soon? If yes, the rule should lean harder on signals (1)+(2) and weight (3) less.
  6. Field mechanism (informs TECH, not a pure product call). Dedicated source_scope field on the relationship vs a reserved canonical source sentinel — Liam’s preference on schema-surface change vs prompt-only change can steer the TECH decision.

Section titled “Feasibility verdict + recommended PRODUCT scope”

Verdict: Option C is feasible. The classifier can reliably tag a compliance subject internal-vs-external when the rule is anchored on the existing supplier-disclaimer guard (signal 1) + possessive/authorship signals (2)+(3) and abstains to null on ambiguity. holder_rule can key off the tag at TS↔Python parity as a third stamp branch, because the holder decision and the disclaimer guard already live at the relationship source on both paths, the parity harness already exists, and the change preserves Inv-10 and composes orthogonally with the PI-15 alias model. The reliability ceiling is set by the disambiguation signal, not by the mechanism — and the supplier-disclaimer-dominant + abstain-on-ambiguity design keeps it high-precision (safe) at the cost of some recall on genuinely ambiguous inputs.

Recommended PRODUCT ({109.2}) scopepending the OQ-1 product call:

  • Behaviour: define internal-function self-attribution as a new holder class, gated by the supplier-disclaimer-dominant + possessive/authorship + abstain-on-ambiguity rule. Numbered, testable Behavior invariants for: (a) internal→self positive, (b) disclaimer-wins negative, (c) named-third-party negative, (d) ambiguous→null, (e) Inv-10 no-signal→null preserved, (f) TS↔Python parity, (g) short-term holder=null until shipped.
  • TECH ({109.3}) will cover: the field mechanism (source_scope vs sentinel), the twin prompt-surface edits (classification.md + prompts.py) at byte-stable parity, the holder_rule third-branch implementation on both paths, resolving the ENTITY_MENTION_PROMPT exclusion divergence, and the parity-harness + golden-test extensions.
  • Out of scope (restate): consumption/KG — no changes to dashboards, MCP tools, or entity UI; this Task changes only what metadata.holder carries at capture.

If OQ-1 resolves to “do not attribute”: close ID-109 as Option A (documented null), no code change beyond optionally documenting the deliberate gap.