Skip to content

context-graph: T1/T2 enrichment + hypaware.completion capability - #110

Merged
philcunliffe merged 4 commits into
masterfrom
context-graph/enrichment-t1-t2
Jun 16, 2026
Merged

context-graph: T1/T2 enrichment + hypaware.completion capability#110
philcunliffe merged 4 commits into
masterfrom
context-graph/enrichment-t1-t2

Conversation

@philcunliffe

Copy link
Copy Markdown
Contributor

What

Adds the T1/T2 enrichment layer on top of the existing T0 context graph, plus a new hypaware.completion inference capability the tiers depend on.

This is the prototype of cgproto's LLP 0006 projection pipeline above T0: a recall-tuned proposer (T1) over-proposes prospect knowledge from source text, and a graph-and-source-aware curator (T2) prunes / merges / deepens / commits.

New capability: hypaware.completion

Text generation, mirroring the embedder pattern — its own capability, the provider is an explicit plugins[] choice, and a localhost base_url keeps inference on-machine. Two providers, both shipping:

  • @hypaware/completion-anthropic — native Claude Messages API (/v1/messages). One provider serves both tiers by per-call model (Haiku for T1, Opus for T2). Adaptive thinking + effort on the T2 path; refusals returned as stopReason, never thrown.
  • @hypaware/completion-openai — OpenAI-compatible /v1/chat/completions (OpenAI, proxies, Ollama/LM Studio).

Both resolve the API key from an env var at call time (never logged), support streaming, and use an injected fetch seam for tests.

New plugin: @hypaware/context-graph-enrich

One plugin, two daemon sources + commands. Requires hypaware.context-graph, hypaware.vector-search, and hypaware.completion.

  • enrich-propose (T1) — reads new source rows since a watermark, over-proposes prospects via a forced-tool extraction, writes enrichment_prospects.
  • enrich-curate (T2) — selects pending prospects ordered by salience (vector-distance novelty), then per prospect assembles the serve path — recall (vector-search) + expand (SQL over the published node/edge surface) + source deref (provenance slice) — and asks the curator to prune/merge/deepen/commit.
  • enrich propose / enrich curate / enrich status commands.

Key design decision: committed-only projection

The graph projector is append + dedup-by-id with no retract path (the incremental-vs-full-regeneration question is still open upstream). So the prospect lifecycle lives in this plugin's own datasets (enrichment_prospects + append-only enrichment_resolutions), and a contract projects only enrichment_committed into the graph. Rejected prospects never reach the graph — this is derive-don't-store applied to the enrichment layer.

Other notes

  • The completion + enrich plugins are excluded from default activation (model-provider opt-in), mirroring the embedder.
  • vector-search + completion are resolved lazily on first use: the dependency resolver orders by requires.plugins, not requires.capabilities, and the completion provider is swappable so it can't be named in requires.plugins. (Graph stays eager — needed for registerContract, and it's correctly ordered first.)

Testing

  • 47 new unit tests (completion providers ×28, enrichment ×19) — config validation, request/response mapping, SSE streaming, contract toRow, prompt parsing, id determinism. Injected fetch so no network.
  • Full suite green: 1102/1103 pass (1 pre-existing skip), typecheck + lint clean.
  • Verified end-to-end against a real ai_gateway_messages corpus with live Claude calls: Haiku T1 proposed 12 items → Opus T2 committed 3, rejected 1 → projected into the graph as Decision/Concept/Constraint nodes (carrying props.confidence) with produced edges, and the rejected prospect was confirmed absent from the graph.

Enabling

These are opt-in. Add to config, e.g.:

{ "name": "@hypaware/completion-anthropic", "config": { "api_key_env": "ANTHROPIC_API_KEY" } },
{ "name": "@hypaware/context-graph-enrich", "config": { "text_column": "content_text" } }

(also needs @hypaware/context-graph + an embedder/@hypaware/vector-search).

🤖 Generated with Claude Code

…mpletion capability
Adds the enrichment layer (T1/T2) on top of the T0 context graph, plus a
new inference capability the tiers need.
hypaware.completion (kernel types) — text generation, mirroring the
embedder (own capability; provider is an explicit plugins[] choice;
localhost base_url keeps it on-machine). Two providers, both v1:
- @hypaware/completion-anthropic — Claude /v1/messages (Haiku for T1,
Opus for T2 via per-call model).
- @hypaware/completion-openai — /v1/chat/completions (OpenAI/Ollama).
@hypaware/context-graph-enrich — one plugin, two daemon sources:
- enrich-propose (T1): recall-tuned over-proposer; writes prospects.
- enrich-curate (T2): salience-ordered (vector-distance novelty) ->
recall (vector-search) + expand (SQL over published node/edge) +
source deref -> prune/merge/deepen/commit.
The prospect lifecycle lives in the plugin's own datasets
(enrichment_prospects/_resolutions); a contract projects committed-only
knowledge into the graph, so rejected prospects never reach it
(derive-don't-store; the graph projector has no retract path yet).
Plus enrich propose/curate/status commands.
The completion + enrich plugins are excluded from default activation
(model-provider opt-in), mirroring the embedder. vector-search and
completion are resolved lazily (the resolver orders by requires.plugins,
not requires.capabilities, and the completion provider is swappable).
Verified end-to-end against a real ai_gateway_messages corpus: Haiku T1
proposed, Opus T2 committed/rejected, committed items projected as graph
nodes (with props.confidence) and produced edges, rejected prospects
absent. 47 unit tests; full suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
ContributorAuthor

Dual-agent review — block

  • Verdict:block
  • Risk class:high
  • Auto-merge advisory: 👎 thumbs down — verdict is block; needs human-gated follow-up

Advisory only: no merge was attempted.

Risk capstone

Cross-reference: reviewer findings vs high-risk surfaces

SourceFinding (severity, evidence)Intersects
Claudeblocker — default text_column missing (config.js:15)Config field chain; Risks #1
Claudemajor — NUL byte corrupts candidateKey (propose.js:88)Targets (runProposeTick)
Claudemajor — watermark skips source rows (propose.js:51-69,75,113)Concurrency surface; Risks #2
Claudemajor — runProposeTick/runCurateTick untested (propose.js:31)Targets; Risks #3
Claudemajor — state.js watermark untested (state.js:23)Concurrency surface; Risks #3
Claudeminor — @typedef style violation (state.js:17)Targets (state.js)
Claudeminor — inline import() type (commands.js:93)Direct callers (commands.js)
Claudeminor — sqlQuote untested (sql.js:54)Risks #5
Codexmajor — propose watermark skips rows (propose.js:41,51,75,113)Concurrency surface; Risks #2
Codexmajor — provider portability broken (kernel-types:1586, prompts.js:85,107, completion-openai/client.js:112,124)Cross-package usage; Risks #4
Codexminor — unvalidated SQL identifiers (config.js:60,150, propose.js:41,44, curate.js:225)Config field chain; Risks #5
Codexminor — NUL byte makes file binary (propose.js:88)Targets (runProposeTick)
Codex review

Fix Validations

No bug-fix claims to validate; this PR is feature/additive.

Findings

1) Behavioral Correctness

  • Severity: major
  • Confidence: high
  • Evidence:hypaware-core/plugins-workspace/context-graph-enrich/src/propose.js:41, hypaware-core/plugins-workspace/context-graph-enrich/src/propose.js:51, hypaware-core/plugins-workspace/context-graph-enrich/src/propose.js:75, hypaware-core/plugins-workspace/context-graph-enrich/src/propose.js:113, hypaware-core/plugins-workspace/context-graph-enrich/src/propose.js:143
  • Why it matters: T1 can permanently skip source rows because maxCursor is computed from all fetched rows before processing, then written even if the deadline breaks out before some groups are completed; timestamp-only WHERE ts > cursor also skips rows sharing the boundary timestamp.
  • Suggested fix: Store a tuple cursor such as (timestamp, id), order by both, and advance only to the last fully processed row/group; alternatively avoid writing the cursor on partial deadline exits.

2) Contract & Interface Fidelity

  • Severity: major
  • Confidence: high
  • Evidence:collectivus-plugin-kernel-types.d.ts:1586, hypaware-core/plugins-workspace/context-graph-enrich/src/prompts.js:85, hypaware-core/plugins-workspace/context-graph-enrich/src/prompts.js:107, hypaware-core/plugins-workspace/completion-openai/src/client.js:112, hypaware-core/plugins-workspace/completion-openai/src/client.js:124
  • Why it matters:@hypaware/context-graph-enrich is advertised as provider-swappable, but it sends Anthropic-native tool_choice, thinking, and output_config through the provider-specific params field; the OpenAI provider blindly forwards those params while translating tools to OpenAI function tools, so OpenAI-compatible enrichment will fail or not force structured output.
  • Suggested fix: Add a provider-neutral forced-tool/thinking abstraction to CompletionRequest and translate inside providers, or make enrichment branch on provider capability metadata instead of passing Anthropic params unconditionally.

6) Security Surface

  • Severity: minor
  • Confidence: high
  • Evidence:hypaware-core/plugins-workspace/context-graph-enrich/src/config.js:60, hypaware-core/plugins-workspace/context-graph-enrich/src/config.js:150, hypaware-core/plugins-workspace/context-graph-enrich/src/propose.js:41, hypaware-core/plugins-workspace/context-graph-enrich/src/propose.js:44, hypaware-core/plugins-workspace/context-graph-enrich/src/curate.js:225
  • Why it matters: Configured dataset and column names are accepted as any non-empty string and interpolated as SQL identifiers, so typos become runtime SQL failures and crafted config can alter the generated query.
  • Suggested fix: Validate identifier fields with a strict identifier/schema rule, or quote identifiers with a real SQL identifier helper instead of using raw strings.

8) Release Safety

  • Severity: minor
  • Confidence: high
  • Evidence:hypaware-core/plugins-workspace/context-graph-enrich/src/propose.js:88
  • Why it matters: The literal NUL separator makes propose.js appear as a binary file in git/rg output, hiding the changed source from normal diff and review tooling.
  • Suggested fix: Replace the literal NUL byte with an escaped separator like `${c.type}\\0${c.label}`.

No Finding

  1. Change Impact / Blast Radius; 4) Concurrency, Ordering & State Safety; 5) Error Handling & Resilience; 7) Resource Lifecycle & Cleanup; 9) Test Evidence Quality beyond gaps tied to the findings above; 10) Architectural Consistency; 11) Debuggability & Operability.

Evidence Bundle

  • Changed hot paths: completion capability types; Anthropic/OpenAI completion clients; enrichment activation; T1 propose loop; T2 curate loop; committed-only graph contract; bundled plugin discovery defaults.
  • Impacted callers:hypaware-core/plugins-workspace/context-graph-enrich/src/commands.js:37, hypaware-core/plugins-workspace/context-graph-enrich/src/commands.js:54, hypaware-core/plugins-workspace/context-graph-enrich/src/propose.js:143, hypaware-core/plugins-workspace/context-graph-enrich/src/curate.js:71, hypaware-core/plugins-workspace/context-graph-enrich/src/curate.js:278
  • Impacted tests:test/plugins/context-graph-enrich-prompts.test.js:13, test/plugins/context-graph-enrich-config.test.js:21, test/plugins/completion-openai-client.test.js:6, test/plugins/completion-anthropic-client.test.js:6; no direct tests for runProposeTick cursor/deadline behavior or OpenAI-backed enrichment requests.
  • Unresolved uncertainty: I did not run the suite; review was diff-first. The prompt omitted propose.js as binary, so I opened that file from the worktree for line evidence.
Claude review

Claude review

Default text_column: 'content' does not exist in the schema-v5 ai_gateway_messages dataset

  • Severity: blocker
  • Confidence: 95
  • Evidence: hypaware-core/plugins-workspace/context-graph-enrich/src/config.js:15
  • Why it matters:SOURCE_DEFAULTS defaults source_dataset: 'ai_gateway_messages' + text_column: 'content', but the projector schema (message_projector.js:60, schema v5 / commit 62f1a32) exposes content_text with no bare content column, so on the documented default config every propose SELECT ${text_column} and curate deref throws ColumnNotFoundError and the enrichment pipeline yields zero prospects (verified: no name: 'content' column exists; test config.test.js:13 even locks in the broken default).
  • Suggested fix: Change SOURCE_DEFAULTS.text_column to 'content_text', update the assertion in test/plugins/context-graph-enrich-config.test.js:13, and fix the stale "may be a string or an array of content blocks" comment in propose.js (post-v5 it is a part-level STRING).

Literal NUL byte in propose.js makes the file binary and corrupts the dedup key

  • Severity: major
  • Confidence: 92
  • Evidence: hypaware-core/plugins-workspace/context-graph-enrich/src/propose.js:88
  • Why it matters: Verified exactly one 0x00 byte at offset 0xf06 sitting between ${c.type} and ${c.label} in candidateKey where a space was intended — file reports the file as data, git renders it as a binary diff (the core T1 propose tick is invisible in pr.diff), ripgrep skips it by default, and the NUL poisons every prospectId hash; Node tolerates it so tests pass, making it a silent latent defect CI will not catch.
  • Suggested fix: Replace the NUL byte with a normal space so the literal reads `${c.type} ${c.label}`, then confirm file propose.js reports text and git diff shows it as text.

Propose watermark can silently skip source rows (two distinct mechanisms)

  • Severity: major
  • Confidence: 85
  • Evidence: hypaware-core/plugins-workspace/context-graph-enrich/src/propose.js:51-69,75,113
  • Why it matters:maxCursor is the max timestamp over all fetched rows and is written unconditionally (:113) even when the proposal loop breaks early on the per-tick deadline (:75, default 60s), so un-proposed groups' rows are permanently excluded by the next tick's WHERE ts > cursor; additionally, because ai_gateway_messages is part-level (many parts share one message_created_at), a strict-> cursor combined with LIMIT max_rows_per_tick that truncates mid-message permanently drops the remaining same-timestamp parts — both are silent source-row loss. (Codex independently flagged the same defect, category 1.)
  • Suggested fix: Advance the cursor only over groups actually processed before the break (the already-collected g.maxTs is unused), and use a strictly monotonic compound cursor (message_created_at, part_id) with >= + in-tick dedup so boundary parts are not skipped.

Core enrichment transforms runProposeTick / runCurateTick have zero test coverage

  • Severity: major
  • Confidence: 88
  • Evidence: hypaware-core/plugins-workspace/context-graph-enrich/src/propose.js:31
  • Why it matters: CLAUDE.md requires traditional tests for deterministic logic, yet the two most material new transforms — anchor grouping, prospect-id dedup, cursor advance (propose) and decision routing reject/merge/commit/deepen into committed-vs-resolution rows (curate) — have no test and no smoke flow; this is not academic, the watermark bug above lives in exactly this untested code, and the completion capability is injected via the runtime object so a stub runtime makes both tick functions unit-testable.
  • Suggested fix: Add context-graph-enrich-propose.test.js and -curate.test.js driving the tick functions with a fake runtime (stub getCompletion returning canned tool_use, in-memory runSql/appendRows); assert dedup + cursor advance in propose and reject-vs-commit row shapes in curate.

Watermark persistence in state.js is untested

  • Severity: major
  • Confidence: 85
  • Evidence: hypaware-core/plugins-workspace/context-graph-enrich/src/state.js:23
  • Why it matters:readState/writeState is exactly the "path helpers / local contracts" deterministic logic CLAUDE.md calls out, with real branches (schema_version === 1 gating, malformed-JSON fallback, type coercion, atomic temp-then-rename) where a wrong watermark either reprocesses or silently skips rows, and it is pure temp-dir filesystem I/O that is trivially testable.
  • Suggested fix: Add context-graph-enrich-state.test.js round-tripping state through a tmpdir and asserting fallback for missing, malformed, and wrong-schema_version files.

Forbidden @typedef in JSDoc violates CLAUDE.md code style

  • Severity: minor
  • Confidence: 88
  • Evidence: hypaware-core/plugins-workspace/context-graph-enrich/src/state.js:17
  • Why it matters: CLAUDE.md states verbatim "Do not use @typedef in JSDoc. Define shared types as interfaces in .d.ts files" — this new line declares EnrichStateFile as a @typedef even though the directory already has a src/types.d.ts that is the correct home.
  • Suggested fix: Move EnrichStateFile into context-graph-enrich/src/types.d.ts as an export interface, delete the @typedef, and reference it via an @import block.

Forbidden inline import('...') type annotation violates CLAUDE.md code style

  • Severity: minor
  • Confidence: 85
  • Evidence: hypaware-core/plugins-workspace/context-graph-enrich/src/commands.js:93
  • Why it matters: CLAUDE.md states "Never use inline import('...') types. Declare type imports at the top of the file with @import" — this @param {import('./types.d.ts').EnrichRuntime} is inconsistent even within its own file, which already declares CommandRunContext correctly via an @import block.
  • Suggested fix: Add EnrichRuntime to the existing top-of-file @import block and change the annotation to the bare @param {EnrichRuntime} runtime.

sqlQuote SQL-literal escaping is untested

  • Severity: minor
  • Confidence: 82
  • Evidence: hypaware-core/plugins-workspace/context-graph-enrich/src/sql.js:54
  • Why it matters:sqlQuote is the only escaping guard for values interpolated into the hand-built SQL in propose/curate (anchor ids, node ids, source keys, cursor); it is the pure "SQL/TOML transform" category CLAUDE.md requires tests for, and an off-by-one in the ''' replacement would be a correctness/injection bug.
  • Suggested fix: Add a test asserting sqlQuote("a'b") === "a''b", idempotency, and that isMissingDatasetError matches ENOENT/unknown dataset but not arbitrary errors.

Reports: .git/dual-review/pr-110

philcunliffeand others added 2 commits June 15, 2026 15:34
…ead once)
T2 previously curated one prospect per Opus call, each re-reading the same
session source via the prospect's provenance — so N prospects from one
session sent that session's excerpt N times.
Group the selected prospects by anchor (session) and make ONE curate call
per group: the graph neighborhood and the union of the group's source rows
are read once and shared; the model returns one decision per prospect keyed
by 1-based index. curate_decision -> curate_decisions (array), buildCurateRequest
-> buildCurateBatchRequest, parseDecision -> parseDecisions; max_tokens scales
with group size; an empty/refused response leaves the group pending to retry.
Verified: 4 prospects across 2 sessions now curate in 2 calls instead of 4
(a 6-prospect session collapses 6 calls -> 1), cutting Opus input
proportionally.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tability, config, tests)
Fixes the dual-review block findings on the T1/T2 enrichment + completion work.
Correctness:
- Default text_column 'content' -> 'content_text'; schema v5 exposes the
per-part text as content_text (there is no bare 'content' column).
- Propose watermark no longer skips source rows. The old `WHERE ts > 'cursor'`
never advanced: the engine surfaces a TIMESTAMP column as a Date and only
compares it against a numeric epoch literal (string/`=` match nothing). The
cursor is now a keyset tuple {ts: epochMillis, id: part_id}: a coarse
`ts >= cursorMs` filter that includes the boundary millisecond, an exact
(ts, part_id) keyset drop in JS, and advance only over the processed prefix
on a deadline break (no boundary-part loss, no skips).
- Removed the literal NUL byte in propose.js (file was binary in diffs/rg).
Provider portability:
- New provider-neutral CompletionRequest.toolChoice ('auto' | 'required' |
{name}); each provider translates to its native tool_choice. T1 forces its
tool portably; T2 is provider-aware (Anthropic keeps thinking/effort, others
force the tool). The completion capability is now genuinely interchangeable.
Hardening + style:
- Validate configured dataset/column fields as strict SQL identifiers.
- Move EnrichStateFile / CurateDecision to types.d.ts interfaces; drop the
@typedef and the inline import() type (CLAUDE.md style).
Tests:
- Extract pure helpers (buildProposeQuery, groupSourceRows, nextProposeCursor,
collectProspectRows, routeDecision) and cover them; new state/sql tests;
toolChoice translation tests for both completion providers.
Full suite green (1135 pass, 1 pre-existing skip), typecheck + lint clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
ContributorAuthor

Dual-agent review — request_changes

  • Verdict:request_changes
  • Risk class:medium
  • Auto-merge advisory: 👎 thumbs down — verdict is request_changes; needs human-gated follow-up

Advisory only: no merge was attempted.

Risk capstone

Cross-reference: reviewer findings vs high-risk surfaces

SourceFinding (severity, evidence)Intersects
Codexmajor — cross-tick prospect_id non-idempotency (propose.js:63,65,188; curate.js:41,123)Concurrency surface; Risk #1 (duplicate commits/resolutions/spend)
Codexminor — runSql empty-result fallback masks bad source_dataset (sql.js:19,29; propose.js:41)Config field chain; Risk #2 (silent no-op)
Claudeminor — inline import('...') type in test (propose.test.js:14)none of the high-risk surfaces (style nit)
Claude (sub-80)major — runProposeTick/runCurateTick orchestration untestedConcurrency surface; Risk #3 (orchestration test gap)
Claude (sub-80)minor — salience_threshold>0 strands pending prospects (curate.js:173)Concurrency surface; Risk #4 (pending-queue drainage)
Codex review

Fix Validations

No explicit bug-fix failure mode was claimed; this is a feature PR, so there are no fix validations.

Findings

4) Concurrency, Ordering & State Safety

  • Severity: major
  • Confidence: high
  • Evidence:hypaware-core/plugins-workspace/context-graph-enrich/src/propose.js:63, hypaware-core/plugins-workspace/context-graph-enrich/src/propose.js:65, hypaware-core/plugins-workspace/context-graph-enrich/src/propose.js:188, hypaware-core/plugins-workspace/context-graph-enrich/src/curate.js:41, hypaware-core/plugins-workspace/context-graph-enrich/src/curate.js:123
  • Why it matters: Deterministic prospect_ids only dedupe within the current tick, but retries/reprocessing append the same IDs again, and T2 later processes every duplicate row, producing duplicate resolutions, duplicate committed rows, and extra model calls.
  • Suggested fix: Make prospect append idempotent across persisted rows, or at minimum dedupe pending by prospect_id before curation; ideally filter newRows against existing enrichment_prospects.prospect_id before appendRows.

11) Debuggability & Operability

  • Severity: minor
  • Confidence: high
  • Evidence:hypaware-core/plugins-workspace/context-graph-enrich/src/sql.js:19, hypaware-core/plugins-workspace/context-graph-enrich/src/sql.js:29, hypaware-core/plugins-workspace/context-graph-enrich/src/propose.js:41
  • Why it matters:runSql() turns every “unknown dataset” into [], but it is also used for the configured source dataset, so a missing or misspelled source silently makes enrich propose no-op forever instead of surfacing an actionable config/runtime error.
  • Suggested fix: Restrict the missing-dataset fallback to plugin-owned enrichment tables, or add an explicit allowMissing option and leave source-dataset reads fail-fast.

No Finding

  1. Behavioral Correctness
  2. Contract & Interface Fidelity
  3. Change Impact / Blast Radius
  4. Error Handling & Resilience
  5. Security Surface
  6. Resource Lifecycle & Cleanup
  7. Release Safety
  8. Test Evidence Quality
  9. Architectural Consistency

Evidence Bundle

  • Changed hot paths:hypaware.completion provider clients, context-graph enrichment activation, T1 runProposeTick, T2 runCurateTick, enrichment datasets, committed-only graph contract.
  • Impacted callers:hypaware-core/plugins-workspace/context-graph-enrich/src/commands.js:38, hypaware-core/plugins-workspace/context-graph-enrich/src/commands.js:55, hypaware-core/plugins-workspace/context-graph-enrich/src/index.js:72, hypaware-core/plugins-workspace/context-graph-enrich/src/index.js:79.
  • Impacted tests:test/plugins/context-graph-enrich-propose.test.js:156, test/plugins/context-graph-enrich-curate.test.js:10, test/plugins/context-graph-enrich-sql.test.js:17.
  • Unresolved uncertainty: I did not run the test suite; review was limited to the provided diff plus targeted caller/contract reads.
Claude review

Claude review

Inline import('...') type annotation violates CLAUDE.md code style

  • Severity: minor
  • Confidence: 92
  • Evidence: test/plugins/context-graph-enrich-propose.test.js:14
  • Why it matters: CLAUDE.md > Code Style states "Never use inline import('...') types. Declare type imports at the top of the file with @import JSDoc comments"; the PR's own sibling tests (completion-anthropic-client.test.js, completion-openai-client.test.js) already use the correct top-of-file @import form, so this is an inconsistent, citable rule break rather than a pre-existing convention.
  • Suggested fix: Replace the inline @returns {import('.../types.d.ts').EnrichConfig} with a top-of-file /** @import { EnrichConfig } from '../../hypaware-core/plugins-workspace/context-graph-enrich/src/types.d.ts' */ block, then annotate @returns {EnrichConfig}.

Sub-threshold findings (below the ≥80 keep bar, recorded for the cross-reference only):

  • Idempotency of persisted prospect_ids across ticks (major, ~75 — corroborates Codex finding 4): deterministic prospect_ids dedupe only within a tick; a tick that appends prospects then fails before advancing the watermark would re-append the same rows, and T2 curation processes every pending row. Real, but contingent on a mid-tick failure and not independently verified to ≥80.
  • runProposeTick/runCurateTick orchestration untested (major, 75): the pure helpers (watermark, routing, grouping) are well covered, but the glue that wires them — including the confidence_floor / salience_threshold filters and the batch-per-session curation grouping — has no tick-level test.
  • unionSources (enrich datasets.js) exported but untested (major, 70): the sibling context-graph plugin tests the equivalent limit/offset-stripping helper; the copy here does not.
  • salience_threshold > 0 can strand pending prospects (minor, 55): with a recall_index configured, sub-threshold prospects are dropped from scoring and never get a terminal resolution row, so they re-score every tick. Default 0.0 never triggers it.
  • No LLP doc for the new hypaware.completion capability / T1-T2 design (minor–major, 55-60): +4837 lines of new capability ship with no llp/ decision doc, where prior subsystems (vector-search 0024) each got one. CLAUDE.md "land the doc edit in the same commit" — possibly owned by a stacked PR.

Reports: .git/dual-review/pr-110

…l-fast, salience drain, tick tests, LLP 0028)
Latest dual-review (request_changes) findings:
- Cross-tick prospect_id idempotency (Codex major): T1 now filters
candidates against already-persisted prospect_ids before appending
(mirrors the graph projector's pre-write dedup); T2 dedups its pending
selection by id as defense-in-depth. Without this a crash-before-watermark
re-appended duplicate prospects that T2 re-curated (dup rows + model spend).
- runSql missing-dataset fallback (Codex minor): tolerance is now opt-in
(allowMissing, default fail-fast). The configured source read fail-fasts so
a misspelled source_dataset surfaces instead of a silent no-op forever;
plugin-owned tables and the not-yet-projected node/edge surface opt in.
- salience_threshold stranding (Claude): below-threshold prospects get a
terminal `skip` resolution (no curator call) so they drain instead of
re-scoring every tick. Adds a skipped counter to telemetry + curate output.
- Inline import() type in propose test (Claude): replaced with a top-of-file
@import block.
- Orchestration tests (Claude): tick-level tests for runProposeTick /
runCurateTick via a fake runtime (new injected execSql seam), covering
dedup, idempotency-across-ticks, confidence floor, decision routing,
salience auto-skip, and no-decisions-stays-pending; plus unionSources and
runSql allowMissing tests.
- LLP doc gap (Claude): adds LLP 0028 documenting the hypaware.completion
capability + T1/T2 enrichment design (committed-only projection,
idempotency, operability, salience drain), wired into the explainer map,
with six @ref annotations validated by ref-check.
Full suite green (1146 pass, 1 pre-existing skip); typecheck + lint clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@philcunliffe
philcunliffe merged commit f6100e7 into masterJun 16, 2026
6 checks passed
@philcunliffe
philcunliffe deleted the context-graph/enrichment-t1-t2 branch June 16, 2026 01:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@philcunliffe