feat(api): implement source and artifact base REST APIs - #1438
Conversation
|
resolve conflicts |
frf12
left a comment
There was a problem hiding this comment.
Please see the inline comments.
| artifacts=current.lineage.artifacts, | ||
| ) | ||
| try: | ||
| revised = await self._artifacts.revise(connection, scope_id, current, draft) |
There was a problem hiding this comment.
Each Revision created by this management API should reference a server-owned lineage_only Source that records the direct write. The Revision 1 Source is filtered above, but Replace does not create a replacement Source, so Revision 2 can return sources: []. Please create a Source bound to the next Revision and commit it in the same transaction as revise().
There was a problem hiding this comment.
Agreed. We will update Replace so that every Revision created through this management API has its own server-owned lineage_only Source recording the direct write.
For the next Revision N, Replace will:
- create a new Source with role=lineage_only, operation=artifact_replace, and a target bound to (scope_id, family, artifact_id, revision=N);
- use that new Source as the Source lineage of Revision N;
- preserve the previous Revision and its Source unchanged;
- inherit the previous Artifact lineage for now;
- commit the Source, new Artifact Revision, head update, and lineage rows in the same transaction.
| async with self._database.transaction() as connection: | ||
| stored = await self._sources.add(connection, scope_id, source) | ||
| draft = draft.model_copy(update={"sources": (stored.ref,)}) | ||
| artifact = await self._artifacts.create( |
There was a problem hiding this comment.
This path validates the Family content model and writes directly through ArtifactRepository, but it does not perform the complete Family write behavior. It skips Memory entry versions and projections, Handoff citation validation and fixed identity, Experience Search updates, and Skill package validation and Search updates. Please dispatch Create and Replace through Family-specific write handlers while retaining the direct management API behavior.
There was a problem hiding this comment.
Thanks — fixed. The Base Artifact API now dispatches Create/Replace through a FamilyManagementWriter registry:
- Memory reuses MemoryService, maintaining Entry Versions/heads, changes and search projections.
- Experience and Skill maintain their existing search/package projections.
- Handoff reuses HandoffService, including singleton enforcement and citation lineage.
The lineage-only Source, Artifact Revision and Family-derived state are committed in one transaction and roll back together on failure. No LLM generation or Candidate workflow is triggered. Regression tests and all PR checks pass.
| name=self._id_factory("source"), | ||
| materialization=SourceMaterialization.CAPTURED, | ||
| content=_canonical_source_text(content), | ||
| wire_content=None if isinstance(content, str) else _JSON_VALUE.validate_python(content, strict=True), |
There was a problem hiding this comment.
wire_content=None represents both a plain string Source and JSON null. As a result, content: null is stored and returned as the JSON string "null". Please preserve the distinction between an absent wire_content value and an explicit JSON null.
There was a problem hiding this comment.
I’ll add an explicit persisted presence discriminator for wire_content, keep missing values backward-compatible with existing text Sources, and update response/digest mapping to use it. Regression tests will cover JSON null, the string "null", persistence round-trips, and HTTP responses.
| selected_rows = rows[:limit] | ||
| items = [] | ||
| for row in selected_rows: | ||
| artifact = await self._artifacts.get( |
There was a problem hiding this comment.
This loop performs a separate Artifact and Source/Artifact reference read for every item after loading the heads. A 100-item page therefore produces many database queries, while RFC #1437 requires the response data to be read in batches. Please batch-load the selected Revisions and their sources and artifacts.
There was a problem hiding this comment.
Agreed. The current implementation is an N+1 path: after loading the heads, each item performs one Revision query and two lineage queries.
I’ll add a repository-level batch loader keyed by the complete Revision identity, batch-load Source and Artifact lineage, and assemble results in page and ordinal order. The list path will then use a bounded constant number of queries. I’ll add ordering, lineage, pagination, and query-budget regression tests.
| self._artifacts = artifacts | ||
| self._clock = _utc_now if clock is None else clock | ||
| self._id_factory = _resource_id if id_factory is None else id_factory | ||
| self._cursor_secret = secrets.token_bytes(32) if cursor_secret is None else cursor_secret |
There was a problem hiding this comment.
This default creates a new cursor_secret whenever the Runtime starts. A cursor issued before a server restart therefore becomes invalid before its recorded expiration time. Please provide a stable configured secret, or define server restart as part of the cursor validity contract.
|
resolve conflicts |
…ct-rest-api-implementation # Conflicts: # integrations/dsh/plugins/powercontext/lib/index.js # integrations/dsh/plugins/powercontext/openapi/powercontext.yaml # integrations/dsh/plugins/powercontext/scripts/openapi-ops.mjs # integrations/dsh/plugins/powercontext/src/client.ts # integrations/dsh/plugins/powercontext/src/operations.generated.ts # integrations/dsh/plugins/powercontext/tests/client.spec.ts # integrations/dsh/plugins/powercontext/tests/operations-coverage.spec.ts # integrations/opencode/plugins/powercontext/lib/index.js # integrations/opencode/plugins/powercontext/src/client.ts # integrations/opencode/plugins/powercontext/src/operations.generated.ts # integrations/pi/plugins/powercontext/src/client.ts # integrations/pi/plugins/powercontext/src/operations.generated.ts # openapi/powercontext.yaml # scripts/generate_api.py # scripts/generate_js_operations.py # src/powercontext/builtin/runtime/application.py # src/powercontext/builtin/runtime/relational.py # src/powercontext/client/client.py # src/powercontext/http/__init__.py # src/powercontext/http/_generated/operations.py # src/powercontext/server/app.py # tests/e2e/test_builtin_runtime.py # tests/test_api_contract.py # tests/test_client.py # tests/test_js_operations.py
…ct-rest-api-implementation
Which issue or RFC does this PR close?
Implements RFC #1437.
Scope creation, retrieval, and binding remain covered by #1401.
Rationale for this change
RFC #1437 defines a consistent base HTTP API for directly accessing durable Sources and committed Artifacts below an existing Scope.
This change implements that contract across the OpenAPI specification, server, persistence layer, Python client, and integration clients. It keeps Source creation independent from downstream generation and preserves the existing domain-specific Memory, Candidate review, Skill, and Handoff workflows.
What changes are included in this PR?
source_id(scope_id, source_type, source_id)GETartifact_idGETLocation, ETag,If-Match, andIf-None-Matchbehavior for creation, optimistic concurrency, and conditional reads.pc_sources.created_atpc_artifacts.created_atpc_artifact_heads.deleted_atopenapi/powercontext.yamland regenerate the checked-in Python models, operations, and schema.
Are there any user-facing changes?
Yes. This is an additive API change.
The new endpoints expose:
List and Search share the same collection endpoints: an omitted or blank
queryperforms List, while a non-emptyqueryperforms Search.
Existing domain-specific APIs and workflows remain available and are not replaced by these base APIs. No existing public endpoint is removed or changed.
How was this change tested?
make checkmake contract-testmake api-generate-checkmake js-api-generate-checkmake test(1047 passed, 12 skipped)git diff --check
AI usage statement
OpenAI Codex was used to update the cross-layer contract and generated clients, add regression coverage, review implementation risks, and draft the PR description.