Add IdeaOS portfolio graph domain and hydration seam - #262
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
❌ PR Too Large 📋 Best Practices for Large Changes
🚫 This PR is blocked until reviewable size limits are met. |
L9 Audit Harness Report
Step Results
Architecture Audit Findings
See Spec Coverage
See Next StepsAll checks passed. Safe to merge. |
There was a problem hiding this comment.
🟡 Changes recommended
The domain spec currently exposes a generic sync endpoint that can bypass the intended hydration validation/write seam, and there are a couple of correctness/quality issues (CLI path robustness, unused constant) that should be addressed before merge.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR introduces the new CEG-owned idea-portfolio domain pack and a dedicated hydration seam that ingests IdeaOS IdeaGraphProjection artifacts into Neo4j with revision-chained, resumable semantics. It extends the engine with a domain-specific persistence compiler while keeping matching on the existing generic match path.
Changes:
- Add the
idea-portfoliodomain spec with ontology, gates, and scoring dimensions for cross-idea portfolio matching. - Implement a dedicated hydrator/compiler (
engine/sync/idea_portfolio.py) to validate projections, compile facet IDs, and apply revision-chained hydration batches. - Add a CLI tool, unit tests, and documentation for validating/applying hydration envelopes and explaining the authority boundary.
File summaries
| File | Description |
|---|---|
| tools/hydrate_idea_portfolio.py | Adds a CLI to validate (dry-run) or apply IdeaOS hydration envelopes to the idea-portfolio graph. |
| tests/unit/test_idea_portfolio.py | Adds unit coverage for domain loading/compilation, projection validation, query compilation, and revision chaining behavior. |
| engine/sync/idea_portfolio.py | Implements the IdeaOS projection boundary models, deterministic digests, and the domain-specific hydration writer. |
| domains/idea-portfolio/spec.yaml | Introduces the idea-portfolio domain pack (ontology, gates, scoring, compliance/capabilities). |
| docs/idea-portfolio.md | Documents the authority boundary, hydration contract, revision chain, and matching semantics for the new domain. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 207a25b8f2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
❌ The last analysis has failed. |
Remediates CEG-262-002 (RELIABILITY, High) and CEG-262-003 (REVIEW, Low) from audit ceg-open-prs-2026-09-17, against PR #262 source head 0058b0e. CEG-262-002 — the --apply path checked the feature flag, loaded the domain, connected the driver and called IdeaPortfolioHydrator without ever running init_schema or verifying the state-id uniqueness constraint (E007). The hydrator serialises revisions with MERGE on IdeaPortfolioHydrationState {state_id} (E008), and MERGE alone is not single-node-safe under concurrent load without a uniqueness constraint on the merged property (E010). So two concurrent initial hydrations could each create a canonical state node, each read current_revision=null, and each commit a child revision. The apply path now runs the repository's existing schema-init contract — the same _init_schema the admin/init_schema subaction invokes, which provisions the constraint from the ontology's required state_id (E009) — and then verifies the constraint actually exists, failing closed if it does not. Verification is not redundant: _init_schema logs and swallows per-constraint failures, so calling it proves nothing on its own. The label, property and SHOW CONSTRAINTS query now have one definition in engine/sync/idea_portfolio.py, alongside a pure predicate over the returned rows. Neo4j 5 names node uniqueness NODE_PROPERTY_UNIQUENESS (4.x called it UNIQUENESS); NODE_KEY also satisfies the precondition. The check is deliberately outside the write transaction: the hydrator's transaction statement sequence is unchanged, so the existing atomicity and replay tests keep their exact tx.run counts. CEG-262-003 — GraphWriter.execute_write carried a bare ellipsis body, the subject of the one current unresolved review thread (E012). Replaced with the repository-accepted Protocol form used by engine/hoprag/indexer.py GraphStore: class docstring, method docstring with Args/Returns, ellipsis on its own line. No behaviour change. Scope: strictly the two write surfaces the handoff allows. The bundle grants no publish, merge or policy authority, and PRESERVE-L9-ANALYSIS-OWNER remains VIOLATED on this branch — that is CEG-262-001, CI_PIPELINE-owned, untouched here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BXqHneK2x7xKy1WkbBMumX
Pushed audit remediation:
|
…tocol github-code-quality re-flagged "Statement has no effect" on the ellipsis at engine/sync/idea_portfolio.py after the previous push, proposing `raise NotImplementedError`. The earlier fix matched the form used by engine/hoprag/indexer.py GraphStore (docstring plus a bare `...`), which the bot still counts as a no-op statement — so CEG-262-003's closure condition, that the review thread actually closes, was not met. Protocols are structural: IdeaPortfolioHydrator calls execute_write on the concrete object it is given, never on the protocol class, so raising here changes no behaviour and gives a clear error if it is ever invoked directly. The 1425-test unit suite passes unchanged, including the test doubles that implement this protocol. Also tightens prose that duplicated the previous commit message, cutting this remediation from +98 to +63 added lines. No logic changed: the constants, the SHOW CONSTRAINTS predicate, the schema-init call and the fail-closed check are byte-identical in behaviour, and the discriminating proof still shows the apply path reaching IdeaPortfolioHydrator iff state_id uniqueness is present. This does not clear `Enforce PR Policies`: #262 stood at +969 additions against a 1000 limit before any of this work, so the arithmetic is reported on the PR rather than solved by deleting the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BXqHneK2x7xKy1WkbBMumX
|
| additions | |
|---|---|
#262 before any of my work (0058b0e) |
969 |
| Block threshold (additions only; deletions excluded) | 1000 |
| Headroom the branch had | 31 |
| My remediation, first push | +98 → 1067 ❌ |
| My remediation, after trimming prose | +63 → 1032 ❌ |
I cut my footprint from 98 to 63 added lines by removing prose that duplicated the commit message. Getting to ≤31 would mean deleting the SHOW CONSTRAINTS predicate or the fail-closed verification — that is, deleting the CEG-262-002 fix itself to make a size gate pass. I am not doing that.
I also checked a hypothesis I had and it was wrong, so I am correcting it rather than leaving it implied: removing the unrelated CI/governance changes would not get this under the limit. On additions they are almost free — ci.yml +1, supply-chain.yml +1, .github/governance/quality-thresholds.yaml +6, and l9-analysis.yml is +0/−186 (the gate ignores deletions by design). The volume is the feature itself:
587 + engine/sync/idea_portfolio.py
183 + tests/unit/test_idea_portfolio.py
148 + domains/idea-portfolio/spec.yaml
108 + tools/hydrate_idea_portfolio.py
So this branch was always going to sit at ~97% of its additions budget, and any substantive remediation tips it over. Two ways out, both yours, not mine: split the feature per the policy's own advice, or have the owner adjust the threshold for this PR. Tell me which and I will do the work.
Not mine, for the record — SonarCloud Quality Gate, C Security Rating on New Code. I queried the analysis rather than guessing. None of the 10 open issues is attributable to my commits:
tools/hydrate_idea_portfolio.py:34pythonsecurity:S8707path traversal in_load()— pre-existing content of this PR; my hunks in that file are elsewhere. Worth a look regardless: it isMAJORand is one of the two things driving the C rating. For an operator CLI whose argument is the file to read, it may be a false positive — your call..github/workflows/ci.yml×5 andsupply-chain.yml×1githubactions:S8541(--only-binary :all:) — the CI baseline edits, the other driver of the C rating.engine/sync/idea_portfolio.py:494python:S3776, cognitive complexity 18 > 15 inapply()— I did not touchapply().tests/unit/test_idea_portfolio.py×2python:S5778.
Sonar also did not have a clean baseline here: the previous run on 0058b0e reported "The last analysis has failed", so 2026-09-18 is the first completed verdict on this PR.
CEG-262-001 is still untouched and still VIOLATED — this branch deletes .github/workflows/l9-analysis.yml.
Generated by Claude Code
…ns the raise Reverts the `raise NotImplementedError` from 6404959, which turned `Scan for Contract Violations` and `compliance` red on this PR. tools/contract_scanner.py rejects it: [STUB-001] engine/sync/idea_portfolio.py:222 (CRITICAL) Stub in engine/ - unimplemented code path The two bots want opposite things. github-code-quality reads the ellipsis as a no-op statement and proposes `raise NotImplementedError`; this repository's own contract scanner classifies that raise as a stub in engine/ and blocks the merge on it. The repository's contract is the authority — it is a blocking CI gate and repository law, where the bot comment is advisory. This is also what the audit's closure condition meant by "the repository-accepted no-op Protocol body": the form already used by engine/hoprag/indexer.py GraphStore, which is a docstring followed by an ellipsis, and which the scanner accepts. Verified before pushing this time, including the gates the previous push skipped: contract scanner clean (no violations), verify_contracts 27/27 present and wired, ruff, ruff format, mypy engine/ 134 files, 1425 unit tests, 2010 tests in CI shape. The github-code-quality thread cannot be satisfied as proposed; answering it on the PR rather than breaking a blocking contract to silence it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BXqHneK2x7xKy1WkbBMumX
…l Neo4j Adds tests/integration/test_idea_portfolio_hydration.py, the closing validation audit ceg-open-prs-2026-09-17 names for CEG-262-002, and fixes a fail-open bug in the precondition that only a real server could reveal. The bug: state_uniqueness_constraint_present accepted NODE_PROPERTY_UNIQUENESS but not UNIQUENESS. Neo4j 5.18 — the server tests/conftest.py pins — reports `type: 'UNIQUENESS'`; the rename to NODE_PROPERTY_UNIQUENESS is later. So on the pinned server the predicate returned False even with the constraint correctly created by _init_schema, and _require_state_uniqueness would have refused every hydration permanently. Verified directly against 5.18.1: u1 type='UNIQUENESS' props=['a'] # IS UNIQUE k1 type='NODE_KEY' props=['b'] # IS NODE KEY e1 type='NODE_PROPERTY_EXISTENCE' props=['c'] # IS NOT NULL Both spellings are now accepted, so the check works on the pinned server and on later ones. The hand-written rows in the unit-level proof could not catch this — they encoded the same wrong assumption as the code. That is precisely why the audit required a real-Neo4j validation rather than a mocked one. The tests: - the predicate agrees with a real server before and after the constraint exists - two concurrent initial envelopes yield exactly one commit, one rejection, one canonical state node, and a committed revision matching the winner's receipt - replaying the committed envelope is reused, not a second revision - a child naming the wrong parent revision is rejected Run against neo4j:5.18-enterprise via the existing testcontainers fixtures: 4 passed. Contract scanner clean, ruff, ruff format, mypy engine/ 134 files, 1425 unit tests green. Note for reviewers: this adds ~193 lines and pushes #262 further past the 1000-addition size policy, which was already failing at 1032. That gate needs an owner decision either way; it is not a reason to omit the validation the audit requires. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BXqHneK2x7xKy1WkbBMumX
|
| Finding | State |
|---|---|
| CEG-262-002 | Both closing validations now executed and passing |
| CEG-262-003 | Code closed; thread cannot be satisfied — see #267 |
| CEG-262-001 | Untouched, CI_PIPELINE-owned — #265 |
One consequence you should weigh
This adds ~193 lines, taking the PR to +1239 against the 1000-addition policy. That gate was already failing at 1032 and the branch arrived at 969, so it needs a decision either way — #268 lays out the options, including excluding test files from the accounting. I did not omit the validation to protect a line count.
Generated by Claude Code
SonarCloud python:S5778 on the file added in 3b16333: the `pytest.raises` block held three calls that could throw — `_digest`, `_envelope` and the `apply` under test — so a failure in the fixture builders would have been indistinguishable from the rejection the test asserts. Only `apply` belongs inside the block. Same shape as the surrounding suite. Behaviour unchanged: 4 passed against neo4j:5.18-enterprise, ruff and format clean, contract scanner clean. This was the only finding SonarCloud raised against the new file; the other ten open issues on this PR are pre-existing and attributed in earlier comments. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BXqHneK2x7xKy1WkbBMumX
…refusal Found by running the CLI end-to-end against a live Neo4j as a least-privilege user. A database user may be denied SHOW CONSTRAINTS itself: Neo.ClientError.Security.Forbidden: Show constraints on database 'idea-portfolio' is not allowed for user 'limited' The precondition already failed closed in that case — nothing was written, the graph stayed empty — but it surfaced as a raw neo4j driver traceback. An operator could not tell "the constraint is missing" from "this user may not read constraints", and those have different remedies: one is a schema init, the other is a grant. The driver error is now caught and re-raised as IdeaPortfolioHydrationError naming the cause and the grant needed. Unverifiable is treated exactly like absent — not provably safe, so it still refuses to mutate. Verified against neo4j:5.18-enterprise, all four paths of the real CLI: restricted user, cannot read constraints -> refuses, 0 nodes written admin, constraint absent -> _init_schema creates it, applies, 1 state node admin, replay same envelope -> "reused", still 1 state node no --apply -> "validated", no mutation Contract scanner clean, ruff, ruff format, mypy engine/ 134 files, and the full suite with a real database: 2039 passed, 10 skipped, 56 xfailed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BXqHneK2x7xKy1WkbBMumX
|
| Scenario | Result |
|---|---|
| admin, constraint absent | _init_schema creates it → applied, 1 state node |
| admin, replay same envelope | reused, still 1 state node |
no --apply |
validated, no mutation |
| least-privilege user | refuses, 0 nodes written |
The last one exposed a flaw. A restricted database user is denied SHOW CONSTRAINTS itself:
Neo.ClientError.Security.Forbidden: Show constraints on database
'idea-portfolio' is not allowed for user 'limited'
The precondition still failed closed — the graph stayed empty, so the safety property held — but it surfaced as a raw neo4j traceback. An operator could not distinguish "the constraint is missing" from "this user may not read constraints", and the remedies differ: a schema init versus a grant. cbaa90c catches the driver error and re-raises it as IdeaPortfolioHydrationError naming the cause and the grant required. Unverifiable is treated exactly like absent — not provably safe, so still no mutation.
This is the second defect that only a real database could reveal; the first was the UNIQUENESS vs NODE_PROPERTY_UNIQUENESS spelling in 3b16333. Worth noting for the record that the live run confirmed the constraint _init_schema actually creates is typed UNIQUENESS — the spelling my original code missed, which would have deadlocked hydration on this exact server.
Verification on cbaa90c
tools/contract_scanner.py no violations
ruff check . / format clean, 367 files
mypy engine/ 134 files, no issues
pytest tests/ 2039 passed, 10 skipped, 56 xfailed
Size note: this adds 11 lines, so the reviewable-size gate moves from 1240 to ~1251. That gate needs a decision either way (#268); I am not leaving a known defect in the fail-closed path to protect a line count.
Generated by Claude Code
|
The enrichment assertion is red on main and takes seven checks down with it on this PR: Test Suite, Pre-commit Hooks (via the pytest hook), Coverage (Codecov), Quality Gate, Baseline Ratchet (Required Tests + Verdict, which records it as an unledgered finding), and the CI Gate rollup that blocks merge. The fix exists in open PRs #264 and #262, which make byte-identical changes to this hunk. Porting it rather than waiting: it no-ops once main carries it. This is the exact hunk both PRs carry, deliberately with no edits of my own. An earlier attempt here rewrote the same assertion with an explanatory comment inside the dict, which made it textually different and so a genuine add/add conflict — that is what the overlap gate caught, and it was right to. Identical text on both sides merges cleanly in a three-way merge, so this port conflicts with neither PR. engine/gate_egress.py documents the contract this restores: "retry is the caller's decision and requires the idempotency key returned in the result". All three return paths of request_enrichment honour it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nwd3DgnqkpEuaBMPKYLhF3
PR Remediation — Cycle 1 SummaryCommit: Fixed (0)none Deferred (0)none Acknowledged (8)
Disagreed (1)
Local verify: NOT_RUN (no source edit; digest UNKNOWN; consumer Makefile has no precommit-repo target) | Threads resolved: 9/9 |
… main (#264) * fix(test): assert idempotency_key in fail-closed enrichment result `request_enrichment` mints the idempotency key before the GATE_URL check and returns it on every path, including the fail-closed `gate_not_configured` branch. The test for that branch asserted an exact dict without the key, so it has failed since the seam-audit commit that introduced both (#259) — CI's Test Suite is red on main HEAD for this one assertion and nothing else. Classified as a test defect, not a product defect: the success-path test in the same file already asserts `result["idempotency_key"]`, so the contract is that the key is always returned. The assertion now names the key via `enrichment_idempotency_key(...)`, keeping the exact-dict form so no extra field can leak in unnoticed. Verified: tests/unit 1421 passed, 7 skipped; full suite (no Docker) 2006 passed, 56 xfailed; ruff check + ruff format + mypy engine/ clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BXqHneK2x7xKy1WkbBMumX * test(gate-egress): assert idempotency_key on the SDK-error return Validate & Repair surfaced that the "always replayable" contract was asserted on the success and fail-closed returns but not on the SDK-error one, though request_enrichment returns the key there too. Adds the missing assertion so every failing return of request_enrichment is covered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BXqHneK2x7xKy1WkbBMumX * test(gate-egress): match PR #262's fail-closed hunk byte-for-byte Open PR #262 (feat/idea-portfolio-domain) already carries the identical fail-closed assertion fix. The only difference was an explanatory comment on this branch, which made the same region conflict textually and blocked the overlap gate. Dropping the comment makes both sides of the hunk identical, so whichever PR lands first the other merges clean. The rationale lives in the PR body and in this branch's first commit message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BXqHneK2x7xKy1WkbBMumX --------- Co-authored-by: Claude <noreply@anthropic.com>
…entifiers (#271) * fix(tests): assert the full fail-closed enrich envelope in gate_egress test test_request_enrichment_fails_closed_without_gate_url compared the whole result dict against a three-key literal that omitted idempotency_key, so it failed against the implementation it was introduced alongside (#259). engine/gate_egress.py documents the authoritative contract: "one attempt per call; retry is the caller's decision and requires the idempotency key returned in the result". All three return paths of request_enrichment honour that, the sibling success-path test already asserts result["idempotency_key"], and engine/health/enrichment_trigger.py forwards the whole envelope to its caller, so the key is load-bearing on the fail-closed path too. The test encoded the wrong envelope, so the test is corrected. Strict whole-dict equality is kept — the envelope stays exactly pinned, with the expected key derived from the module's public enrichment_idempotency_key helper rather than a hardcoded digest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nwd3DgnqkpEuaBMPKYLhF3 * fix(compliance): stop the phone pattern matching inside opaque identifiers handle_match builds query_id as `q_` + uuid4().hex[:12]. The PHONE value pattern was the only one in _PII_PATTERNS without boundary guards, so any 10-digit run inside that token matched it. ComplianceEngine.redact_response then removed the field outright (PIIHandler.redact pops the key), so the match response silently lost query_id. Measured on 200,000 generated ids: 1.572% were classified as a phone number. That is a live response-shape defect, and it is what made tests/test_handlers.py::test_match_returns_structure fail intermittently — the assertion is correct, the engine was dropping the key. `\b` is not usable here: the pattern can start with `+` or `(`, which are not word characters, so a leading `\b` would break `+1 (555) 123-4567`. SSN and IP_ADDRESS can use `\b` because they start with a digit. Lookarounds for [0-9A-Za-z_] give the same protection without that constraint. Verified: 0/200,000 false positives after the change, while `555-123-4567`, `+1 (555) 123-4567`, `5551234567` and `call 555.123.4567 now` are all still detected, as is detection by field name. Both directions are pinned by new regression tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nwd3DgnqkpEuaBMPKYLhF3 * revert(tests): drop the gate_egress fix already carried by open PR #264 The overlap gate blocked publication: tests/unit/test_gate_egress.py textually conflicts with PR #264 ("fix(test): repair the fail-closed enrichment assertion that is red on main") and PR #262. PR #264 already contains the same correction, derived independently and byte-identical in the block it touches, and additionally asserts the idempotency key on the SDK-error path. That PR owns this file, so carrying a duplicate here would only add an add/add conflict for whichever landed second. This branch keeps only the compliance repair, which neither #264 nor #262 touches. This PR therefore stacks on #264's head so the enrichment assertion is inherited rather than duplicated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nwd3DgnqkpEuaBMPKYLhF3 * fix(tests): port the fail-closed enrich assertion fix from #264 The enrichment assertion is red on main and takes seven checks down with it on this PR: Test Suite, Pre-commit Hooks (via the pytest hook), Coverage (Codecov), Quality Gate, Baseline Ratchet (Required Tests + Verdict, which records it as an unledgered finding), and the CI Gate rollup that blocks merge. The fix exists in open PRs #264 and #262, which make byte-identical changes to this hunk. Porting it rather than waiting: it no-ops once main carries it. This is the exact hunk both PRs carry, deliberately with no edits of my own. An earlier attempt here rewrote the same assertion with an explanatory comment inside the dict, which made it textually different and so a genuine add/add conflict — that is what the overlap gate caught, and it was right to. Identical text on both sides merges cleanly in a three-way merge, so this port conflicts with neither PR. engine/gate_egress.py documents the contract this restores: "retry is the caller's decision and requires the idempotency key returned in the result". All three return paths of request_enrichment honour it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nwd3DgnqkpEuaBMPKYLhF3 --------- Co-authored-by: Claude <noreply@anthropic.com>
…ersedes #280, #281, #283, #284) (#285) * fix(c-009): add make cypher-lint and close live interpolation holes C-009 named make cypher-lint as automated enforcement but the target did not exist. Add the scanner, wire it to Make and pre-commit, and parameterize the live quoted interpolations it now finds. Issue-Remediation-Cycle: #272/cycle-1 Co-authored-by: Cursor <cursoragent@cursor.com> * style: commit gate writer rewrites so make pr finishes once * fix(c-009): return label-safety check as a boolean Ruff SIM103 failed the PR writer wave on the two-return form. Issue-Remediation-Cycle: #272/cycle-1 Co-authored-by: Cursor <cursoragent@cursor.com> * test(c-009): cover cypher_quoted_ident wrap and reject Carry the isolate follow-up tests onto the open PR so identifier quoting has the same unit coverage as sanitize_label. Issue-Remediation-Cycle: #272/cycle-1 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ci): declare consumer repo_class python for org-ci language detect CEG is Python-only but had no .l9/ci.json, so REPO_CLASS=auto left Analyze (central Core) fail-closed on ambiguous SDK language detect. Consumer metadata is the documented escape hatch and does not touch workflows. Issue-Remediation-Cycle: #273/cycle-1 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ceg): close CEG-001..009 from the Constellation E2E action log CEG-001 supply chain. The release set kept the moving @v1 tag and closed the finding on the lock instead: poetry.lock records resolved_reference e9f829f, which is what every deployed image installs, and validate_sdk_pin.py now prints that commit on PASS so a build log says which SDK object was taken. EIE's validator enforces the same contract over its requirements.lock. The trade-off the moving tag accepts — a live-resolving build takes whatever v1 points at that minute — is now stated in the script rather than left to be discovered. CEG-002 configuration. Both compose files set L9_REQUIRE_SIGNATURE=true and neither set L9_VERIFYING_KEYS_JSON. That flag also verifies the responses Gate signs, and Gate signs with its own key id, so as shipped CEG rejected every signed Gate response — the E2E passed only because its harness supplied the map. Dev carries the shared HMAC secret; prod requires the variable rather than defaulting it, because a wrong key here is a silent trust failure. CEG-003 configuration. Compose set L9_NODE_SPEC_PATH, which nothing reads. The SDK reads GATE_NODE_SPEC_PATH. Registration worked only because the SDK default happens to be engine/spec.yaml and WORKDIR is /app, so relocating the spec would have silently had no effect until the default path stopped existing — at which point register_with_gate swallows FileNotFoundError and returns False. CEG-005 architecture. engine/spec.yaml now declares owner: ceg. Registration previously succeeded only because Gate's _OWNER_ALIASES maps the node name "graph" to "ceg": correct today, and an alias rather than a declaration. Renaming the node would have broken registration for every canonical action at once. Pairs with the resolve ownership entry added in Constellation.Gate. CEG-006 + EIE-008 architecture. Two halves of one loop, neither connected. engine/health/api.py implemented three handlers and was imported by nothing, so request_enrichment — the entire CEG -> Gate -> EIE direction — had no trigger an inbound packet could reach; it now hangs off `admin` alongside trigger_gds, where operator surfaces belong. In the other direction EIE advertises and fully implements `graph-inference-result` and no code here ever produced one, so the feedback loop had a consumer and no producer. emit_graph_inference_result sends it through Gate, filtering at the same 0.55 confidence floor EIE drops below — the two sides were built to the same number and never joined. Flag-gated (GRAPH_INFERENCE_FEEDBACK_ENABLED), because each emission spends EIE budget. CEG-007 dependency hygiene. No import of redis exists anywhere under engine/ or chassis/, yet redis was a requirement, a service in both compose files with depends_on: service_healthy (so the api service could not start without it), a CI service container in two workflows, a required env var in the env contract, and a dependency contract claiming "Scoring result caching" and "Domain pack cache" that were never implemented. Four contract tests asserted that description continuously, which is what made an unused dependency look load-bearing. All of it removed together. CEG-008 operability. match and sync route to a Neo4j database named after the domain id and nothing created it, so on a fresh instance every one failed until an operator ran CREATE DATABASE by hand. GraphDriver now provisions it on first use under AUTO_CREATE_DOMAIN_DATABASE (once per database per process, Enterprise-only), and either way an absent database raises DatabaseNotProvisionedError naming the database and the exact command. The name is validated before it is quoted into the statement: CREATE DATABASE takes no query parameter, and sanitize_label cannot be used because domain ids legitimately contain dashes. CEG-009 configuration. Nine specs used a flat <name>_domain_spec.yaml shape the loader never reads — it resolves domains/<domain_id>/spec.yaml — so they looked like available verticals and no tenant id could select them. Migrated. Two were additionally invalid, with edges to undeclared Skill and ZipCode nodes; nothing caught that, because a file the loader never opens is never validated either. Loadable domains go from one to ten, idea-portfolio still behind its flag. Verification: 1818 passed, 12 skipped, 56 xfailed across unit, contracts, compliance, architecture and invariants. Contract 21 caught the new feature flag as undocumented and FEATURE_GATES.md now carries both new gates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CoomAnwQt9gxGhHAJJSDz2 * fix(ceg): repair kernel-surfaced drift from the CEG-007 removal Recursive Alignment found the redis removal stopped at the declarations. The Makefile still drove the service: `health` exec'd l9-graph-redis, `redis-shell` opened a CLI on a container that no longer exists, `local-dbs` ran `docker compose up -d neo4j redis` against a service absent from both compose files, and two local-api targets exported PLASTICOS_REDIS_URL for a setting that was deleted. docs/CI_PIPELINE.md still listed the CI redis service. Also registered the three capabilities this branch adds in .claude/rules/capability-registry.md. CLAUDE.md's first "Always" is to check that registry before building, so a capability missing from it is how a second implementation gets written — which is exactly what EIE-006 turned out to be. Apply report: .l9/autonomy/kernel-apply.md. Suites unchanged at 1818 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CoomAnwQt9gxGhHAJJSDz2 * fix(ceg): remediate PR #283 review findings and mypy CI failure Nine findings from the PR #283 review plus the single mypy error that turned all four CEG lint jobs red. CI fix - handlers.py: the inference-rule walrus reused `result`, already bound to dict[str, Any] earlier in handle_admin. Renamed to `inferred`. mypy: "Success: no issues found in 133 source files". Correctness - graph/driver.py: ensure_database() claimed the database name AFTER the await, so concurrent first-use requests each issued their own CREATE DATABASE. The claim now precedes the suspension point and is released on failure so a later attempt can retry. - handlers.py: `rules: []` in emit_inference_feedback silently became "run every rule"; a bare string was iterated character by character. Absent key still means all rules; a non-list of non-strings is now rejected. - gate_egress.py: get_gate_client() raises ValueError on missing or invalid SDK environment material. Both egress paths caught only GateClientError, so a configuration fault crashed the caller instead of returning the typed failure result every other path returns. - health/health_report.py: _conversion_events was an unbounded list fed by every Seed-tier assess/report call — a leak CEG-006 made reachable, and one CLAUDE.md's "never create unbounded caches" covers. Now a deque(maxlen=10_000). - scripts/validate_sdk_pin.py: printed "PASS ... -> None" when the tree has no poetry.lock. Says what is true instead. Feature-flag discipline (C-21) - health_* admin subactions now gate on health_api_enabled. auto_enrich_via_gate gates only the outbound Gate request, not assessment, reporting or conversion tracking, so the surface needed its own flag. - admin feature_status reports all five Constellation-seam flags. Domain packs — correction to the PR body - Five of the nine packs CEG-009 made readable compile to Cypher that cannot execute: `type: traversal` gates written with pattern/condition fall back to a RELATES_TO edge no ontology declares, and scalar queryparams (85.0, 5, 1) are emitted as parameter NAMES, producing $85.0. Readable is not correct. They are gated behind unvalidated_domain_packs_enabled until the compiler or query schema grows the support they assume. - The PR's "loadable domains 1 -> 10" is therefore "1 -> 5 discoverable-and-executable, plus 5 migrated-but-dormant". - test_domain_pack_shape.py compiles every discoverable pack's gates and fails on either defect shape; a shrink-only test fails if a gated pack starts compiling cleanly, so the flag cannot become a place defects go to be forgotten. Verification - pytest tests/unit tests/contracts tests/compliance tests/architecture tests/invariants: exit 0 (jsonschema installed so the two previously skipped contract modules actually ran: 34 passed). - mypy engine/: clean. ruff check/format clean on changed files under the CI-pinned rule set. - tools/contract_scanner.py: no violations. tools/verify_contracts.py: 27/27. - scripts/validate_sdk_pin.py: PASS Quantum-L9/Gate_SDK@v1 -> e9f829f. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CoomAnwQt9gxGhHAJJSDz2 * fix(ci): restore L9 analysis and settle Protocol body + Scorecard GHCR Restore the owner-managed l9-analysis.yml deleted in #262. Keep Scorecard on GHCR-backed v2.4.4 (v2.4.0 still pulls gcr.io/openssf). Protocol methods are docstring-only so STUB-001, ruff, and code-quality agree. Issue-Remediation-Cycle: #265/cycle-1 Issue-Remediation-Cycle: #267/cycle-1 Issue-Remediation-Cycle: #269/cycle-1 Co-authored-by: Cursor <cursoragent@cursor.com> * style: commit gate writer rewrites so make pr finishes once * fix(c-009): AST cypher-lint scanner and value parameterization (F280-1, F280-2) Audit findings F280-1/F280-2 on PR #280: - tools/cypher_lint.py is now an AST scanner. Every f-string that builds Cypher (by text, by sink, or by compiler context) has each interpolation classified by syntactic role: quoted value and LIMIT/SKIP always fail; label, property, parameter-name and back-quoted positions must be validated; a bare fragment fails when it reads a raw spec value. No keyword precondition, multi-line f-strings analysed as one unit, and waivers are explicit, reasoned, and printed on every run. - cypher_quoted_ident() is removed: it sanitized data as if it were a label. EnumMapGate mapping keys/values and GDS equipment type names now travel as $parameters via BaseGate._bind_param() / query_params. - ExclusionGate sanitizes edge/node identifiers; _prop_ref/_param_ref validate identifiers; operators and composite logic pass through literal allow-lists aligned with the schema's validated set. - GateCompiler validates query parameter names with sanitize_label() and casts numeric spec values; scoring builders validate dimension aliases and property/parameter names. - sanitize_database_name() added for CREATE DATABASE quoting. Tests cover both directions of the scanner (including the audit's false-negative NOT EXISTS shape and false-positive module-constant MERGE), non-identifier and injection-shaped enum values, and the live engine tree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018dcSADmR2nDESm69Szd1fT * fix(driver): make concurrent first-use callers await in-flight provisioning (F283-1) ensure_database() claimed the name before awaiting CREATE DATABASE, so a second request arriving mid-flight saw the name, returned True, and ran its domain query against a database that did not exist yet. In-flight provisioning is now a per-database asyncio.Task kept apart from the ensured set; every concurrent caller awaits (shielded) the same task and receives its result, exactly one CREATE is issued, a failed CREATE is retried on the next call, and cancelling one waiter does not cancel the CREATE. Tests prove one CREATE and no domain query before provisioning completes under concurrency, shared outcome, retry after failure, and cancellation isolation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018dcSADmR2nDESm69Szd1fT * ci: keep L9 analysis central; test: enforce docstring-only Protocol bodies (F284-1, F284-2) - Drop the restored consumer-local .github/workflows/l9-analysis.yml. Current l9-ci-core owner law (consumer_copy_required=false) prohibits copied L9 workflows in consumers; Analyze (central Core) already passes on this repository, and the local duplicate only produced a startup_failure plus Scorecard/Sonar alerts. - test_protocol_bodies.py now strips the optional leading docstring and fails on any remaining statement with its location, instead of only recognising pass / raise NotImplementedError / ellipsis. Negative fixtures cover return, assignment, arbitrary raise, calls, expressions and compound statements. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018dcSADmR2nDESm69Szd1fT * fix(ci): validated numeric casts, logger docstring examples, no bare awaits in tests - Semgrep Policy Check (float-requires-try-except): numeric spec values now pass through engine.utils.security.cypher_number(), which does the float() conversion inside try/except and raises ValueError with the offending value; cypher-lint treats it as a validator alongside sanitize_label. - Check Terminology Consistency (print( on changed files): docstring usage examples in hoprag/indexer, scoring/helpfulness, scoring/importance, traversal/multihop and traversal/pseudo_query use logger.info; the Protocol-body fixture uses len("x") as its call statement. - github-code-quality "statement has no effect": the provisioning tests assert on the awaited task results instead of bare await statements. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018dcSADmR2nDESm69Szd1fT * feat(deps): detect a stale Gate_SDK lock, and enforce the pin in CI CEG already declares the moving major channel correctly — pyproject rev = "v1", requirements.txt @v1, poetry.lock reference = "v1" — and none of that changes here. What was missing is the one failure a moving tag makes possible and structural checks cannot see: Gate_SDK advances `v1`, and this repository's lock silently stops matching it without a single local file changing. - scripts/validate_sdk_pin.py: adds --verify-tag. It resolves Quantum-L9/Gate_SDK@v1 at the canonical remote and requires poetry.lock's resolved_reference to equal it, failing closed when the remote cannot be resolved — an unverifiable lock has not been verified. check_text() and check_tree() are untouched, so the existing offline contract and its tests are unchanged; the new logic is three additive pure functions plus an argparse front door. - tests/unit/test_validate_sdk_pin.py: covers the comparison directly — current lock, stale lock, unresolvable channel, lock with no resolved_reference. Pure logic, no network in the test suite. - ci.yml: the validator was never actually wired into CI; it was only reachable through a unit test. Both modes now run in the merge-blocking validate job. poetry.lock is NOT regenerated: resolved_reference is already e9f829f982110be13752da8f18c7a9692e8ed908, which is what v1 resolves to today, so there is nothing to refresh and no reason to churn the file. Verified: ruff check, ruff format --check, 9 passed in tests/unit/test_validate_sdk_pin.py, and both validator modes against the live remote. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0153NHEJmFreprQ5tHLztSGq * fix: address Codex and Copilot review on PR #285 - cypher-lint (Codex P1, Copilot): name facts are now lexically scoped — a sanitizer binding in one function no longer certifies a same-named variable in a sibling function; nested functions see enclosing scopes; a helper counts as validated only by its own return statements, never by returns inside a nested def. Scoping exposed five private helpers (resolution/similarity, feedback/drift_detector, feedback/signal_weights) that interpolated a label parameter relying on their callers to have sanitized it; each now validates the parameter itself. - GraphDriver (Codex P2): execute_write provisions the domain database on first use and translates a missing-database error exactly like execute_query, via shared _provision_on_first_use / _translate_absent_database helpers. - domain_extractor (Codex P2): writes domains/<domain-id>/spec.yaml, the only layout DomainPackLoader discovers. - strictwhen (Copilot): declared by GateSpec, consumed by no compiler, so legal-discovery and research-agent are withheld behind unvalidated_domain_packs_enabled; the pack-shape test records strictwhen as a defect so the shrink-only guard still holds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018dcSADmR2nDESm69Szd1fT * docs(contracts): point the SDK install contract at the canonical channel SHARED_MODELS.md is status: active and CLAUDE.md tells agents to load it before touching engine/packet/. Under a heading reading "Installation (pyproject.toml)" it prescribed: constellation-node-sdk = {git = "https://github.com/cryptoxdog/Gate_SDK.git"} which is wrong twice over — the forbidden fork, and no ref at all, so it floats on that fork's default branch. scripts/validate_sdk_pin.py fails closed on both, meaning the doc instructed the next agent to write something the repository's own gate rejects. This is not the historical seam-audit evidence the release-identity campaign deliberately leaves alone; it is a live instruction. Corrected to the canonical repo on the v1 compatibility channel, with a note on why the resolved object belongs in poetry.lock instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0153NHEJmFreprQ5tHLztSGq * fix(security): refuse a Gate_SDK remote git would parse as an option SonarCloud pythonsecurity:S8705 on the new --verify-tag code, and it is a real finding, not a false positive. Passing argv as a list and never invoking a shell stops *command* injection but not *argument* injection: `git ls-remote --upload-pack=<cmd> <repo>` executes <cmd>, so a --remote value beginning with `-` is an execution vector on its own. Two independent guards, because either alone is a single point of failure: - safe_remote() rejects an empty remote or one starting with `-`, before it reaches git. - --end-of-options is passed so git treats the remote as a positional even if the guard is ever loosened. The rejection fails closed through the same path as any other unresolvable channel, but names which of the two reasons it was rather than reporting a generic resolution failure. Tests cover both directions: a canonical https remote and a local fixture path are accepted, and --upload-pack=..., -u, --exec=sh and "" are refused. The Gate_SDK test drives it end to end through the CLI and asserts the payload file was never created. Introduced by my own --verify-tag commit on this branch; SonarCloud was green on the previous head and red on mine. The same code was written into all four validators, so all four carry this fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0153NHEJmFreprQ5tHLztSGq * fix(security): the canonical remote is the contract, not a CLI option My previous commit guarded the remote with a negative check and --end-of-options. That is correct on the merits but SonarCloud still reported pythonsecurity:S8705, and it was right to: a `startswith("-")` rejection is not a sanitizer, so the argparse value still reached git having only been filtered, not replaced. On Gate_SDK the report simply moved from the resolver to the _git() helper. The real problem was the flag existing at all. `--remote` let an operator point a release-identity check at a repository that is not Gate_SDK — which is precisely what this contract exists to prevent. So the flag is gone: - main() passes the CANONICAL_REMOTE module constant. No command-line argument reaches git. - safe_remote() returns the constant on an equality match, so the value handed to the sink is provably not derived from any input. A non-canonical value is only accepted if it is an existing directory — the local fixture repositories the tests use — and a `-`-leading or empty value is still refused outright. --end-of-options stays as the third layer. - The Gate_SDK tests that drove --verify-tag through the CLI now call validate(..., remote=...) in process, so the fixture path is a function parameter rather than a command-line argument. This narrows what the tool can be pointed at, which is the behaviour the release-identity contract wanted in the first place. Nothing about the v1 channel, the lock comparison or the fail-closed semantics changes. Verified in every repo: ruff, ruff format, the validator suites (Gate_SDK 22, Gate 126, CEG 15, EIE 25 passed) and --verify-tag against the live remote still reporting e9f829f982110be13752da8f18c7a9692e8ed908. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0153NHEJmFreprQ5tHLztSGq --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Claude <noreply@anthropic.com>




Objective
Add the CEG-owned
idea-portfoliodomain and the first owner-native hydration seam for IdeaOS semanticIdeaGraphProjectionartifacts.Authority boundary
Ideas/filenames, ZIP names, or invents Idea-to-Idea source truth.Runtime design
idea_portfolio_enableddefaultsfalse.DomainPackLoaderhides/rejects the domain while disabled, so normal match reads fail closed.IdeaPortfolioHydratorhas a second fail-closed admission guard.SyncGeneratoris deliberately not exposed for this domain because it cannot preserve variable relationship types, relationship-level provenance, replacement semantics, tombstones, or corpus revision authority.reusedwithout a second semantic mutation.Epistemic contract
All source assertions are preserved on the graph with
evidence_stateand source references. Ranking admits only source-backedVERIFIEDandSUPPORTED_INFERENCEassertions.HYPOTHESISandUNKNOWNremain visible graph context but cannot silently become numeric ranking evidence.The Python boundary models use internal
schema_idfields aliased to wire keyschema; digests serialize with aliases so wire compatibility is retained without shadowing PydanticBaseModel.schema.Files
domains/idea-portfolio/spec.yamlengine/config/loader.pyengine/config/settings.pyengine/sync/idea_portfolio.pytools/hydrate_idea_portfolio.pytests/unit/test_idea_portfolio.pySafety / consistency
Validation target
python tools/validate_domain.py domains/idea-portfolio/spec.yaml --strict