docs(6206): retrieval, snippets, notes routes and rule_usage model drop citations
Corrected while reading: routes/notes.py said lessons and snippets pass the owner to set_record_systems, "a real inconsistency". Every door passes the caller; the comment now says so. rule_usage's model docstring states what an outcome measures and why the row cannot share the note table, without the measurement and record ids that motivated it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,16 +7,15 @@ from scribe.models.base import CreatedAtMixin, iso
|
|||||||
SURFACED = "surfaced"
|
SURFACED = "surfaced"
|
||||||
PULLED = "pulled"
|
PULLED = "pulled"
|
||||||
|
|
||||||
# The outcome half (#4212, milestone 419). A rule that was read and then
|
# The outcome half. Without these, a rule that was read and then ignored is
|
||||||
# ignored has always been indistinguishable from one that was read and
|
# indistinguishable from one that was read and obeyed; these are the two
|
||||||
# obeyed; these are the two events that can tell them apart.
|
# events that can tell them apart.
|
||||||
#
|
#
|
||||||
# There is deliberately NO third value for "read and ignored". That state is
|
# There is deliberately NO third value for "read and ignored". That state is
|
||||||
# real and is the whole point of the milestone, but it cannot be reported:
|
# real and is the point of measuring outcomes, but it cannot be reported: an
|
||||||
# an agent that knew it was ignoring a rule would not be ignoring it. It is
|
# agent that knew it was ignoring a rule would not be ignoring it. It is
|
||||||
# DERIVED — a pull with no outcome — and an enum member for it would collect
|
# DERIVED — a pull with no outcome — and an enum member for it would collect
|
||||||
# nothing while reading as though it had measured something, which is #3311's
|
# nothing while reading as though it had measured something.
|
||||||
# failure exactly.
|
|
||||||
APPLIED = "applied"
|
APPLIED = "applied"
|
||||||
DEPARTED = "departed"
|
DEPARTED = "departed"
|
||||||
OUTCOMES = (APPLIED, DEPARTED)
|
OUTCOMES = (APPLIED, DEPARTED)
|
||||||
@@ -26,15 +25,14 @@ class RuleUsageEvent(Base, CreatedAtMixin):
|
|||||||
"""One row per time a rule was SURFACED to the agent, PULLED in full, or
|
"""One row per time a rule was SURFACED to the agent, PULLED in full, or
|
||||||
ACTED ON — applied, or departed from with a stated reason.
|
ACTED ON — applied, or departed from with a stated reason.
|
||||||
|
|
||||||
The sibling `note_usage_events` has had since 2026-07, third in the line
|
The rule-side sibling of `note_usage_events`, beside `rule_embeddings`
|
||||||
after `rule_embeddings` and `rule_versions` — and, like those, it exists
|
and `rule_versions` — each exists because machinery built for notes gives
|
||||||
because the rule side kept inheriting machinery built for notes and
|
rules the weaker version of it.
|
||||||
quietly getting the weaker version of it.
|
|
||||||
|
|
||||||
WHY RULES NEED THEIR OWN AND CANNOT SHARE THE NOTE TABLE. Not squeamishness
|
WHY RULES NEED THEIR OWN AND CANNOT SHARE THE NOTE TABLE. Not squeamishness
|
||||||
about a polymorphic column — the row shares no note-specific fields and the
|
about a polymorphic column — the row shares no note-specific fields and the
|
||||||
aggregate readout is the same shape, which is the strongest case for
|
aggregate readout is the same shape, which is the strongest case for
|
||||||
sharing that note #3163 admits. What decides it is IDENTITY AT RESTORE. A
|
sharing there is. What decides it is IDENTITY AT RESTORE. A
|
||||||
note id and a rule id are different namespaces resolved through different
|
note id and a rule id are different namespaces resolved through different
|
||||||
maps, and `note_usage_events`'s importer maps `note_id` through
|
maps, and `note_usage_events`'s importer maps `note_id` through
|
||||||
`note_id_map` and drops what does not resolve. A rule id parked in that
|
`note_id_map` and drops what does not resolve. A rule id parked in that
|
||||||
@@ -42,11 +40,10 @@ class RuleUsageEvent(Base, CreatedAtMixin):
|
|||||||
happened to take that number — telemetry that is not merely lost but wrong,
|
happened to take that number — telemetry that is not merely lost but wrong,
|
||||||
and wrong in a way nothing downstream could detect.
|
and wrong in a way nothing downstream could detect.
|
||||||
|
|
||||||
WHAT THIS MEASURES, AND WHY IT DID NOT EXIST. The write-path standing-rule
|
WHAT THIS MEASURES. A standing-rule arm's usefulness cannot be observed
|
||||||
arm is the only retrieval surface in Scribe whose usefulness cannot be
|
from its scores: an arm can fire on every call, clearing its threshold
|
||||||
observed — and, not coincidentally, the only one that has never declined to
|
every time, and say nothing about whether it helped. `retrieval_logs`
|
||||||
fire (#3311: 296 calls, zero zero-result, 100% clearing its threshold).
|
gives it scores; scores say what the ranker thought, never
|
||||||
`retrieval_logs` gives it scores; scores say what the ranker thought, never
|
|
||||||
whether the hint landed. Without a pull counter no install can tune the arm
|
whether the hint landed. Without a pull counter no install can tune the arm
|
||||||
from evidence, only from the shape of a histogram.
|
from evidence, only from the shape of a histogram.
|
||||||
|
|
||||||
@@ -58,7 +55,7 @@ class RuleUsageEvent(Base, CreatedAtMixin):
|
|||||||
forty times and opened never, because that evidence is precisely the case
|
forty times and opened never, because that evidence is precisely the case
|
||||||
for having deleted it.
|
for having deleted it.
|
||||||
|
|
||||||
Cells left deliberately empty (note #3163's step 3): no share ACL — rules
|
Cells left deliberately empty: no share ACL — rules
|
||||||
have none of their own; no soft delete — nothing restores a telemetry row,
|
have none of their own; no soft delete — nothing restores a telemetry row,
|
||||||
and the table is append-only; no embedding — an event is not a document.
|
and the table is append-only; no embedding — an event is not a document.
|
||||||
"""
|
"""
|
||||||
@@ -76,7 +73,7 @@ class RuleUsageEvent(Base, CreatedAtMixin):
|
|||||||
# 'surfaced' | 'pulled' | 'applied' | 'departed'
|
# 'surfaced' | 'pulled' | 'applied' | 'departed'
|
||||||
#
|
#
|
||||||
# Plain Text with no CHECK, as created in 0094 — which is why 0106 added
|
# Plain Text with no CHECK, as created in 0094 — which is why 0106 added
|
||||||
# the outcome pair without a DROP/ADD migration. Rule 36 governs
|
# the outcome pair without a DROP/ADD migration — that step is for
|
||||||
# CHECK-whitelisted columns and this is not one. Said here as well as in
|
# CHECK-whitelisted columns and this is not one. Said here as well as in
|
||||||
# the migration because this is where the next person adding a value will
|
# the migration because this is where the next person adding a value will
|
||||||
# look first.
|
# look first.
|
||||||
@@ -84,9 +81,9 @@ class RuleUsageEvent(Base, CreatedAtMixin):
|
|||||||
|
|
||||||
# Which surface produced it. A CONVENTION, not a fixed vocabulary, and the
|
# Which surface produced it. A CONVENTION, not a fixed vocabulary, and the
|
||||||
# note twin's comment explains why this one deliberately does not enumerate
|
# note twin's comment explains why this one deliberately does not enumerate
|
||||||
# its members: the previous such list went stale, naming a source nothing
|
# its members: such a list goes stale, naming a source nothing writes while
|
||||||
# wrote while omitting ones that existed, and a half-true enumeration reads
|
# omitting ones that exist, and a half-true enumeration reads as
|
||||||
# as authoritative in exactly the way that misleads (#2476).
|
# authoritative in exactly the way that misleads.
|
||||||
# `grep -rn record_rule_pulled\|record_rule_surfaced src/` is the
|
# `grep -rn record_rule_pulled\|record_rule_surfaced src/` is the
|
||||||
# authoritative list, and unlike a comment it cannot drift.
|
# authoritative list, and unlike a comment it cannot drift.
|
||||||
#
|
#
|
||||||
|
|||||||
+11
-15
@@ -131,10 +131,8 @@ async def create_note_route():
|
|||||||
# 403, not 400: the request is well-formed and the caller simply
|
# 403, not 400: the request is well-formed and the caller simply
|
||||||
# may not write the target. The note itself was created.
|
# may not write the target. The note itself was created.
|
||||||
return jsonify({"error": str(exc), "note": note.to_dict()}), 403
|
return jsonify({"error": str(exc), "note": note.to_dict()}), 403
|
||||||
# #4249: this door could not file a note to a System at all, while the
|
# Both doors file a note to Systems: the door nobody exercises for a kind
|
||||||
# MCP door could — the reverse of the asymmetry the same issue records for
|
# is the one that never grows the parameter.
|
||||||
# lessons. The pattern is not that one door is richer; it is that the door
|
|
||||||
# nobody exercised for a kind is the one that never grew the parameter.
|
|
||||||
if data.get("system_ids") is not None:
|
if data.get("system_ids") is not None:
|
||||||
await systems_svc.set_record_systems(uid, note.id, data["system_ids"])
|
await systems_svc.set_record_systems(uid, note.id, data["system_ids"])
|
||||||
out = note.to_dict()
|
out = note.to_dict()
|
||||||
@@ -235,7 +233,7 @@ async def get_note_route(note_id: int):
|
|||||||
# join away, but which surface asked is not recoverable after the fact.
|
# join away, but which surface asked is not recoverable after the fact.
|
||||||
# Keeping rest_* apart from mcp_* is load-bearing, not tidiness — "was that
|
# Keeping rest_* apart from mcp_* is load-bearing, not tidiness — "was that
|
||||||
# injected line useful?" is answered by agent pulls alone, and a human
|
# injected line useful?" is answered by agent pulls alone, and a human
|
||||||
# clicking a link would inflate exactly the number #1038 and #2085 gate on.
|
# clicking a link would inflate exactly the number pruning gates on.
|
||||||
record_pulled(user_id=uid, note_id=note_id, source="rest_note")
|
record_pulled(user_id=uid, note_id=note_id, source="rest_note")
|
||||||
await supersession_svc.attach_relations(uid, note_id, data)
|
await supersession_svc.attach_relations(uid, note_id, data)
|
||||||
return jsonify(data)
|
return jsonify(data)
|
||||||
@@ -246,8 +244,8 @@ async def get_note_route(note_id: int):
|
|||||||
async def update_note_route(note_id: int):
|
async def update_note_route(note_id: int):
|
||||||
"""Partial update — only the keys present in the payload change. PUT and
|
"""Partial update — only the keys present in the payload change. PUT and
|
||||||
PATCH are the same handler on purpose: the form sends the field set it
|
PATCH are the same handler on purpose: the form sends the field set it
|
||||||
edited, and the two verbs used to be two near-identical copies of this
|
edited, and two copies of this function would drift (one carrying the
|
||||||
function that drifted (one carried the supersedes contract, one did not)."""
|
supersedes contract, one not)."""
|
||||||
uid = get_current_user_id()
|
uid = get_current_user_id()
|
||||||
# Share-aware: resolve through the ACL and write as the OWNER, so a shared
|
# Share-aware: resolve through the ACL and write as the OWNER, so a shared
|
||||||
# editor's save isn't rejected by the owner-scoped update service.
|
# editor's save isn't rejected by the owner-scoped update service.
|
||||||
@@ -265,7 +263,7 @@ async def update_note_route(note_id: int):
|
|||||||
"milestone_id", "status", "priority", "note_type",
|
"milestone_id", "status", "priority", "note_type",
|
||||||
# A cleared form input arrives as "" and the service reads that as
|
# A cleared form input arrives as "" and the service reads that as
|
||||||
# NULL (NULLABLE_NOTE_TEXT), so this door expresses "remove the check"
|
# NULL (NULLABLE_NOTE_TEXT), so this door expresses "remove the check"
|
||||||
# with its own idiom and needs no `clear` list (milestone 317).
|
# with its own idiom and needs no `clear` list.
|
||||||
"verify_with", "expires_when",
|
"verify_with", "expires_when",
|
||||||
):
|
):
|
||||||
if key in data:
|
if key in data:
|
||||||
@@ -298,12 +296,10 @@ async def update_note_route(note_id: int):
|
|||||||
return jsonify({"error": str(exc)}), 403
|
return jsonify({"error": str(exc)}), 403
|
||||||
# Set-semantics like the above: present-and-empty clears, absent leaves
|
# Set-semantics like the above: present-and-empty clears, absent leaves
|
||||||
# alone. Scoped by the CALLER rather than owner_uid, for the same reason
|
# alone. Scoped by the CALLER rather than owner_uid, for the same reason
|
||||||
# the supersedes call is (#47) — `set_record_systems` links only Systems
|
# the supersedes call is — `set_record_systems` links only Systems the
|
||||||
# the acting user can read, and an editor-share holder should tag from
|
# acting user can read, and an editor-share holder should tag from what
|
||||||
# what they can see rather than inherit the owner's reach. This matches
|
# they can see rather than inherit the owner's reach. Every door that
|
||||||
# routes/tasks.py; routes/lessons.py and routes/snippets.py pass the owner
|
# tags a record (tasks, lessons, snippets) does the same.
|
||||||
# for the same operation, which is a real inconsistency, recorded in #4249
|
|
||||||
# rather than changed here.
|
|
||||||
if "system_ids" in data:
|
if "system_ids" in data:
|
||||||
await systems_svc.set_record_systems(uid, note_id, data["system_ids"] or [])
|
await systems_svc.set_record_systems(uid, note_id, data["system_ids"] or [])
|
||||||
out = note.to_dict()
|
out = note.to_dict()
|
||||||
@@ -521,7 +517,7 @@ async def graph_route():
|
|||||||
return jsonify(graph)
|
return jsonify(graph)
|
||||||
|
|
||||||
|
|
||||||
# ── The staleness sweep (milestone 317) ──────────────────────────────────────
|
# ── The staleness sweep ──────────────────────────────────────────────────────
|
||||||
# The web half of notes_due_for_verification / mark_note_verified. Same
|
# The web half of notes_due_for_verification / mark_note_verified. Same
|
||||||
# contract as the MCP door and the rules routes beside it — the service holds
|
# contract as the MCP door and the rules routes beside it — the service holds
|
||||||
# the behaviour, these two just parse and serialise.
|
# the behaviour, these two just parse and serialise.
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"""The operator's view of retrieval tuning — what the dials are, and who moved them (#4102).
|
"""The operator's view of retrieval tuning — what the dials are, and who moved them.
|
||||||
|
|
||||||
WHY THIS EXISTS SEPARATELY FROM /api/settings
|
WHY THIS EXISTS SEPARATELY FROM /api/settings
|
||||||
|
|
||||||
The numbers themselves are ordinary settings rows and could be written through
|
The numbers themselves are ordinary settings rows and could be written through
|
||||||
the generic KV endpoint. They must not be, and that is the whole point of this
|
the generic KV endpoint. They must not be, and that is the whole point of this
|
||||||
blueprint: a floor changed through `/api/settings` moves silently, leaving the
|
blueprint: a floor changed through `/api/settings` moves silently, leaving the
|
||||||
history saying nothing happened. From milestone 416 those dials are moved by
|
history saying nothing happened. Those dials are moved by the model on the
|
||||||
the model on the operator's behalf, so a trail with holes in it is worse than
|
operator's behalf, so a trail with holes in it is worse than
|
||||||
no trail — it reads as complete.
|
no trail — it reads as complete.
|
||||||
|
|
||||||
So every change to a retrieval dial goes through `set_dial`, from the UI as
|
So every change to a retrieval dial goes through `set_dial`, from the UI as
|
||||||
@@ -39,7 +39,7 @@ retrieval_bp = Blueprint("retrieval", __name__, url_prefix="/api/retrieval")
|
|||||||
# behalf. The operator IS that someone: they are the audience the trail is
|
# behalf. The operator IS that someone: they are the audience the trail is
|
||||||
# written for, they cannot be uninformed about their own decision, and a
|
# written for, they cannot be uninformed about their own decision, and a
|
||||||
# mandatory justification textarea on every control would be friction charged
|
# mandatory justification textarea on every control would be friction charged
|
||||||
# to the one participant who owes no explanation (rule 24).
|
# to the one participant who owes no explanation.
|
||||||
#
|
#
|
||||||
# The event is still written, because "the operator set this by hand" is the
|
# The event is still written, because "the operator set this by hand" is the
|
||||||
# single most useful thing the history can tell a later session — it is the one
|
# single most useful thing the history can tell a later session — it is the one
|
||||||
@@ -116,12 +116,12 @@ async def tuning_history_route():
|
|||||||
@retrieval_bp.route("/moments", methods=["GET"])
|
@retrieval_bp.route("/moments", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
async def moments_route():
|
async def moments_route():
|
||||||
"""The moments of work that rules mount on (milestone 458), with the
|
"""The moments of work that rules mount on, with the
|
||||||
actions that reach each one on this install.
|
actions that reach each one on this install.
|
||||||
|
|
||||||
The same payload `list_moments` returns, from the same services, so the
|
The same payload `list_moments` returns, from the same services, so the
|
||||||
session and the Settings view cannot name different moments — plus what
|
session and the Settings view cannot name different moments — plus what
|
||||||
the view shows beside each one (step 6): `mounted`, how many rules each
|
the view shows beside each one: `mounted`, how many rules each
|
||||||
carries, and `usage`, the deliveries and opens per moment over `days`
|
carries, and `usage`, the deliveries and opens per moment over `days`
|
||||||
(the block `retrieval_telemetry` reports as `moment_usage`).
|
(the block `retrieval_telemetry` reports as `moment_usage`).
|
||||||
"""
|
"""
|
||||||
@@ -147,11 +147,11 @@ async def moments_route():
|
|||||||
@retrieval_bp.route("/reply-shapes", methods=["GET"])
|
@retrieval_bp.route("/reply-shapes", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
async def reply_shapes_route():
|
async def reply_shapes_route():
|
||||||
"""The default reply shapes and the moment each rides (milestone 500).
|
"""The default reply shapes and the moment each rides.
|
||||||
|
|
||||||
`list_reply_shapes`' payload from the same service, so the Settings view
|
`list_reply_shapes`' payload from the same service, so the Settings view
|
||||||
and the session cannot show different defaults — plus what only the
|
and the session cannot show different defaults — plus what only the
|
||||||
operator's view needs (step 5): per shape, the preferences mounted on its
|
operator's view needs: per shape, the preferences mounted on its
|
||||||
moment and its deliveries over `days` (default 30, 1–90).
|
moment and its deliveries over `days` (default 30, 1–90).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@@ -201,7 +201,7 @@ async def unmap_action_route():
|
|||||||
@retrieval_bp.route("/moments/proposals", methods=["GET"])
|
@retrieval_bp.route("/moments/proposals", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
async def moment_proposals_route():
|
async def moment_proposals_route():
|
||||||
"""The moment proposals waiting on a judgment (milestone 458 step 7) —
|
"""The moment proposals waiting on a judgment —
|
||||||
`rule_moment_proposals`' payload, from the same service. `?rule_id=`
|
`rule_moment_proposals`' payload, from the same service. `?rule_id=`
|
||||||
narrows to one rule, for the rule editor."""
|
narrows to one rule, for the rule editor."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ routes feed the web management UI; the MCP tools (mcp/tools/snippets.py) are the
|
|||||||
agent-facing surface. Both go through services/snippets.py, so the
|
agent-facing surface. Both go through services/snippets.py, so the
|
||||||
serialize/parse contract and embedding-on-create live in one place (DRY).
|
serialize/parse contract and embedding-on-create live in one place (DRY).
|
||||||
|
|
||||||
ACL (rule #78): reads/writes of a single snippet resolve through the share-aware
|
ACL: reads/writes of a single snippet resolve through the share-aware
|
||||||
`get_note_for_user` + `can_write_note`, and writes are performed as the OWNER so
|
`get_note_for_user` + `can_write_note`, and writes are performed as the OWNER so
|
||||||
a shared editor isn't rejected by the owner-scoped service — mirroring
|
a shared editor isn't rejected by the owner-scoped service — mirroring
|
||||||
routes/notes.py. The list is owner-scoped, matching the note-browse surface.
|
routes/notes.py. The list is owner-scoped, matching the note-browse surface.
|
||||||
@@ -65,7 +65,7 @@ async def list_snippets_route():
|
|||||||
repo = request.args.get("repo", "")
|
repo = request.args.get("repo", "")
|
||||||
path = request.args.get("path", "")
|
path = request.args.get("path", "")
|
||||||
symbol = request.args.get("symbol", "")
|
symbol = request.args.get("symbol", "")
|
||||||
# Drift check (#2086). "attention" is what the UI's filter chip sends.
|
# Drift check. "attention" is what the UI's filter chip sends.
|
||||||
verification = request.args.get("verification", "")
|
verification = request.args.get("verification", "")
|
||||||
limit, offset = parse_pagination()
|
limit, offset = parse_pagination()
|
||||||
items, total = await snippets_svc.list_snippets(
|
items, total = await snippets_svc.list_snippets(
|
||||||
@@ -115,7 +115,7 @@ async def create_snippet_route():
|
|||||||
is_task=False,
|
is_task=False,
|
||||||
note_type=snippets_svc.SNIPPET_NOTE_TYPE,
|
note_type=snippets_svc.SNIPPET_NOTE_TYPE,
|
||||||
# Matched on the artefact — location and code — before prose, the
|
# Matched on the artefact — location and code — before prose, the
|
||||||
# same way the MCP create path does (#2518). Both surfaces must
|
# same way the MCP create path does. Both surfaces must
|
||||||
# apply the identical gate or the web UI becomes the way to record
|
# apply the identical gate or the web UI becomes the way to record
|
||||||
# a duplicate the agent would have been stopped from writing.
|
# a duplicate the agent would have been stopped from writing.
|
||||||
code=data.get("code", ""),
|
code=data.get("code", ""),
|
||||||
@@ -163,7 +163,7 @@ async def get_snippet_route(snippet_id: int):
|
|||||||
return not_found("Snippet")
|
return not_found("Snippet")
|
||||||
note, permission = loaded
|
note, permission = loaded
|
||||||
data = snippets_svc.snippet_to_dict(note)
|
data = snippets_svc.snippet_to_dict(note)
|
||||||
# Forge-checked freshness (#2690) — same decoration the MCP pull gets.
|
# Forge-checked freshness — same decoration the MCP pull gets.
|
||||||
await snippets_svc.attach_live_body(note, data)
|
await snippets_svc.attach_live_body(note, data)
|
||||||
data["permission"] = permission
|
data["permission"] = permission
|
||||||
# Read the association as the OWNER: a shared reader isn't scoped to the
|
# Read the association as the OWNER: a shared reader isn't scoped to the
|
||||||
@@ -177,7 +177,7 @@ async def get_snippet_route(snippet_id: int):
|
|||||||
await attach_usage([data])
|
await attach_usage([data])
|
||||||
# Opening the detail view IS a pull — the operator chose to look. Tagged
|
# Opening the detail view IS a pull — the operator chose to look. Tagged
|
||||||
# apart from the MCP sources so "the agent reused it" and "a human read it"
|
# apart from the MCP sources so "the agent reused it" and "a human read it"
|
||||||
# stay distinguishable; they mean different things for pruning (#2085).
|
# stay distinguishable; they mean different things for pruning.
|
||||||
record_pulled(user_id=uid, note_id=snippet_id, source="rest_snippet")
|
record_pulled(user_id=uid, note_id=snippet_id, source="rest_snippet")
|
||||||
return jsonify(data)
|
return jsonify(data)
|
||||||
|
|
||||||
@@ -212,7 +212,7 @@ async def update_snippet_route(snippet_id: int):
|
|||||||
if updated is None:
|
if updated is None:
|
||||||
return not_found("Snippet")
|
return not_found("Snippet")
|
||||||
if data.get("system_ids") is not None:
|
if data.get("system_ids") is not None:
|
||||||
# The CALLER, not owner_uid — see routes/lessons.py for the why (#4249).
|
# The CALLER, not owner_uid — see routes/lessons.py for the why.
|
||||||
await systems_svc.set_record_systems(uid, snippet_id, data["system_ids"])
|
await systems_svc.set_record_systems(uid, snippet_id, data["system_ids"])
|
||||||
out = snippets_svc.snippet_to_dict(updated)
|
out = snippets_svc.snippet_to_dict(updated)
|
||||||
out["systems"] = [
|
out["systems"] = [
|
||||||
|
|||||||
Reference in New Issue
Block a user