Skip to content
Merged
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
4 changes: 2 additions & 2 deletions docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1556,7 +1556,7 @@ Known domain failures: `400` Scheduling/input errors; `404` NOT_FOUND.

#### `POST /api/ideas/{ident}/adopt`

Create hypothesis plus configured experiment, or hypothesis/implementation/experiment nodes when implementation is needed. Optional expected_revision is the project graph revision. request_id is 1..120 characters and adoption-specific; same idea retry returns current graph. inputs accepts up to 50 explicit path/reference inputs in this project. config is the executable experiment configuration, not a dummy case. Return the updated GraphState as one graph edit/undo.
Create hypothesis plus configured experiment, or hypothesis/implementation/experiment nodes when implementation is needed. Optional expected_revision is the project graph revision. request_id is 1..120 characters and adoption-specific; replaying the same request_id for the same idea returns the current graph. An idea that already has nodes in the graph rejects a second adoption without that receipt with IDEA_ALREADY_ADOPTED (409). Undoing the adoption permits a new path. inputs accepts up to 50 explicit path/reference inputs in this project. config is the executable experiment configuration, not a dummy case. Return the updated GraphState as one graph edit/undo.

| Parameter | Location | Type | Required | Default / description |
| --- | --- | --- | --- | --- |
Expand All @@ -1566,7 +1566,7 @@ Body: `application/json`: `IdeaAdoptRequest`; optional.

Success `200`: `application/json`: `GraphState`.

Known domain failures: `404` NOT_FOUND; `409` REVISION_CONFLICT, REQUEST_ID_CONFLICT or BRANCH_INACTIVE; `422` INVALID_REQUEST_ID, INVALID_EXPERIMENT_CONFIG, INVALID_INPUTS, CROSS_PROJECT, MISSING_BRANCH, INVALID_TITLE or INVALID_INSTRUCTIONS.
Known domain failures: `404` NOT_FOUND; `409` REVISION_CONFLICT, REQUEST_ID_CONFLICT, BRANCH_INACTIVE or IDEA_ALREADY_ADOPTED; `422` INVALID_REQUEST_ID, INVALID_EXPERIMENT_CONFIG, INVALID_INPUTS, CROSS_PROJECT, MISSING_BRANCH, INVALID_TITLE or INVALID_INSTRUCTIONS.

#### `PATCH /api/projects/{ident}/objective`

Expand Down
4 changes: 2 additions & 2 deletions packages/contracts/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -13274,7 +13274,7 @@
},
"/api/ideas/{ident}/adopt": {
"post": {
"description": "Create hypothesis plus configured experiment, or hypothesis/implementation/experiment nodes when implementation is needed. Optional expected_revision is the project graph revision. request_id is 1..120 characters and adoption-specific; same idea retry returns current graph. inputs accepts up to 50 explicit path/reference inputs in this project. config is the executable experiment configuration, not a dummy case. Return the updated GraphState as one graph edit/undo.",
"description": "Create hypothesis plus configured experiment, or hypothesis/implementation/experiment nodes when implementation is needed. Optional expected_revision is the project graph revision. request_id is 1..120 characters and adoption-specific; replaying the same request_id for the same idea returns the current graph. An idea that already has nodes in the graph rejects a second adoption without that receipt with IDEA_ALREADY_ADOPTED (409). Undoing the adoption permits a new path. inputs accepts up to 50 explicit path/reference inputs in this project. config is the executable experiment configuration, not a dummy case. Return the updated GraphState as one graph edit/undo.",
"operationId": "adopt_api_ideas__ident__adopt_post",
"parameters": [
{
Expand Down Expand Up @@ -13345,7 +13345,7 @@
}
}
},
"description": "REVISION_CONFLICT, REQUEST_ID_CONFLICT or BRANCH_INACTIVE"
"description": "REVISION_CONFLICT, REQUEST_ID_CONFLICT, BRANCH_INACTIVE or IDEA_ALREADY_ADOPTED"
},
"422": {
"content": {
Expand Down
2 changes: 1 addition & 1 deletion services/api/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,7 @@ def operation(method: str, path: str, tag: str, summary: str, description: str,
("/api/analysis/run", "analysis", "Enqueue an analysis task over declared real inputs; output fields depend on the selected analysis type.")):
operation("post", path, "Research", f"Enqueue {kind} work", description + " Return Run, not the final scientific output.", ref("Run"), body="QueuedTaskRequest", errors={400: "Scheduling/input errors", 404: "NOT_FOUND"})
operation("post", "/api/ideas/{ident}/adopt", "Research", "Adopt an idea into an editable execution path",
"Create hypothesis plus configured experiment, or hypothesis/implementation/experiment nodes when implementation is needed. Optional expected_revision is the project graph revision. request_id is 1..120 characters and adoption-specific; same idea retry returns current graph. inputs accepts up to 50 explicit path/reference inputs in this project. config is the executable experiment configuration, not a dummy case. Return the updated GraphState as one graph edit/undo.", ref("GraphState"), body="IdeaAdoptRequest", errors={404: "NOT_FOUND", 409: "REVISION_CONFLICT, REQUEST_ID_CONFLICT or BRANCH_INACTIVE", 422: "INVALID_REQUEST_ID, INVALID_EXPERIMENT_CONFIG, INVALID_INPUTS, CROSS_PROJECT, MISSING_BRANCH, INVALID_TITLE or INVALID_INSTRUCTIONS"})
"Create hypothesis plus configured experiment, or hypothesis/implementation/experiment nodes when implementation is needed. Optional expected_revision is the project graph revision. request_id is 1..120 characters and adoption-specific; replaying the same request_id for the same idea returns the current graph. An idea that already has nodes in the graph rejects a second adoption without that receipt with IDEA_ALREADY_ADOPTED (409). Undoing the adoption permits a new path. inputs accepts up to 50 explicit path/reference inputs in this project. config is the executable experiment configuration, not a dummy case. Return the updated GraphState as one graph edit/undo.", ref("GraphState"), body="IdeaAdoptRequest", errors={404: "NOT_FOUND", 409: "REVISION_CONFLICT, REQUEST_ID_CONFLICT, BRANCH_INACTIVE or IDEA_ALREADY_ADOPTED", 422: "INVALID_REQUEST_ID, INVALID_EXPERIMENT_CONFIG, INVALID_INPUTS, CROSS_PROJECT, MISSING_BRANCH, INVALID_TITLE or INVALID_INSTRUCTIONS"})
operation("post", "/api/theory/check", "Research", "Evaluate a restricted symbolic expression",
"Require project_id/expression; variable defaults x. kind differentiate/solve/numeric selects an operation; any other/omitted kind simplifies. values supplies numeric substitutions. Restricted arithmetic grammar permits sin/cos/exp/log/sqrt/abs, bounded constants/powers and no arbitrary eval. Save a checked Derivation RecordItem, with result/latex and an explicit supplied-expression scope.", ref("RecordItem"), body="SymbolicCheckRequest", errors={400: "INVALID_EXPRESSION", 404: "NOT_FOUND", 422: "EXPRESSION_FAILED"})
operation("post", "/api/experiments/{ident}/launch", "Experiments", "Enqueue a saved experiment specification",
Expand Down
2 changes: 2 additions & 0 deletions services/api/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ def adopt(ident:str,body:dict=Body(default={})):
return g
if body.get('expected_revision',p.revision)!=p.revision:
error('REVISION_CONFLICT','The project changed; refresh before adopting the idea',409)
if any(node.get('adopted_idea_id')==idea.id for node in g['nodes']):
error('IDEA_ALREADY_ADOPTED','This idea has already been adopted; use its existing execution path',409)
original=deepcopy(g); commands=[]
config=deepcopy(body.get('config',idea.data.get('experiment_config',{})))
if not isinstance(config,dict): error('INVALID_EXPERIMENT_CONFIG','Experiment config must be an object',422)
Expand Down
38 changes: 38 additions & 0 deletions tests/test_idea_adoption.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,44 @@ def concurrent(_): return adopted(client,i,request_id='one-adoption',expected_re
assert s.scalar(select(func.count()).select_from(CommandReceipt).where(CommandReceipt.project_id==p['id']))==1


def case_repeat_adoption_rejected_without_shared_receipt(client):
p=create(client); i=idea(client,p)
g=ok(adopted(client,i))
for body in ({},{'request_id':'different-adoption','title':'Another title'}):
rejected=adopted(client,i,**body)
assert rejected.status_code==409,rejected.text
assert rejected.json()['detail']['code']=='IDEA_ALREADY_ADOPTED'
assert graph(client,p)==g
assert ok(client.get(f"/api/ideas/{i['id']}"))['status']=='adopted'
assert ok(client.get(f"/api/projects/{p['id']}/runs"))==[]
from sqlalchemy import select,func
from services.api.db import Session,CommandReceipt
with Session() as s:
assert s.scalar(select(func.count()).select_from(CommandReceipt).where(CommandReceipt.project_id==p['id']))==1


def case_adoption_after_undo_can_create_a_new_path(client):
p=create(client); i=idea(client,p)
first=ok(adopted(client,i))
undone=ok(command(client,p,'undo'))['graph']
assert undone['nodes']==[]
second=ok(adopted(client,i,request_id='after-undo',expected_revision=undone['revision']))
assert second['revision']==undone['revision']+1 and len(second['nodes'])==3
assert {n['id'] for n in first['nodes']}.isdisjoint(n['id'] for n in second['nodes'])
assert ok(client.get(f"/api/projects/{p['id']}/runs"))==[]


def case_concurrent_adoptions_without_shared_receipt(client):
p=create(client); i=idea(client,p)
with ThreadPoolExecutor(max_workers=5) as pool:
results=list(pool.map(lambda _:adopted(client,i),range(5)))
assert sorted(r.status_code for r in results)==[200,409,409,409,409],[(r.status_code,r.text) for r in results]
assert all(r.json()['detail']['code']=='IDEA_ALREADY_ADOPTED' for r in results if r.status_code==409)
g=graph(client,p)
assert g['revision']==1 and len(g['nodes'])==3 and len(g['edges'])==2
assert ok(client.get(f"/api/projects/{p['id']}/runs"))==[]


def case_completed_producer_file_reused_but_stale_rejected(client):
p=create(client); i=idea(client,p)
g=ok(adopted(client,i,config={'kind':'command','command':['python','pilot.py']}))
Expand Down