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 frombl-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).
Summary
Section titled “Summary”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.
Scope boundary (explicit)
Section titled “Scope boundary (explicit)”- 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.holderis 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:
-
The subject is never emitted as an organisation mention.
lib/ai/skills/classification.md:390lists “Internal departments: IT Department, HR Team, the project team, senior management” as an explicit extraction exclusion. So “Internal IT” is never anorganisationentity mention — there is no client-org-named subject for the cert. -
The synonym fallback rejects the non-org source. Even when the classifier emits a
complies_with/evidencesrelationship with source “Internal IT”,holder_rule’s Pass 2 accepts a synonym only when the source is the client org or an extractedorganisationmention:- Python
scripts/cocoindex_pipeline/holder_rule.py:159-171:source_is_client_org = source_c == client_org_lowersource_is_extracted_org = source_c in org_sourcesif 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-572is structurally identical (sourceIsClientOrg/sourceIsExtractedOrg,if (!… && !…) continue;).
“Internal IT” canonicalises to neither the configured client org nor any extracted
organisationmention (point 1 guarantees the latter), so the synonym rel is dropped. - Python
-
No
holds_by_targetentry → no holder signal →holder=null. The stamp loop (holder_rule.py:176-194; TSclassify.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.holderis unset (null). -
Self-match can never fire for an internal-function subject. Self-attribution is
holds_source == client_org_lower(holder_rule.py:187; TSclassify.ts:583).client_org_loweris the relationship-canonicalisedPIPELINE_CLIENT_ORGenv knob (Python; R4 fail-fast atholder_rule.py:124-131) orBRANDING.organisationName.toLowerCase()(TS,classify.ts:529). An internal-function subject carries no client name or alias, so it never equalsclient_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.”
User-facing impact
Section titled “User-facing impact”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.
Code-intelligence orientation (cited)
Section titled “Code-intelligence orientation (cited)”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.py—derive_holder_metadata(the Python port).scripts/cocoindex_pipeline/parity_driver.py—build_holder_states(120-142),build_holder_diagnostics(145-198) (cross-path parity harness).scripts/tests/test_holder_rule_parity.py—test_self_attribution(68-75),test_supplier_disclaimer_attribution(81-95) (the six-case parity suite).scripts/tests/test_holder_rule_alias_resolution.py—test_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.ts—resolveClientOrgLower(110-113), the TS eval harness.components/dashboard/compliance-status-section.tsx—ComplianceStatusSection(42-191),CertificationReport(19-30) (downstream consumer — OUT of scope, but confirms themetadata.holderread 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.py—ENTITY_MENTION_PROMPT(139),RELATIONSHIP_PROMPT(194). The holder-disambiguation block isprompts.py:226-272. Finding: the PythonENTITY_MENTION_PROMPTdoes not carry an explicit “Internal departments” exclusion line (the TSclassification.md:390exclusion has no exact Python prompt analogue) — a latent cross-path divergence noted for the PRODUCT/TECH phases. - Holder rule (Python):
holder_rule.py—HOLDS_SYNONYMS(96), Pass-1holds(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.ts—HOLDS_SYNONYMS(522),deriveHolderMetadata(525), client org viaBRANDING.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_rowsimport atflow.py:100, holder-rule call inside the Inv-15 best-effort try/except atflow.py:2240-2262. - Canonicalisation:
canonicalisation.py—canonicalise_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) isflow.py:3697-3708.
The call-chain (both paths)
Section titled “The call-chain (both paths)”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 jsonbThe 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),
nullis 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 thesourceof aholds/complies_withrel and already runs the supplier-disclaimer disambiguation. Add an optional field (e.g.source_scope: "internal" | "external" | null, or a sentinel canonicalsourcevalue like"__internal__") that marks “thisholds/complies_withsource 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 setsexternal; an unqualified “our internal IT” setsinternal), andholder_rulereads it as a third branch (internal → self). - C2 — mention-level tag. Tag the
certificationmention 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_functionpseudo-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.
- C1 — relationship-level field on
-
How holder_rule consumes it: a third branch in the stamp loop. Today (
holder_rule.py:187-193):selfifsource == client_org_lower, elsesupplier. With C1, before the supplier branch: if the resolved source is taggedinternal(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-10nullpreserved). -
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_snapshotgate (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 (
nullwhen no tag). Con: requires a prompt-contract change at both paths (TSclassification.md- Python
prompts.py) and a schema/field addition with cross-language parity — the bulk of the implementation cost.
- Python
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 disambiguation signal (the crux)
Section titled “The disambiguation signal (the crux)”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:
- 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. - 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.
- 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.
- 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.
- 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 abstain → null (Inv-10 honoured). This abstention is what
makes Option C safe where Option B is not.
Recommended rule
Section titled “Recommended rule”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.
- 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-778andscripts/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 theholds/complies_withrel with a marked-internalsource. Mechanism choice (a dedicatedsource_scopefield vs a reserved canonicalsourcesentinel) 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. - How holder_rule integrates it — a third stamp branch, before the supplier branch.
In both
derive_holder_metadata(holder_rule.py:176-194) andderiveHolderMetadata(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=nullunchanged (Inv-10 preserved). Theholder_basisbreadcrumb distinguishes name-resolved self from internal-function self for auditing (and lets a future consumption Task treat them differently if desired). - 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.pybuild_holder_states/build_holder_diagnostics, theeval-holder-rulecross-path eval, andtest_holder_rule_parity.py) must be extended with internal-function cases on both sides. Also resolve the latent prompt divergence found in orientation: the PythonENTITY_MENTION_PROMPTlacks the “Internal departments” exclusion line thatclassification.md:390has — 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). - Short-term
holder=nullstays until shipped. Until the C1 rule lands on both paths with parity proven, internal-function compliance continues to stampholder=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-taggedholds/complies_withrel 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 stampedandno-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 + zeroprovenance='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, unsetPIPELINE_CLIENT_ORG→ValueError) 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)”- 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”. - 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? - 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.
holder_basisprovenance breadcrumb — keep it? Stampingholder_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?- 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.
- Field mechanism (informs TECH, not a pure product call). Dedicated
source_scopefield on the relationship vs a reserved canonicalsourcesentinel — Liam’s preference on schema-surface change vs prompt-only change can steer the TECH decision.
Feasibility verdict + recommended PRODUCT scope
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}) scope — pending 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=nulluntil shipped. - TECH (
{109.3}) will cover: the field mechanism (source_scopevs sentinel), the twin prompt-surface edits (classification.md+prompts.py) at byte-stable parity, the holder_rule third-branch implementation on both paths, resolving theENTITY_MENTION_PROMPTexclusion 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.holdercarries 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.