feat(ledger): mechanical proposer — every refresh proposes instances against canon and groups derive-first candidates; agents confirm in batches (#2792, milestone 294 step 6)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Failing after 34s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Successful in 1m17s
CI & Build / Build & push image (push) Successful in 48s
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Failing after 34s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Successful in 1m17s
CI & Build / Build & push image (push) Successful in 48s
Shapes now carry a content fingerprint (signature + whitespace/comment- insensitive body_sha; migration 0080) and the proposer runs inside the coverage refresh, the one moment bodies exist: symbol elsewhere → textual containment → body references the canon → signature resemblance → semantic (capped per refresh, unreached rows stay unexamined for the next). A hit is a proposal on the row (proposed_snippet_id/basis/score), never a classification; rows with no canon hit group by the derive-first rule (identical body in ≥2 places, same name in ≥3 files) as proposal_basis= derive + a group key. list_shapes(proposal=any|canon|derive|<basis>) is the queue; confirm_shape_proposals(project_id, snippet_id|path|basis) confirms in batches as agent instances; any classify_shapes/hook stamp retires the proposal. Readout carries proposed + derive_groups (line, payload, card). Plugin 0.1.35 (skill: the machine proposes, judgment classifies). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ from datetime import datetime
|
||||
from sqlalchemy import (
|
||||
BigInteger,
|
||||
DateTime,
|
||||
Float,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
@@ -19,6 +20,12 @@ from scribe.models.base import TimestampMixin
|
||||
SHAPE_STATUSES = ("canonical", "instance", "variant", "exempt", "unclassified")
|
||||
SHAPE_CLASSIFIERS = ("agent", "audit", "hook", "mechanical", "import")
|
||||
|
||||
# How the mechanical proposer (#2792) 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.
|
||||
PROPOSAL_BASES = ("symbol", "text", "reference", "signature", "semantic", "derive")
|
||||
|
||||
|
||||
class CodeShape(Base, TimestampMixin):
|
||||
"""One extracted code shape and its classification against canon (#2787).
|
||||
@@ -42,6 +49,20 @@ class CodeShape(Base, TimestampMixin):
|
||||
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
|
||||
rows as unclassified so they rejoin the todo instead of dangling.
|
||||
|
||||
`signature` / `body_sha` (#2792) 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
|
||||
still never stores code bodies.
|
||||
|
||||
The proposal columns hold the proposer's standing suggestion for an
|
||||
UNCLASSIFIED row: `proposed_snippet_id` + `proposal_basis` + score for
|
||||
"looks like an instance of #N", or `proposal_basis="derive"` +
|
||||
`proposal_group` for "repeats with no canon". `proposed_sha` is the
|
||||
body_sha the row was last examined at, so a refresh re-examines only
|
||||
what changed. A judgment clears the proposal — the machine proposes,
|
||||
judgment classifies.
|
||||
"""
|
||||
|
||||
__tablename__ = "code_shapes"
|
||||
@@ -52,6 +73,7 @@ class CodeShape(Base, TimestampMixin):
|
||||
),
|
||||
Index("ix_code_shapes_project_status", "project_id", "status"),
|
||||
Index("ix_code_shapes_snippet", "snippet_id"),
|
||||
Index("ix_code_shapes_proposed", "project_id", "proposed_snippet_id"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
@@ -76,6 +98,31 @@ class CodeShape(Base, TimestampMixin):
|
||||
vanished_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
signature: Mapped[str] = mapped_column(Text, default="")
|
||||
body_sha: Mapped[str] = mapped_column(Text, default="")
|
||||
proposed_snippet_id: Mapped[int | None] = mapped_column(
|
||||
BigInteger, ForeignKey("notes.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
proposal_basis: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
proposal_score: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
proposal_group: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
proposed_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
proposed_sha: Mapped[str] = mapped_column(Text, default="")
|
||||
|
||||
@property
|
||||
def proposal(self) -> dict | None:
|
||||
"""The standing proposal as one object, or None when the proposer
|
||||
has nothing to say about this row."""
|
||||
if self.proposed_snippet_id is None and not self.proposal_group:
|
||||
return None
|
||||
out: dict = {"basis": self.proposal_basis, "score": self.proposal_score}
|
||||
if self.proposed_snippet_id is not None:
|
||||
out["snippet_id"] = self.proposed_snippet_id
|
||||
if self.proposal_group:
|
||||
out["group"] = self.proposal_group
|
||||
return out
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
@@ -93,6 +140,9 @@ class CodeShape(Base, TimestampMixin):
|
||||
"first_seen_commit": self.first_seen_commit,
|
||||
"last_seen_commit": self.last_seen_commit,
|
||||
"vanished_at": self.vanished_at.isoformat() if self.vanished_at else None,
|
||||
"signature": self.signature,
|
||||
"body_sha": self.body_sha,
|
||||
"proposal": self.proposal,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user