Compare commits
5
Commits
fa4f88747e
...
0c237172e4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c237172e4 | ||
|
|
cad74df190 | ||
|
|
a01f5492bc | ||
|
|
edf725687a | ||
|
|
87e8f8f777 |
+20
-23
@@ -187,12 +187,12 @@ def create_app() -> Quart:
|
||||
start_notification_loop()
|
||||
start_auth_token_retention_loop()
|
||||
|
||||
# Write down any shipped default that moved in this release (#4225).
|
||||
# Write down any shipped default that moved in this release.
|
||||
#
|
||||
# INLINE AND AWAITED, deliberately, against the instinct the block
|
||||
# below argues for. What #4181 cost three hours was CONCURRENCY — a
|
||||
# background task created here racing this hook for the same pool.
|
||||
# Running sequentially creates no such contention, and this is twelve
|
||||
# below argues for. The startup hazard is CONCURRENCY — a background
|
||||
# task created here racing this hook for the same pool. Running
|
||||
# sequentially creates no such contention, and this is a dozen
|
||||
# single-row reads on an index against a table with a handful of rows.
|
||||
#
|
||||
# It has to finish before serving because the thing it protects is a
|
||||
@@ -216,16 +216,14 @@ def create_app() -> Quart:
|
||||
|
||||
# Backfill embeddings for any notes that don't have one. Runs in the
|
||||
# background so it never blocks the server from accepting requests —
|
||||
# and, since #4181, not until the rest of this hook has finished.
|
||||
# and not until the rest of this hook has finished.
|
||||
#
|
||||
# "Background" was true of REQUESTS and false of STARTUP. A task
|
||||
# "Background" is true of REQUESTS and false of STARTUP. A task
|
||||
# created here begins immediately, while `before_serving` is still
|
||||
# running, so its four passes competed with the startup hook's own
|
||||
# running, so its passes would compete with the startup hook's own
|
||||
# database reads for the same connection pool. That is survivable on a
|
||||
# healthy disk and fatal on a sick one: on 2026-09-19 both this
|
||||
# backfill's first query and the hook's maintenance-hour read were
|
||||
# cancelled together when Hypercorn killed the worker at its lifespan
|
||||
# timeout, and the instance stayed down for three hours.
|
||||
# healthy disk and fatal on a sick one: Hypercorn kills a worker whose
|
||||
# lifespan hook overruns, and nothing retries it.
|
||||
#
|
||||
# The wait is on the SERVING FLAG rather than a sleep, because a sleep
|
||||
# would be a guess about how long the rest of the hook takes and would
|
||||
@@ -236,21 +234,20 @@ def create_app() -> Quart:
|
||||
await backfill_note_embeddings()
|
||||
except Exception:
|
||||
logger.warning("Embedding backfill failed", exc_info=True)
|
||||
# Rules got vectors in milestone 307; every rule written before it
|
||||
# has none, so this is the pass that makes them findable at all.
|
||||
# A rule with no vector is unfindable by meaning; this pass fills
|
||||
# any that are missing.
|
||||
try:
|
||||
await backfill_rule_embeddings()
|
||||
except Exception:
|
||||
logger.warning("Rule embedding backfill failed", exc_info=True)
|
||||
# Milestones got vectors in milestone 415, so a plan written before
|
||||
# it is findable only after this pass.
|
||||
# Likewise a milestone: a plan with no vector is findable only
|
||||
# after this pass.
|
||||
try:
|
||||
await backfill_milestone_embeddings()
|
||||
except Exception:
|
||||
logger.warning("Milestone embedding backfill failed", exc_info=True)
|
||||
# Systems got vectors in #4251, so before this pass every charter
|
||||
# ever written is unfindable — a System could narrow a search and
|
||||
# never be the answer to one.
|
||||
# A System charter with no vector is unfindable — the System could
|
||||
# narrow a search and never be the answer to one.
|
||||
try:
|
||||
await backfill_system_embeddings()
|
||||
except Exception:
|
||||
@@ -270,7 +267,7 @@ def create_app() -> Quart:
|
||||
# work overlaps startup's own.
|
||||
asyncio.create_task(_delayed_backfill())
|
||||
|
||||
# RELEASED IN A `finally`, never after the work (rules 156 and 157).
|
||||
# RELEASED IN A `finally`, never after the work.
|
||||
# The waiter above has no deadline of its own, and the way to make an
|
||||
# undeadlined wait safe is to make the release unmissable: if anything
|
||||
# below raises, the flag is still set and the task ends instead of
|
||||
@@ -292,7 +289,7 @@ def create_app() -> Quart:
|
||||
|
||||
# DB maintenance scheduler (daily targeted VACUUM ANALYZE, default
|
||||
# 04:00 UTC). `get_maintenance_hour` is the FIRST database read in
|
||||
# this hook and is bounded for that reason (#4181) — see the long
|
||||
# this hook and is bounded for that reason — see the long
|
||||
# comment above `_STARTUP_READ_TIMEOUT`. Anything added here that
|
||||
# touches the database needs the same treatment: a lifespan hook
|
||||
# that does not return is a worker that never serves.
|
||||
@@ -304,8 +301,8 @@ def create_app() -> Quart:
|
||||
asyncio.get_running_loop(), await get_maintenance_hour()
|
||||
)
|
||||
|
||||
# Scheduled backups (milestone 523). An hourly tick that reads its
|
||||
# config when it fires, so starting it reads nothing here (#4181).
|
||||
# Scheduled backups. An hourly tick that reads its config when it
|
||||
# fires, so starting it reads nothing here.
|
||||
from scribe.services.backup_job import start_backup_scheduler
|
||||
start_backup_scheduler(asyncio.get_running_loop())
|
||||
|
||||
@@ -315,7 +312,7 @@ def create_app() -> Quart:
|
||||
from scribe.services.diagnostics import start_diagnostics
|
||||
start_diagnostics(asyncio.get_running_loop())
|
||||
finally:
|
||||
# STARTUP IS OVER — release the backfill (#4181). Everything above
|
||||
# STARTUP IS OVER — release the backfill. Everything above
|
||||
# is work the server needs done before it serves; everything the
|
||||
# backfill does is work that can wait for a server already up.
|
||||
logger.info("Startup complete; releasing deferred backfill")
|
||||
|
||||
@@ -15,9 +15,9 @@ from sqlalchemy.orm import Mapped, mapped_column
|
||||
from scribe.models import Base
|
||||
from scribe.models.base import TimestampMixin, iso
|
||||
|
||||
# The classification vocabulary (note 2786). `unclassified` is the default and
|
||||
# The classification vocabulary. `unclassified` is the default and
|
||||
# THE todo state; every other status is a judgment, stamped with who made it —
|
||||
# except `scoped` (#2869): the coverage sync's mechanical stamp on shapes that
|
||||
# except `scoped`: the coverage sync's mechanical stamp on shapes that
|
||||
# are one-offs BY CONSTRUCTION (a Vue component's scoped <style> rules and its
|
||||
# <script setup> functions — unreachable from any other file). Scoped rows are
|
||||
# accounted for without a human judging them, so `exempt` keeps meaning "a
|
||||
@@ -26,11 +26,11 @@ from scribe.models.base import TimestampMixin, iso
|
||||
SHAPE_STATUSES = ("canonical", "instance", "variant", "exempt", "scoped", "unclassified")
|
||||
SHAPE_CLASSIFIERS = ("agent", "audit", "hook", "mechanical", "import")
|
||||
|
||||
# The reason catalogue (#2874): an OPTIONAL code beside the prose reason on
|
||||
# The reason catalogue: an OPTIONAL code beside the prose reason on
|
||||
# variant/exempt rows, so the ledger can be filtered and aggregated by kind
|
||||
# of one-off. The prose remains the record; the code is the index.
|
||||
REASON_CODES = (
|
||||
"scoped-css", # a scoped rule styling one element (pre-#2869 rows)
|
||||
"scoped-css", # a scoped rule styling one element (rows judged before `scoped`)
|
||||
"one-off-handler", # a view/component handler or loader, one per surface
|
||||
"test-helper", # a test module's stub, driver or fixture data
|
||||
"convention-plumbing", # registration, wiring, app factory — one of each
|
||||
@@ -40,17 +40,16 @@ REASON_CODES = (
|
||||
"typed-record", # a NamedTuple / dataclass / error class — one each
|
||||
)
|
||||
|
||||
# How the mechanical proposer (#2792) arrived at a proposal, strongest first.
|
||||
# How the mechanical proposer arrived at a proposal, strongest first.
|
||||
# `derive` is the odd one out: not "this is an instance of #N" but "this
|
||||
# shape repeats with NO canon — derive one first" (note 2786's derive-first
|
||||
# rule), so it carries a group key instead of a snippet.
|
||||
# shape repeats with NO canon — derive one first", so it carries a group key instead of a snippet.
|
||||
PROPOSAL_BASES = ("symbol", "text", "reference", "signature", "semantic", "derive")
|
||||
|
||||
|
||||
class CodeShape(Base, TimestampMixin):
|
||||
"""One extracted code shape and its classification against canon (#2787).
|
||||
"""One extracted code shape and its classification against canon.
|
||||
|
||||
The accounting half of the pattern system (governing note 2786): the
|
||||
The accounting half of the pattern system: the
|
||||
snippet library records CANON (small); this ledger accounts for EVERY
|
||||
shape the coverage extractor finds in a bound repo (total). A row's
|
||||
status says how the shape relates to canon — it IS a snippet's reference
|
||||
@@ -62,15 +61,15 @@ class CodeShape(Base, TimestampMixin):
|
||||
|
||||
Identity is (project, repo_key, path, symbol, kind) — kind is part of it
|
||||
because one file can define `.foo` (css) and `foo` (sym) as distinct
|
||||
shapes. A rename therefore reads as vanish + new row: accepted for v1,
|
||||
shapes. A rename therefore reads as vanish + new row: accepted,
|
||||
because chasing renames needs content identity the extractor doesn't
|
||||
have. `vanished_at` keeps the history instead of deleting it.
|
||||
|
||||
snippet_id is SET NULL on snippet deletion: the classification's target
|
||||
is gone but the judgment happened; the sync pass (step 2) re-files such
|
||||
is gone but the judgment happened; the sync pass re-files such
|
||||
rows as unclassified so they rejoin the todo instead of dangling.
|
||||
|
||||
`signature` / `body_sha` (#2792) are the shape's content fingerprint —
|
||||
`signature` / `body_sha` are the shape's content fingerprint —
|
||||
its definition line and a whitespace/comment-insensitive hash of its
|
||||
block — refreshed by every sync. They are what the mechanical proposer
|
||||
matches on and what a later drift recheck compares against; the ledger
|
||||
@@ -80,7 +79,7 @@ class CodeShape(Base, TimestampMixin):
|
||||
when a later sync sees the body change under an instance/variant, the
|
||||
row is flagged `recheck_at` (the judgment stands, it just asks to be
|
||||
confirmed again) and a `drifted` event is written. `diverges_from`
|
||||
(#2793) is the button-B flag: a shape new since the previous refresh, in
|
||||
is the button-B flag: a shape new since the previous refresh, in
|
||||
a directory+kind where one canon dominates the judged siblings, that the
|
||||
proposer did not match to that canon — "button B appeared where button
|
||||
A is canon: divergence or variant? classify it." Both clear on judgment.
|
||||
@@ -119,7 +118,7 @@ class CodeShape(Base, TimestampMixin):
|
||||
BigInteger, ForeignKey("notes.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
reason: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
reason_code: Mapped[str | None] = mapped_column(Text, nullable=True) # REASON_CODES (#2874)
|
||||
reason_code: Mapped[str | None] = mapped_column(Text, nullable=True) # REASON_CODES
|
||||
classified_by: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
classified_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
@@ -190,7 +189,7 @@ class CodeShape(Base, TimestampMixin):
|
||||
}
|
||||
|
||||
def to_compact(self) -> dict:
|
||||
"""The row as an audit reads it (#2868): identity, standing, the
|
||||
"""The row as an audit reads it: identity, standing, the
|
||||
definition line and the proposer's word — none of the bookkeeping
|
||||
(commits, shas, timestamps). A 500-row page of these fits the tool
|
||||
budget; a page of to_dict() does not."""
|
||||
@@ -217,16 +216,16 @@ class CodeShape(Base, TimestampMixin):
|
||||
return out
|
||||
|
||||
|
||||
# How a uses edge was established (#2870): who/what said "this shape calls
|
||||
# How a uses edge was established: who/what said "this shape calls
|
||||
# that canon". `reference` is the proposer's mechanical by-name hit on the
|
||||
# body (language-gated, #2871); `hook` is write-path evidence (pulled the
|
||||
# body (language-gated); `hook` is write-path evidence (pulled the
|
||||
# snippet, then wrote code naming its symbol); agent/audit/import are
|
||||
# judgments carried on classify_shapes(..., uses=[...]).
|
||||
USE_BASES = ("reference", "hook", "agent", "audit", "import")
|
||||
|
||||
|
||||
class CodeShapeUse(Base):
|
||||
"""One consumption edge: shape → canonical snippet it calls/uses (#2870).
|
||||
"""One consumption edge: shape → canonical snippet it calls/uses.
|
||||
|
||||
Conformance (CodeShape.status/snippet_id) answers "what shape is this";
|
||||
this table answers "what does it use" — many per shape. A service function
|
||||
@@ -265,7 +264,7 @@ class CodeShapeUse(Base):
|
||||
}
|
||||
|
||||
|
||||
# How a consumer edge was established (milestone 302). `template` is the
|
||||
# How a consumer edge was established. `template` is the
|
||||
# sync's mechanical read of a file's markup (class= / :class= / className=);
|
||||
# the vocabulary is a list so a later basis (a stylesheet `@apply`, a script's
|
||||
# classList) has a name without a schema change.
|
||||
@@ -274,8 +273,8 @@ CONSUMER_BASES = ("template",)
|
||||
|
||||
class CodeShapeConsumer(Base):
|
||||
"""One consumer edge: CSS shape → the file whose markup names its class
|
||||
(milestone 302; note 2917 — CSS is watched by name, by recipe, by token
|
||||
and by WHAT USES IT). The analogue of CodeShapeUse for styling: `uses`
|
||||
(CSS is watched by name, by recipe, by token and by WHAT USES IT). The
|
||||
analogue of CodeShapeUse for styling: `uses`
|
||||
says what a shape calls, this says who renders a class. Rows, not prose,
|
||||
so "is this recipe shared or scoped?" is a count, not a guess.
|
||||
|
||||
@@ -311,7 +310,7 @@ class CodeShapeConsumer(Base):
|
||||
}
|
||||
|
||||
|
||||
# What a shape's history records (#2793). Not "appeared" — first_seen and
|
||||
# What a shape's history records. Not "appeared" — first_seen and
|
||||
# created_at already say that on the row; history is for what CHANGED:
|
||||
SHAPE_EVENTS = ("classified", "vanished", "reappeared", "drifted")
|
||||
|
||||
|
||||
@@ -92,15 +92,13 @@ class Rule(Base, TimestampMixin, SoftDeleteMixin):
|
||||
# WHEN this rule applies — the trigger, not the instruction. Required of
|
||||
# new rules at the service layer and nullable here, because rules written
|
||||
# before migration 0088 have none and a migration cannot invent one.
|
||||
# It carries three jobs at once (note 3026): it is the readable form of
|
||||
# the canon tag, the half of the document that makes a rule findable by
|
||||
# meaning, and — since milestone 394 removed the always-on tier — the ONLY
|
||||
# thing that decides whether a rule ever reaches a session at all. A rule
|
||||
# It carries three jobs at once: it is the readable form of the canon tag,
|
||||
# the half of the document that makes a rule findable by meaning, and —
|
||||
# with no always-on tier — the ONLY thing that decides whether a rule ever reaches a session at all. A rule
|
||||
# with no trigger is not a quiet rule, it is an unreachable one.
|
||||
when_to_apply: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
# WHAT KIND of instruction this is. `tier` used to sit beside this and
|
||||
# carry delivery; milestone 394 removed it, so kind is now the only axis
|
||||
# on a rule and delivery belongs entirely to retrieval.
|
||||
# WHAT KIND of instruction this is — the only axis on a rule; delivery
|
||||
# belongs entirely to retrieval.
|
||||
# `rule` must be FOLLOWED: ignoring it breaks something or crosses a
|
||||
# boundary. `preference` is how this person wants work DONE: ignoring it
|
||||
# costs consistency, not correctness.
|
||||
@@ -116,7 +114,7 @@ class Rule(Base, TimestampMixin, SoftDeleteMixin):
|
||||
# trigger-dominated document, ownership-scoped search, the retrieval arms,
|
||||
# relations, and `rule_versions`, which is where its drift is recorded.
|
||||
# Defaults to `rule` so nothing changes force on upgrade.
|
||||
# CHECK ck_rules_kind (migration 0098, rule 36).
|
||||
# CHECK ck_rules_kind (migration 0098).
|
||||
kind: Mapped[str] = mapped_column(Text, default="rule", server_default="rule")
|
||||
why: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
how_to_apply: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
@@ -141,7 +139,7 @@ class Rule(Base, TimestampMixin, SoftDeleteMixin):
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
# The record that caused this rule — the edge notes and tasks already
|
||||
# have. Rule 46's `why` names note 2813 in prose; this is that link as a
|
||||
# have. A `why` can name its origin in prose; this is that link as a
|
||||
# field, so it survives a rewording of the paragraph.
|
||||
arose_from_id: Mapped[int | None] = mapped_column(
|
||||
BigInteger, ForeignKey("notes.id", ondelete="SET NULL"), nullable=True
|
||||
@@ -175,10 +173,9 @@ class Rule(Base, TimestampMixin, SoftDeleteMixin):
|
||||
}
|
||||
|
||||
|
||||
# Which global AREA a rule is about (milestone 307). Points at the canonical
|
||||
# catalog, NEVER at a project's `systems` row: a rule that spans projects
|
||||
# cannot be chained to one project's vocabulary. This is the edge three
|
||||
# projects were drawing by hand, as rule text copied into a System's charter.
|
||||
# Which global AREA a rule is about. Points at the canonical catalog, NEVER at
|
||||
# a project's `systems` row: a rule that spans projects cannot be chained to
|
||||
# one project's vocabulary.
|
||||
rule_systems = Table(
|
||||
"rule_systems",
|
||||
Base.metadata,
|
||||
@@ -188,7 +185,7 @@ rule_systems = Table(
|
||||
)
|
||||
|
||||
|
||||
# Which MOMENTS of work a rule arrives at (milestone 458). A rule mounted on a
|
||||
# Which MOMENTS of work a rule arrives at. A rule mounted on a
|
||||
# moment is delivered whenever that moment happens, by lookup and with no
|
||||
# score — the route for a rule that is about WHEN something is done rather
|
||||
# than what it is about, which semantic retrieval structurally misses. The
|
||||
@@ -204,17 +201,15 @@ rule_moments = Table(
|
||||
)
|
||||
|
||||
class RuleRelation(Base, CreatedAtMixin):
|
||||
"""A typed edge between two rules. Each kind exists because its ABSENCE
|
||||
forced a workaround somewhere in the operator's rulebook (note 3026).
|
||||
"""A typed edge between two rules. Each kind exists because without it a
|
||||
rulebook needs a workaround.
|
||||
|
||||
- ``co_surfaces`` — these fail together, so they must arrive together.
|
||||
Without it, the only way to guarantee that was to merge them into one
|
||||
row, which is what happened to rule 46: split into 144, folded back the
|
||||
same day because "either rule could surface without the other."
|
||||
Without it, the only way to guarantee that is to merge them into one
|
||||
row, because either rule could surface without the other.
|
||||
Symmetric in meaning; stored once and read both ways.
|
||||
- ``overrides`` — this rule supersedes that one for its scope. Only
|
||||
*suppression* existed, so an override had to be written as a parallel
|
||||
rule that then drifts from its parent.
|
||||
- ``overrides`` — this rule supersedes that one for its scope. Without
|
||||
it, an override is a parallel rule that drifts from its parent.
|
||||
- ``elaborates`` — this rule adds local specifics to that one; surfacing
|
||||
the parent brings the addendum with it.
|
||||
|
||||
@@ -232,7 +227,7 @@ class RuleRelation(Base, CreatedAtMixin):
|
||||
to_rule_id: Mapped[int] = mapped_column(
|
||||
BigInteger, ForeignKey("rules.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
# CHECK ck_rule_relations_kind (migration 0088, rule 36).
|
||||
# CHECK ck_rule_relations_kind (migration 0088).
|
||||
kind: Mapped[str] = mapped_column(Text)
|
||||
note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
@@ -251,10 +246,8 @@ class RuleRelation(Base, CreatedAtMixin):
|
||||
}
|
||||
|
||||
|
||||
# `project_rulebook_subscriptions`, `project_rule_suppressions` and
|
||||
# `project_topic_suppressions` lived here until milestone 414 (migration 0101).
|
||||
# A rule's home is its scope now: a rule in a rulebook topic is global, a rule
|
||||
# on a project applies to that project, and retrieval reads that directly. A
|
||||
# subscription had stopped changing anything a session received, and a
|
||||
# suppression muted rules from a subscription. A project that departs from a
|
||||
# global rule writes a project rule with an `overrides` relation instead.
|
||||
# A rule's home is its scope: a rule in a rulebook topic is global, a rule on
|
||||
# a project applies to that project, and retrieval reads that directly. There
|
||||
# are no per-project subscriptions or suppressions (migration 0101 dropped
|
||||
# those tables); a project that departs from a global rule writes a project
|
||||
# rule with an `overrides` relation.
|
||||
|
||||
+33
-35
@@ -72,14 +72,14 @@ async def session_context():
|
||||
unbound repo yields a "bind this repo" hint instead. This is how the
|
||||
active project is determined — the plugin never pins a project id.
|
||||
project_id (optional int) — the project named directly, and the only
|
||||
key a caller outside a git repo has (#4085): the plugin's hooks
|
||||
key a caller outside a git repo has: the plugin's hooks
|
||||
send it when a `.scribe` marker file names a project. Takes
|
||||
precedence over `repo`. Access-checked like any other read — an id
|
||||
this account cannot read loads no project rather than failing.
|
||||
source (optional str) — the host's SessionStart source (startup,
|
||||
resume, compact, clear, fork); decides what the claim section says.
|
||||
session_id (optional str) — the session's id, so a claim bound to it
|
||||
reads as this session's own (milestone 381).
|
||||
reads as this session's own.
|
||||
"""
|
||||
project_id, _repo, unbound_repo = await _project_scope()
|
||||
result = await plugin_ctx_svc.build_session_context(
|
||||
@@ -109,7 +109,7 @@ async def autoinject_retrieve():
|
||||
project_id (opt) — explicit project scope override (ad-hoc/testing).
|
||||
exclude_ids (opt) — comma-separated note ids already injected this
|
||||
session; skipped so each note injects at most once.
|
||||
ctx (opt) — the tail of the last assistant reply (#4364). The
|
||||
ctx (opt) — the tail of the last assistant reply. The
|
||||
notes AND rule arms append it to a short prompt,
|
||||
so a follow-up like "yes do that" still names
|
||||
what it is about — and a rule or lesson arrives
|
||||
@@ -119,22 +119,22 @@ async def autoinject_retrieve():
|
||||
(opt) session. SHARED with /prior-art and /tool-rules on
|
||||
purpose: one session keeps ONE rule ledger, so a
|
||||
rule named by any arm is not re-announced by
|
||||
another. Ages out (#3751), so salience decays.
|
||||
another. Ages out, so salience decays.
|
||||
held_rule_ids — comma-separated rule ids the session actually
|
||||
(opt) OPENED, observed from the `get_rule` call itself
|
||||
rather than claimed. A rule on exclude_rule_ids
|
||||
was NAMED; a rule here was READ, and the two say
|
||||
different things about what the reader holds —
|
||||
so they get different lines (#4100). Shares the
|
||||
so they get different lines. Shares the
|
||||
ledger directory and the same clear-on-compact.
|
||||
shapes_seen (opt) — comma-separated reply-shape keys this session was
|
||||
already shown in full (milestone 500). The core
|
||||
already shown in full. The core
|
||||
goes out in full when `core` is absent, as its
|
||||
one-line reminder when present; the hook's
|
||||
ledger is swept on compaction, which is how the
|
||||
full core comes back after one.
|
||||
|
||||
THE TURN'S REPLY SHAPE COMES FIRST (milestone 500 step 3). Every turn ends
|
||||
THE TURN'S REPLY SHAPE COMES FIRST. Every turn ends
|
||||
in a reply and this is the last point before it is written, so the core
|
||||
shape and whatever is mounted on `reply.report` lead the payload. Fresh
|
||||
shape keys come back as `shape_keys` for the ledger.
|
||||
@@ -165,7 +165,7 @@ async def autoinject_retrieve():
|
||||
g.user.id, q, project_id=project_id, exclude_ids=exclude_ids, context=ctx,
|
||||
)
|
||||
# The one place both arms' lines meet, so the one place a lesson and a
|
||||
# rule arriving TOGETHER can be counted (#4637). Keyed on the operator's
|
||||
# rule arriving TOGETHER can be counted. Keyed on the operator's
|
||||
# prompt — the situation — and fails open to "".
|
||||
proposal = await lesson_rules_svc.co_surfaced(
|
||||
g.user.id, result.get("lesson_ids") or [], rules.get("shown_rule_ids") or [],
|
||||
@@ -191,13 +191,13 @@ async def autoinject_retrieve():
|
||||
@login_required
|
||||
@memoized_query_embeddings
|
||||
async def pre_tool_rules():
|
||||
"""Standing rules for the plugin's PreToolUse hook on ACTIONS (#3476).
|
||||
"""Standing rules for the plugin's PreToolUse hook on ACTIONS.
|
||||
|
||||
Answers "does a recorded rule speak to the command about to be run?" — the
|
||||
sibling of /prior-art, which can only answer that question about a code
|
||||
write. Rules about which tool to reach for (don't curl the forge, don't
|
||||
stand up a stack, don't run the suite locally) had no retrieval surface at
|
||||
all before this, which is why they all had to live in the resident preload.
|
||||
stand up a stack, don't run the suite locally) have no other retrieval
|
||||
surface: no code write ever happens for them to ride on.
|
||||
|
||||
Titles + trigger only, never the statement: the hint says a rule may apply
|
||||
and hands over `get_rule(id)`. One rule at most (RULEHINT_LIMIT), and empty
|
||||
@@ -219,18 +219,17 @@ async def pre_tool_rules():
|
||||
held_rule_ids (opt) — rule ids the session actually OPENED, as
|
||||
against merely named. Named and read are
|
||||
different claims about the reader's
|
||||
context, so they get different lines
|
||||
(#4100).
|
||||
context, so they get different lines.
|
||||
root, cwd (opt) — the repo's absolute root and the command's
|
||||
working directory. They turn the paths the
|
||||
command names into repo-relative ones, for
|
||||
the rulings arm (milestone 444).
|
||||
the rulings arm.
|
||||
seen_ruling_systems (opt) — comma-separated System ids whose rulings
|
||||
were already shown this session, by either
|
||||
arm; those get a one-line reference. SHARED
|
||||
with /prior-art, like exclude_rule_ids.
|
||||
|
||||
Returns `context`, `rule_ids`, and `checkpoint` (#4214, milestone 419),
|
||||
Returns `context`, `rule_ids`, and `checkpoint`,
|
||||
plus `ruling_system_ids` — the Systems whose rulings were shown in full on
|
||||
this call — when there were any.
|
||||
|
||||
@@ -268,7 +267,7 @@ async def pre_tool_rules():
|
||||
@plugin_bp.get("/moment-tools")
|
||||
@login_required
|
||||
async def moment_tools():
|
||||
"""The tools whose calls can deliver a mounted rule (milestone 458 step 4).
|
||||
"""The tools whose calls can deliver a mounted rule.
|
||||
|
||||
Read by the plugin's catch-all PreToolUse hook once per session window and
|
||||
kept on disk, so a call that cannot reach a mounted rule costs no request.
|
||||
@@ -282,7 +281,7 @@ async def moment_tools():
|
||||
@plugin_bp.post("/moment")
|
||||
@login_required
|
||||
async def moment():
|
||||
"""The rules mounted on the moments this tool call reaches (milestone 458).
|
||||
"""The rules mounted on the moments this tool call reaches.
|
||||
|
||||
Body: the PreToolUse event as Claude Code delivers it — `tool_name` and
|
||||
`tool_input` are read, nothing else. Sent whole because which input field
|
||||
@@ -298,7 +297,7 @@ async def moment():
|
||||
`moments` (the names reached). A lookup, not a ranked search: no
|
||||
retrieval_logs row; each fresh rule is recorded surfaced with its moment.
|
||||
|
||||
A moment that carries a default reply shape (milestone 500) brings it too,
|
||||
A moment that carries a default reply shape brings it too,
|
||||
ahead of the rules: in full unless `shapes_seen` names it, then as its
|
||||
reminder. `shape_keys` are the ones sent in full, for the hook's ledger.
|
||||
"""
|
||||
@@ -330,7 +329,7 @@ async def moment():
|
||||
@plugin_bp.post("/rule-opened")
|
||||
@login_required
|
||||
async def rule_opened():
|
||||
"""A session opened a rule shortly after these acts (milestone 458 step 7).
|
||||
"""A session opened a rule shortly after these acts.
|
||||
|
||||
Body: `{"rule_id", "session_id", "acts": [<PreToolUse event>, ...]}` —
|
||||
the calls the plugin saw in the window before the `get_rule`. Each is
|
||||
@@ -360,16 +359,15 @@ async def rule_opened():
|
||||
@login_required
|
||||
@memoized_query_embeddings
|
||||
async def reply_rules():
|
||||
"""Whether the reply that ends a turn is held for one read (milestone 458).
|
||||
"""Whether the reply that ends a turn is held for one read.
|
||||
|
||||
Called by the plugin's Stop hook with the finished reply. Two halves: the
|
||||
rules MOUNTED on the reply moments (`reply.report`, and `reply.ask` when
|
||||
the reply asks something), and the reply text against every rule's
|
||||
trigger — the backstop for whatever the earlier arms missed.
|
||||
|
||||
ONE END-OF-TURN REQUEST (milestone 500 step 4): a reply that closed tasks
|
||||
is also checked here for the completion sections (services/report_check),
|
||||
which used to be a Stop hook and an endpoint of its own. Both holds fold
|
||||
ONE END-OF-TURN REQUEST: a reply that closed tasks is also checked here
|
||||
for the completion sections (services/report_check). Both holds fold
|
||||
into one `reason`; `report_check` says what the section check recorded, so
|
||||
the hook knows the rewrite is one to report.
|
||||
|
||||
@@ -447,7 +445,7 @@ async def write_path_prior_art():
|
||||
exclude_ids (opt) — comma-separated REUSE-class ids already surfaced
|
||||
this session.
|
||||
exclude_sync_ids (opt) — comma-separated SYNC-class ids (snippets
|
||||
recorded AT the edited file, #2708) already
|
||||
recorded AT the edited file) already
|
||||
surfaced. A separate channel on purpose: a reuse
|
||||
hint shown early must not suppress the record-sync
|
||||
nudge when the recorded file is edited later.
|
||||
@@ -458,31 +456,31 @@ async def write_path_prior_art():
|
||||
every subsequent write.
|
||||
held_rule_ids (opt) — RULE ids the session actually OPENED, as
|
||||
against merely named; drives the third
|
||||
reference wording (#4100).
|
||||
reference wording.
|
||||
exclude_derive (opt) — comma-separated derive keys (a derive group id
|
||||
or `canon:<snippet_id>`) already named this
|
||||
session by the ledger arm (#2900); its own
|
||||
session by the ledger arm; its own
|
||||
channel, like the two above.
|
||||
seen_ruling_systems (opt) — System ids whose rulings were already shown
|
||||
this session (milestone 444); shared with
|
||||
this session; shared with
|
||||
/tool-rules. The response's `ruling_system_ids`
|
||||
are the ones shown in full on this call.
|
||||
(Returns a `checkpoint` block on the same contract as /tool-rules —
|
||||
see that endpoint. The write-path HOOK deliberately does not act on
|
||||
it: scribe_prior_art.sh carries a tested property that it never
|
||||
returns a permissionDecision, on the operator's decision that a recall
|
||||
aid may not stand in the way of a write. The block is computed and
|
||||
returns a permissionDecision, because a recall aid must not stand in
|
||||
the way of a write. The block is computed and
|
||||
returned so that decision can be revisited with evidence rather than
|
||||
re-argued, and so a change of mind is a hook edit and not a feature.)
|
||||
|
||||
shapes (opt) — comma-separated `kind:name` definitions the hook
|
||||
found in (or enclosing) the payload, kind being
|
||||
css|sym. The shape ledger's write-path feed
|
||||
(#2791): when the session recently PULLED a
|
||||
css|sym. The shape ledger's write-path feed:
|
||||
when the session recently PULLED a
|
||||
snippet this payload references or resembles,
|
||||
these carry it as a proposal — evidence for the
|
||||
agent's own end-of-turn judgment, never a
|
||||
verdict (milestone 439). Honoured only for a
|
||||
verdict. Honoured only for a
|
||||
caller allowed to write — a read-scoped key still
|
||||
gets the hint, and never changes the ledger on a
|
||||
GET.
|
||||
@@ -537,7 +535,7 @@ def _parse_shapes(raw: str) -> list[tuple[str, str]]:
|
||||
@plugin_bp.get("/shape-check")
|
||||
@login_required
|
||||
async def shape_check():
|
||||
"""The end-of-turn question (milestone 439): what did this turn write that
|
||||
"""The end-of-turn question: what did this turn write that
|
||||
nobody has judged?
|
||||
|
||||
The client's write hooks keep a ledger of the definitions each turn wrote;
|
||||
@@ -602,7 +600,7 @@ async def shape_check():
|
||||
@plugin_bp.get("/claim-session")
|
||||
@login_required
|
||||
async def claim_session():
|
||||
"""Bind the harness's session id to the caller's live claim on a task (milestone 381).
|
||||
"""Bind the harness's session id to the caller's live claim on a task.
|
||||
|
||||
Called by `scribe_claim_session.sh` after `update_task` / `add_task_log`.
|
||||
The server has already stamped the claim on that write; this only says
|
||||
@@ -631,7 +629,7 @@ async def claim_session():
|
||||
@plugin_bp.get("/release-session")
|
||||
@login_required
|
||||
async def release_session():
|
||||
"""Release the claims a session held, as it ends (milestone 381 step 4).
|
||||
"""Release the claims a session held, as it ends.
|
||||
|
||||
Called by `scribe_session_end.sh`. Best-effort by design: SessionEnd does
|
||||
not fire on a crash, so the claim's lease — not this call — is what makes a
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""A small S3 client for sending backups: AWS Signature Version 4 over httpx,
|
||||
path-style addressing (endpoint/bucket/key), and the two calls a backup needs.
|
||||
|
||||
Ported from FabledSteward's `steward/backup/s3.py`, which is tested against
|
||||
the same AWS examples as tests/test_services_backup_s3.py.
|
||||
The signing is checked against AWS's published SigV4 examples in
|
||||
tests/test_services_backup_s3.py.
|
||||
|
||||
Scribe puts and lists; it never deletes. The bucket keeps the history and
|
||||
thins it on its own policy, so a key that may only add objects is enough —
|
||||
|
||||
@@ -324,8 +324,8 @@ def rule_brief(rule: Rule, **extra) -> dict:
|
||||
of payload for a precision nobody reads.
|
||||
|
||||
`why` and `how_to_apply` are deliberately NOT here — they are the depth a
|
||||
caller gets from get_rule, and putting them in every listing is the bloat
|
||||
this milestone is about.
|
||||
caller gets from get_rule, and putting them in every listing would bloat
|
||||
every payload that lists rules.
|
||||
"""
|
||||
out = {
|
||||
"id": rule.id,
|
||||
@@ -1583,11 +1583,10 @@ async def rules_due_for_verification(
|
||||
) -> list[Rule]:
|
||||
"""Rules that carry a check, oldest verification first, never-checked top.
|
||||
|
||||
THE QUERY THIS MILESTONE EXISTS FOR. `verify_with` and `expires_when` are
|
||||
storage; this is what turns them into something that gets acted on. The
|
||||
307 audit cost a session and found four broken rules by luck — this makes
|
||||
the same question a list, and staleness measurable by age instead of
|
||||
discoverable by accident.
|
||||
`verify_with` and `expires_when` are storage; this is what turns them into
|
||||
something that gets acted on. Without it, finding broken rules takes an
|
||||
audit and luck — this makes the same question a list, and staleness
|
||||
measurable by age instead of discoverable by accident.
|
||||
|
||||
Ordered `verified_at` ASC NULLS FIRST: never-checked outranks
|
||||
checked-long-ago, because a rule nobody has ever confirmed is a claim
|
||||
|
||||
@@ -161,8 +161,8 @@ def summarise(outcomes: list[str]) -> dict:
|
||||
`checked` is turns the question was put to (a first stop that wrote
|
||||
something): passed + blocked. `asked` is the blocked ones. `left` is
|
||||
asks the agent walked past — the stop after a block that still had
|
||||
unjudged shapes. That last number is the slip the milestone exists to
|
||||
make visible."""
|
||||
unjudged shapes. That last number is the slip this check exists to make
|
||||
visible."""
|
||||
checked = sum(1 for o in outcomes if o in ("passed", "blocked"))
|
||||
asked = sum(1 for o in outcomes if o == "blocked")
|
||||
left = sum(1 for o in outcomes if o == "left_after_block")
|
||||
|
||||
Reference in New Issue
Block a user