Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,9 @@ class IngestDecision(BaseModel):
signoff: dict | None = None
feature_group: str | None = None
decision_level: str | None = None # L1 | L2 | L3 — #340 auto-classified when omitted
# #404 — explicit hierarchy parent. Natural format previously dropped this
# (only the internal mapping format honored it); now threaded to the ledger.
parent_decision_id: str | None = None
# #109 — optional governance metadata threaded to the ledger.
governance: dict | None = None

Expand Down
35 changes: 35 additions & 0 deletions docs/META_LEDGER.md
Original file line number Diff line number Diff line change
Expand Up @@ -2931,3 +2931,38 @@ SHA256(content_hash + previous_hash)
**Infrastructure**: `qor/` Python package not installed in this worktree; gate artifact emission to `.qor/gates/<session_id>/research.json` skipped, brief stands on its content per the Entries #53-#54 precedent.

**Required next action**: Governor authorizes cycle 8 implementation. When cycle 8 begins, the implementer must (a) link this brief in the PR description, (b) follow the Priority-1 / Priority-2 recommendations as the plan skeleton, (c) update the stale `sources/notion/poller.py` comment in the same PR, (d) gate on an adversarial `code-reviewer` pass.

---

### Entry #56: GOVERNED CYCLE — #404 Phase 1 (natural-format parent_decision_id threading)

**Timestamp**: 2026-06-01T00:00:00Z
**Phase**: substantiate (local governed cycle via `/qor-auto-dev-1`)
**Author**: Orchestrator (plan→audit→implement→substantiate); independent architect-reviewer audit (SG-007 author-bias mitigation)
**Risk Grade**: L2 — shared ingest contract (`IngestDecision`) + `_normalize_payload`; additive field, backward-compatible; no `ledger/schema.py` change → no §4.7 schema-codeowner gate.

**Content Hash**:
```
SHA256(plan-404-phase1-natural-format-parent-id.md)
= adc582d1a454e8cc0b6cfefada109cb4080b686d493fb165a59bef6e637211a4
```

**Previous Hash**: `144fcdccd63a7ca7e01712decc267bbe881dde4b19b2ac6ce5ae75b9c77fb7f6` (Entry #55)

**Chain Hash**:
```
SHA256(content_hash + previous_hash)
= 44251a5052ddad6fc03441fce05a6311e7cab206439ac289858a4137ea9b4125
```

**Scope**: #404 is a six-item, schema-migration-bearing P1; this cycle delivered **Phase 1 only** — the issue's self-described "smallest enabler" (items #4 + #5). Natural-format ingest now threads a caller-supplied `parent_decision_id` (previously honored only in the internal mapping format; silently dropped in natural format), and `_classify_decision_level`'s docstring documents the full classifier precedence (explicit `decision_level` wins → source-type heuristic → Phase-2 auto-derivation). Satisfies Acceptance criteria #1 and #5.

**Files**: `contracts.py` (`IngestDecision.parent_decision_id`), `handlers/ingest.py` (`_normalize_payload` threading), `ledger/adapter.py` (`_classify_decision_level` docstring), `tests/test_404_natural_format_parent_id.py` (new sociable test, real adapter over `memory://`). Plan `plan-404-phase1-natural-format-parent-id.md` is gitignored per qor convention; locally hashable — content hash above.

**Gate verdicts**: Audit **PASS** (independent architect-reviewer). The load-bearing daemon-serialization concern was verified safe: `handle_ingest` JSON-serializes the raw payload pre-transport (`handlers/ingest.py:845`), so `parent_decision_id` rides inside the opaque `payload` string and is normalized daemon-side via the shared `_handle_ingest_impl`. Substantiation: the new test is **load-bearing** — stashing the `_normalize_payload` threading makes the parent test FAIL (row persists `NONE`); restoring makes it PASS. `ruff check` + `ruff format --check` clean; `test_skill_governance_lint` green.

**Decision**: Land Phase 1 at the single shared normalization point (`_normalize_payload` inside `_handle_ingest_impl`), which both the daemon `write.ingest` dispatcher and the MCP fallback execute — satisfying silongtan's #517 "land daemon-side" steer without a separate daemon edit. Deferred to later governed cycles: L1 auto-derivation + similarity-attach (Phase 2, daemon-side), rejected-alternatives-as-L3-negative-siblings (Phase 3), and the backfill migration (Phase 4 — carries the §4.7 `@jinhongkuan` schema-codeowner gate; the issue's proposed "v23→v24" must be renumbered because v24 is already taken by the input_span-dedup migration).

**Infrastructure**: qor-logic upgraded 0.80.0 → 0.84.0 and redistributed to all three hosts this session (claude / codex / kilo-code, 63 files each). This entry's commit carries the canonical `Authored via [Qor-logic SDLC]` attribution trailer (the `qor` package is reachable via the CLI venv; the prior compact-trailer commits #393/#404-staged predate this fix).

**Required next action**: User review + merge of the Phase-1 PR. Phase 2 (L1 auto-derivation) is the next governed cycle for #404.
4 changes: 4 additions & 0 deletions handlers/ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@ def _normalize_payload(payload: dict) -> dict:
# #340 — thread decision_level from IngestDecision to the mapping.
if d.decision_level is not None:
mapping["decision_level"] = d.decision_level
# #404 — thread parent_decision_id (natural format previously dropped it;
# the adapter already reads mapping["parent_decision_id"] at ingest).
if d.parent_decision_id is not None:
mapping["parent_decision_id"] = d.parent_decision_id
# #109 — thread optional governance metadata from IngestDecision
# to the per-mapping payload so the ledger write picks it up.
if d.governance is not None:
Expand Down
10 changes: 9 additions & 1 deletion ledger/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,15 @@
def _classify_decision_level(source_type: str, code_regions: list) -> str:
"""Deterministic heuristic for decision_level when the caller omits it.

Rules (applied in order):
Classifier precedence (#404 item 5) — overall order at the ingest call site:
1. Explicit ``decision_level`` in the payload WINS — honored upstream at
the processing loop (the ``mapping.get("decision_level")`` guard); this
heuristic runs ONLY when the caller omits the field.
2. This source-type heuristic (rules below).
3. (#404 Phase 2, future) L1 auto-derivation, so no L2 lands without a
parent.

Heuristic rules (case 2 only, applied in order):
1. Code regions present → L2 (architecture, code-grounded).
2. Source is a product conversation → L1 (product commitment).
3. Source is an implementation choice → L3 (technical detail).
Expand Down
81 changes: 81 additions & 0 deletions tests/test_404_natural_format_parent_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Sociable tests for #404 Phase 1.

Verifies that the natural-format ingest API (a) honors a caller-supplied
``parent_decision_id`` (previously silently dropped — only the internal mapping
format honored it) and (b) lets an explicit ``decision_level`` override the
source-type heuristic.

Real ``SurrealDBLedgerAdapter`` over ``memory://`` (via ``BicameralContext``) and
the real ``_handle_ingest_impl`` — no ``MagicMock`` of ctx or ledger, per the
CLAUDE.md sociable-testing rule. The parent_decision_id test is load-bearing:
before ``_normalize_payload`` threads the field it never reaches the mapping, the
adapter writes ``NONE``, and the read-back assertion fails — so the test proves
the threading, not a mock.
"""

from __future__ import annotations

import pytest

from context import BicameralContext
from handlers.ingest import _handle_ingest_impl


async def _get_client(ctx):
"""Idiom from tests/test_ephemeral_authoritative.py::_get_client."""
ledger = ctx.ledger
if hasattr(ledger, "connect"):
await ledger.connect()
inner = getattr(ledger, "_inner", ledger)
return inner._client


def _natural_payload(*, decision_level, parent_decision_id=None, source="agent_session"):
decision: dict = {
"description": "Redis-backed sessions for horizontal scale",
"source_excerpt": "we moved sessions to Redis so checkout could scale",
"decision_level": decision_level,
}
if parent_decision_id is not None:
decision["parent_decision_id"] = parent_decision_id
return {
"query": "checkout scaling",
"source": source,
"title": "sess-404-phase1",
"decisions": [decision],
}


@pytest.fixture
def mem_ctx(monkeypatch):
"""A BicameralContext pinned to an in-memory ledger (never the real db)."""
monkeypatch.setenv("SURREAL_URL", "memory://")
return BicameralContext.from_env()


async def test_natural_format_honors_parent_decision_id(mem_ctx):
"""An L2 decision ingested via natural format with an explicit
parent_decision_id persists that parent on the decision row."""
payload = _natural_payload(decision_level="L2", parent_decision_id="decision:fake_parent_404")
resp = await _handle_ingest_impl(mem_ctx, payload)
assert resp.ingested, f"ingest failed: {resp}"

decision_id = resp.created_decisions[0].decision_id
client = await _get_client(mem_ctx)
rows = await client.query(f"SELECT parent_decision_id FROM {decision_id} LIMIT 1")
assert rows, "decision row not found"
assert rows[0]["parent_decision_id"] == "decision:fake_parent_404"


async def test_explicit_decision_level_overrides_source_heuristic(mem_ctx):
"""source='agent_session' would classify L3 via the heuristic; an explicit
decision_level='L1' in the payload must win (Acceptance #5, #340 precedence)."""
payload = _natural_payload(decision_level="L1", source="agent_session")
resp = await _handle_ingest_impl(mem_ctx, payload)
assert resp.ingested, f"ingest failed: {resp}"

decision_id = resp.created_decisions[0].decision_id
client = await _get_client(mem_ctx)
rows = await client.query(f"SELECT decision_level FROM {decision_id} LIMIT 1")
assert rows, "decision row not found"
assert rows[0]["decision_level"] == "L1"
Loading