docs(6206): settings, projects, api routes and three models drop citations; four capitalised services stragglers
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m7s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 24s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m7s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 24s
- api.py's build-version docstring states why a channel name is not a build id, without the dated incident. - rule_version states why rules need a history, without the rule that was rescoped and the date. - Capitalised "Rule N" / "Milestone N" citations the earlier lowercase grep missed, in reply_shapes, supersession, services/family and models/family. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Family canon (milestone 463): ideas that every project on a platform shares,
|
||||
"""Family canon: ideas that every project on a platform shares,
|
||||
and each project's recorded answer to them.
|
||||
|
||||
A FAMILY IDEA is not a new kind of record. It is an existing `notes` row — a
|
||||
@@ -45,7 +45,7 @@ from scribe.models import Base
|
||||
from scribe.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso
|
||||
|
||||
# Each tuple is the whitelist its CHECK constraint enforces (migration 0120).
|
||||
# Rule 36: a new value means DROP + ADD CONSTRAINT in the same migration.
|
||||
# A new value means DROP + ADD CONSTRAINT in the same migration.
|
||||
MEMBERSHIP_STATES = ("declared", "detected", "rejected")
|
||||
IDEA_STATUSES = ("candidate", "canon", "retired")
|
||||
ADOPTION_STATUSES = ("unassessed", "adopted", "variant", "exempt", "owed")
|
||||
@@ -73,7 +73,7 @@ class Platform(Base, TimestampMixin, SoftDeleteMixin):
|
||||
form a backup carries, since ids are per-install.
|
||||
|
||||
`markers` are glob patterns over a bound repo's paths that suggest a
|
||||
project is this platform (step 2's detection). A pattern without a slash
|
||||
project is this platform (platform detection). A pattern without a slash
|
||||
matches a file's basename anywhere in the tree; one with a slash matches
|
||||
the repo-relative path. Empty means declare-only: some platforms leave no
|
||||
reliable file behind.
|
||||
@@ -166,7 +166,7 @@ class FamilyIdea(Base, TimestampMixin):
|
||||
comparing the two numbers, never stored as a flag that could go stale.
|
||||
|
||||
`topic_id` is the rule topic holding the idea's binding norms, if it has
|
||||
any (the note↔topic link #3236 asked for), set by `set_topic`. The topic
|
||||
any, set by `set_topic`. The topic
|
||||
never carries platforms of its own; the idea's readouts list its rules,
|
||||
so an assessment judges a project against them.
|
||||
"""
|
||||
@@ -308,8 +308,7 @@ class FamilyDecision(Base, CreatedAtMixin):
|
||||
|
||||
`before` and `after` are STATE snapshots — status, version, reason — and
|
||||
deliberately hold no foreign ids: an id inside JSON cannot be remapped by a
|
||||
restore, and would come back pointing at whatever took that number (the
|
||||
#3182 trap). The ids this row needs are columns.
|
||||
restore, and would come back pointing at whatever took that number. The ids this row needs are columns.
|
||||
|
||||
`precedent_ids` are the earlier decisions this one followed. They are what
|
||||
keeps a call consistent with the last similar one when no person approves
|
||||
@@ -367,7 +366,7 @@ class FamilyDecision(Base, CreatedAtMixin):
|
||||
stored again inside `after` (and the prior one inside `before`), and
|
||||
a promotion's evidence carries the whole criteria reasoning. So a
|
||||
state here is its status, version and platforms only, and evidence is
|
||||
the cited list alone (#5131). The full row is `to_dict`, which the
|
||||
the cited list alone. The full row is `to_dict`, which the
|
||||
decision log serves.
|
||||
"""
|
||||
return {
|
||||
|
||||
@@ -10,12 +10,12 @@ from scribe.models.base import CreatedAtMixin, iso
|
||||
SUGGESTED = "suggested"
|
||||
CONFIRMED = "confirmed"
|
||||
REJECTED = "rejected"
|
||||
# CHECK ck_lesson_rule_links_state (migration 0111, rule 36).
|
||||
# CHECK ck_lesson_rule_links_state (migration 0111).
|
||||
LINK_STATES = (SUGGESTED, CONFIRMED, REJECTED)
|
||||
|
||||
|
||||
class LessonRuleLink(Base, CreatedAtMixin):
|
||||
"""A lesson says it is an instance of a rule (milestone 440, #4196).
|
||||
"""A lesson says it is an instance of a rule.
|
||||
|
||||
A lesson records one concrete situation; a rule records the binding choice
|
||||
for a class of them. The link lets the rule be reached through the
|
||||
@@ -53,7 +53,7 @@ class LessonRuleLink(Base, CreatedAtMixin):
|
||||
# Why it was confirmed or rejected — the reasoning a later reader needs to
|
||||
# decide whether it still holds, as a rule relation's `note` is.
|
||||
note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
# What the suggestion rests on (filled by the co-surfacing recorder, #4637).
|
||||
# What the suggestion rests on (filled by the co-surfacing recorder).
|
||||
# Kept after a judgment so a confirmation can be read beside its evidence.
|
||||
evidence: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
||||
# When a judgment moved it out of `suggested`. Null while suggested.
|
||||
@@ -78,12 +78,12 @@ class LessonRuleLink(Base, CreatedAtMixin):
|
||||
|
||||
|
||||
class LessonNoRule(Base):
|
||||
"""A judgment that no rule governs a lesson's situation (#4631).
|
||||
"""A judgment that no rule governs a lesson's situation.
|
||||
|
||||
The third answer to "which rule is this an instance of?", beside naming
|
||||
one and leaving it open. It is what lets an UNJUDGED lesson be told apart
|
||||
from one that was looked at and found to stand alone — and a lesson with
|
||||
no rule is the raw material #4634 reads for convergence, so the
|
||||
no rule is the raw material convergence reads, so the
|
||||
difference is the whole signal.
|
||||
|
||||
A row of its own rather than a key in the lesson's `data`: that column is
|
||||
|
||||
@@ -10,18 +10,15 @@ from scribe.models.base import CreatedAtMixin, iso
|
||||
class RuleVersion(Base, CreatedAtMixin):
|
||||
"""One snapshot of a rule's text, taken before an edit overwrote it.
|
||||
|
||||
THE SIBLING NOTES ALREADY HAD. `note_versions` has existed for a long
|
||||
time, and the design-system note calls its history "the changelog". Rules
|
||||
— which BIND BEHAVIOUR on every session that loads them — had nothing, so
|
||||
an edit destroyed what the rule used to say. Rescoping rule 79 on
|
||||
2026-08-29 meant hand-copying the superseded statement into a task log to
|
||||
keep it (#3237). The more consequential record had the weaker protection.
|
||||
THE SIBLING OF `note_versions`. Rules BIND BEHAVIOUR on every session that
|
||||
loads them; without a history, an edit destroys what the rule used to say,
|
||||
and the more consequential record has the weaker protection.
|
||||
|
||||
`CreatedAtMixin`, not `TimestampMixin`: a version is an EVENT. It is
|
||||
written once and never updated, so an `updated_at` on it would be a column
|
||||
that can only ever lie.
|
||||
|
||||
WHAT IS DELIBERATELY DIFFERENT FROM NoteVersion (milestone 323):
|
||||
WHAT IS DELIBERATELY DIFFERENT FROM NoteVersion:
|
||||
|
||||
- `user_id` is the ACTOR — who made the edit — where NoteVersion's is the
|
||||
owner, because `update_note` passes an owner-scoped id. For an audit
|
||||
@@ -34,7 +31,7 @@ class RuleVersion(Base, CreatedAtMixin):
|
||||
own lifecycle; folding them in would make one word, "version", mean two
|
||||
different things — the rule's wording, and the rule's place in the
|
||||
graph. `verified_at` is likewise absent: it is a stamp about a check,
|
||||
not a property of the text, and step 2's snapshot is taken before
|
||||
not a property of the text, and the snapshot is taken before
|
||||
update_rule clears it precisely so the history shows the check that was
|
||||
in force when this wording was written.
|
||||
"""
|
||||
|
||||
@@ -12,14 +12,11 @@ async def health():
|
||||
|
||||
def build_version_payload() -> dict:
|
||||
"""What build is this, separated into the values that answer different
|
||||
questions (rule 149).
|
||||
questions.
|
||||
|
||||
UNTIL 2026-08-31 THIS RETURNED THE CHANNEL. `BUILD_VERSION` in CI was
|
||||
literally "dev" / "main" / the tag, so a running instance reported
|
||||
`{"version": "main"}` — a channel name sitting where a build identifier
|
||||
belongs. The cost was concrete: with a deploy misbehaving, nothing on the
|
||||
instance could say which commit was serving it, and the one endpoint whose
|
||||
job that is answered with the name of a branch.
|
||||
A channel name ("dev", "main") is not a build identifier: with a deploy
|
||||
misbehaving, the one endpoint whose job is to say which commit is serving
|
||||
must not answer with the name of a branch.
|
||||
|
||||
The three values, and why they are three:
|
||||
|
||||
@@ -33,7 +30,7 @@ def build_version_payload() -> dict:
|
||||
- `channel` — its own field, never folded into the name.
|
||||
|
||||
Plus `commit`, so the artifact's claim about itself can be checked against
|
||||
the `:<sha>` it was published under (rule 145).
|
||||
the `:<sha>` it was published under.
|
||||
|
||||
ABSENT RATHER THAN EMPTY when unknown. A local build has no ordering key
|
||||
and no channel, and saying so is honest; emitting `""` or a placeholder
|
||||
@@ -43,7 +40,7 @@ def build_version_payload() -> dict:
|
||||
payload: dict = {"version": os.environ.get("APP_VERSION", "dev")}
|
||||
|
||||
# Reported verbatim, never validated against an enum — a build claiming
|
||||
# something unexpected is better shown than dropped (rule 149).
|
||||
# something unexpected is better shown than dropped.
|
||||
for key, env in (("channel", "APP_CHANNEL"), ("commit", "APP_COMMIT")):
|
||||
value = (os.environ.get(env) or "").strip()
|
||||
if value:
|
||||
|
||||
@@ -33,10 +33,10 @@ async def list_projects_route():
|
||||
projects = await list_projects_for_user(uid, status=status)
|
||||
if include_summary:
|
||||
# Batched: four queries plus two, in two sessions, for ALL projects.
|
||||
# This replaced an asyncio.gather over a per-project summary that opened
|
||||
# its own session and then one more per milestone — ~250 concurrent
|
||||
# checkouts against a pool of 15, all waiting out the 30s timeout and
|
||||
# starving every other route on the instance (#2384).
|
||||
# An asyncio.gather over a per-project summary would open a session per
|
||||
# project and one more per milestone — far more concurrent checkouts
|
||||
# than the pool holds, all waiting out the checkout timeout and
|
||||
# starving every other route on the instance.
|
||||
#
|
||||
# Grouped by OWNER because a shared project's counts belong to its
|
||||
# owner's records, matching what the per-project path passed.
|
||||
@@ -67,7 +67,7 @@ async def create_project_route():
|
||||
status = data.get("status", "active")
|
||||
if status not in ("active", "paused", "completed", "archived"):
|
||||
return jsonify({"error": "status must be 'active', 'paused', 'completed', or 'archived'"}), 400
|
||||
# The inception decision rides the create (milestone 297): the UI's
|
||||
# The inception decision rides the create: the UI's
|
||||
# second step sends `inception: {choices}`; absent = undecided, and the
|
||||
# project page shows the card until it is. Validated before the create
|
||||
# so a bad decision never leaves a half-made project behind.
|
||||
@@ -98,7 +98,7 @@ async def create_project_route():
|
||||
@projects_bp.route("/<int:project_id>/inception", methods=["POST"])
|
||||
@login_required
|
||||
async def decide_inception_route(project_id: int):
|
||||
"""Record (or re-record) what a project inherits — milestone 297.
|
||||
"""Record (or re-record) what a project inherits.
|
||||
Body: the choices object {design_system_id, seed_systems}; owner-only."""
|
||||
uid = get_current_user_id()
|
||||
data = await request.get_json() or {}
|
||||
@@ -185,7 +185,7 @@ async def get_coverage_route(project_id: int):
|
||||
project, _ = result
|
||||
owner_uid = project.user_id or uid
|
||||
return jsonify({
|
||||
# The OWNER's keyring (#2778) — whether refresh could do anything,
|
||||
# The OWNER's keyring — whether refresh could do anything,
|
||||
# regardless of who is looking.
|
||||
"configured": (await get_forges(owner_uid, project_id)).configured,
|
||||
"coverage": await cached_coverage(owner_uid, project_id),
|
||||
@@ -230,7 +230,7 @@ async def refresh_coverage_route(project_id: int):
|
||||
@projects_bp.route("/<int:project_id>/forge", methods=["PUT"])
|
||||
@login_required
|
||||
async def set_project_forge_route(project_id: int):
|
||||
"""Pin the project to one forge connection, or clear the pin (#2778).
|
||||
"""Pin the project to one forge connection, or clear the pin.
|
||||
|
||||
Body: {"connection_id": <id> | null}. Owner-or-admin may ask; either way
|
||||
the pin can only reference a connection the project OWNER holds — the
|
||||
|
||||
@@ -24,7 +24,7 @@ settings_bp = Blueprint("settings", __name__, url_prefix="/api/settings")
|
||||
# read and skip the mask on write; this generic KV surface has to apply the
|
||||
# same treatment, or it silently un-masks what those endpoints masked — the
|
||||
# rows live on the admin's own user_id, so the plain GET returned them raw.
|
||||
# (forge_token left with 0078: forge credentials are keyring rows now, #2778.)
|
||||
# (No forge_token here: forge credentials are keyring rows, since migration 0078.)
|
||||
_SECRET_KEYS = frozenset({"smtp_password", "forge_webhook_secret"})
|
||||
|
||||
# What the tuning history records for a dial changed through this form. The MCP
|
||||
@@ -62,10 +62,10 @@ async def update_settings_route():
|
||||
for k, v in data.items():
|
||||
str_v = str(v)
|
||||
# A retrieval dial is never written as a plain settings row, wherever
|
||||
# the request came from (#4102). The value would land correctly and the
|
||||
# tuning history would say nothing happened — and since milestone 416
|
||||
# those dials are moved by the model on the operator's behalf, a
|
||||
# history with holes in it is worse than none: it reads as complete.
|
||||
# the request came from. The value would land correctly and the tuning
|
||||
# history would say nothing happened — and since those dials are moved
|
||||
# by the model on the operator's behalf, a history with holes in it is
|
||||
# worse than none: it reads as complete.
|
||||
#
|
||||
# Routed rather than refused on purpose. Refusing would work only for
|
||||
# callers that had been updated; this way every caller that ever writes
|
||||
@@ -113,7 +113,7 @@ async def test_search():
|
||||
return jsonify({"configured": True, "results": [], "searxng_url": Config.SEARXNG_URL})
|
||||
|
||||
|
||||
# --- forge connections (#2778) ------------------------------------------------
|
||||
# --- forge connections ---------------------------------------------------------
|
||||
# The user's keyring: read-only forge credentials, one per host, resolved by
|
||||
# repo host for every server-side forge read on the user's projects. Strictly
|
||||
# own-rows — a connection is a credential, and there is no admin view of
|
||||
@@ -201,7 +201,7 @@ async def delete_forge_connection_route(connection_id: int):
|
||||
async def test_forge_connection_route(connection_id: int):
|
||||
"""Probe the SAVED connection: reachability and token acceptance in one
|
||||
press, so a misconfiguration is visible now rather than as silent
|
||||
fallbacks later (#2663's lesson, applied per keyring row)."""
|
||||
fallbacks later, per keyring row."""
|
||||
from scribe.services.forge import ForgeError, build_adapter
|
||||
from scribe.services.forge_connections import connection_token, get_connection
|
||||
|
||||
|
||||
@@ -449,7 +449,7 @@ async def _open_ledger(session, user_id: int, note_id: int) -> int:
|
||||
)).scalars().all())
|
||||
opened = 0
|
||||
for pid in sorted(project_ids - answered):
|
||||
# Rule 78: a promotion reaches only the projects its promoter could
|
||||
# A promotion reaches only the projects its promoter could
|
||||
# have written an answer into themselves.
|
||||
if await access.can_write_project(user_id, pid):
|
||||
session.add(FamilyAdoption(project_id=pid, idea_id=note_id, status="unassessed"))
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
WHY THIS EXISTS
|
||||
|
||||
A reply is where the person the work is for finds out what happened, and they
|
||||
were not there while it was done. Milestone 409 wrote the shapes that make a
|
||||
reply readable to them — conclusion first, the shortest reply that carries the
|
||||
answer, where the work stands — and put them in one bundled skill. A skill
|
||||
arrives only when the agent decides to load it, so in a long session the shape
|
||||
faded out of context exactly when replies were getting longer.
|
||||
were not there while it was done. The shapes make a reply readable to them —
|
||||
conclusion first, the shortest reply that carries the answer, where the work
|
||||
stands. A skill arrives only when the agent decides to load it, so in a long
|
||||
session a shape held in a skill fades out of context exactly when replies get
|
||||
longer.
|
||||
|
||||
So the shapes are product content held here, on the server, and delivered by
|
||||
the moments they belong to rather than by the agent remembering to ask:
|
||||
|
||||
@@ -82,7 +82,7 @@ async def set_supersedes(
|
||||
silent drop, because it is the one case where the caller might reasonably
|
||||
believe they succeeded and be wrong in a way that matters — demoting someone
|
||||
else's record out of their retrieval is damage you cannot see from the
|
||||
outside. Rule #47.
|
||||
outside.
|
||||
"""
|
||||
if not await access.can_write_note(user_id, note_id):
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user