Skip to content

feat(projection): kernel-owned projection envelope for status, global-summary and global-gates - #5085

Open
huangruiteng wants to merge 1 commit into
mainfrom
codex/projection-envelope-kernel
Open

huangruiteng wants to merge 1 commit into
mainfrom
codex/projection-envelope-kernel

Conversation

@huangruiteng

Copy link
Copy Markdown
Collaborator

Why

LoopX projections (status, global-summary, global-gates) are what operators and agents act on, but today a projection cannot say how old it is, which sources it actually read, or whether it covers the scope it claims. A cached status looks as current as a live one, an unreadable quota file looks like "no quota", and global-summary run against a project-scoped registry quietly reports one project's goals as if they were the whole machine.

This PR makes that metadata a kernel-level read contract, owned by the TypeScript control plane, as part of the TS migration rather than as another Python-side helper.

What changes

Kernel contract: loopx_projection_envelope_v0

  • New TS module loopx/control_plane/projection_envelope.ts, registered as runtime method projection.envelope.seal. It is the single semantic owner of freshness, alerting and completeness.
  • Python passes only compact read facts: per-source read_status / last_read_at / required / counts, plus requested-scope coverage. It never computes freshness or alert state itself. The Python side is a thin facts adapter, loopx/control_plane/projection_envelope_facts.py. It is named so that it does not register as an independently maintained py/ts twin.
  • Envelope fields:
    • timing: observed_at, served_at, age_seconds, served_from_cache;
    • per-source rows: last_read_at, read_status (read, missing, unreadable, not_read), required, a staleness window (default 300s), source_updated_at (informational only);
    • coverage, relative to the requested scope: expected_count, included_count, omitted[] with a reason, count and bounded refs, shown_count / available_count, truncated;
    • derived projections carry upstream[] and inherit upstream source rows (marked via) and upstream completeness;
    • the verdict: fresh, complete, alert, alert_reasons ⊂ {stale_sources, unreadable_sources, missing_required_sources, incomplete_coverage}, alert_source_ids.
  • Alerting rules:
    • an unreadable source always alerts;
    • a missing or not-read source alerts only when it is required;
    • display truncation (shown < available) never alerts on its own.
  • The envelope is path-free, and both request shapes are strictly decoded (§2.5, validate at every trust boundary).

Rollout in this PR

Projection Sources Coverage scope
status / status --goal-id registry, global_registry (optional), goal_run_indexes, goal_state_contract, runtime_projection_routes (optional) registry, goal, or activation.<state>
global-summary goal_quota + upstream status envelope global (expected = global registry goal count)
global-gates same as global-summary global
  • Status cache replay: a cache hit keeps the original observed_at and is re-served through the kernel, which restamps served_at and re-evaluates staleness. An entry with no envelope, or a tampered one, becomes a cache miss (missing_projection_envelope / invalid_projection_envelope) and is never served blind.
  • Global registry health now exposes current_registry_excluded_goal_count and bounded current_registry_excluded_goal_ids. The global views use them to report outside_current_registry omissions.
  • Markdown: one projection: line with age, fresh-source ratio and coverage. When the envelope alerts, it is prefixed with 🔴 and followed by an explicit alert line telling the reader to state the problem before treating the projection as current or whole.

Example: global-summary run with a project-scoped registry on a machine with 48 registered goals now reports:

- projection: 🔴 observed_at=`…` age=`0s` sources_fresh=`2/2` coverage=`1/48` scope=`global`
- 🔴 projection alerts: `incomplete_coverage` omitted=`outside_current_registry=47`; state this before treating the projection as current or whole

RFC: TypeScript control-plane migration

  • New §2.6 "Projection envelope is a kernel read contract" (EN + zh-CN mirror). It covers the rationale, TS ownership through projection.envelope.seal, the adapter exit condition (Python adapters stop computing any envelope semantics; when a projection moves to TS they pass the same facts in-process), the rollout order, and the consumer rule.
  • New §3.1 baseline row for the envelope, and a §6 correctness rule: new or migrated read models must emit the envelope, and their tests must cover a stale source, an unreadable source, an incomplete scope and a replay.
  • New reference contract docs/reference/contracts/projection-envelope-contract.md (indexed), plus a Projection Envelope section in docs/status-data-contract.md.

Migration economics

  • One extra runtime call per projection. status already makes about 106 TS calls and global-summary about 370 on a real machine, so the TS runtime is already a hard dependency of these paths.
  • The envelope adds about 2.5 KB of indented JSON to a status payload of about 5 MB.

Next (not in this PR)

global-todos, global-risks, quota should-run, the review packet, and Decision Context source freshness (which lines up with the freshness work in #5075) should adopt the same envelope, in that order, as they are migrated.

Note: loopx/extensions/presentation.py already uses projection_envelope as an error-context label for extension surface records. That is a different schema and never appears as a payload key; the two do not collide.

Validation

  • npx tsc --project tsconfig.control-plane.json --noEmit: clean
  • npm run -s test:control-plane: 3115 tests, 0 failures (7 new envelope tests: fresh read, per-window staleness at serve time, unreadable/partial/required-missing sources, incomplete vs truncated-only vs undercounted coverage, cache serve, upstream composition with via, decoder rejections)
  • tests/test_projection_envelope.py: 9 new tests (registry, goal and unknown-goal coverage, path-free output, cache replay restamp, cache miss on missing or tampered envelope, global-summary incomplete vs complete, global-gates with unavailable quota and missing upstream)
  • Full Python suite: 12138 passed. The 4 failures (test_delegation_result_use ×2 and the registry I/O census ×2) all reproduce on the unmodified merge base. After rebasing, the census manifest was regenerated for the two line references this PR moves, and the census tests pass.
  • examples/semantic-vocabulary-drift-smoke.py, examples/docs-governance-smoke.py, git diff --check: ok
  • loopx check --scan-path on every touched file: errors=0
  • examples/repository-hygiene-smoke.py fails with release timeline is missing version entries: v1.2.0. This also fails on unmodified origin/main and is unrelated to this PR.

…bal views

Add loopx_projection_envelope_v0, sealed by the TypeScript kernel through
projection.envelope.seal. Python adapters pass compact read facts only; TS
decides freshness, alerts, and completeness.

- status / --goal-id: per-source last_read_at and coverage relative to the
  requested scope; cached copies keep observed_at and restamp served_at
- global-summary / global-gates: goal_quota source, upstream status
  envelope, and outside_current_registry omissions
- Markdown renders a red projection line when stale, unreadable, missing,
  or incomplete
- RFC: TypeScript control-plane migration section 2.6, baseline row, and
  correctness rule; reference contract and status data contract

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

This branch has not been deployed

No deployments
Sign up for free to 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