feat(ledger): divergence readout — button B where button A is canon, shape history, and judged-shape recheck (#2793, milestone 294 step 7)
CI & Build / TypeScript typecheck (push) Failing after 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 28s
CI & Build / Python tests (push) Failing after 37s
CI & Build / Build & push image (push) Skipped
CI & Build / TypeScript typecheck (push) Failing after 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 28s
CI & Build / Python tests (push) Failing after 37s
CI & Build / Build & push image (push) Skipped
Every judgment now goes through one helper that remembers the fingerprint judged (classified_sha) and writes a code_shape_events row; the sync writes vanished / reappeared / drifted events and flags recheck_at when a body moves under an instance/variant. The refresh flags diverges_from on shapes new since the previous computation that sit where one canon dominates the judged siblings of their directory+kind and were not proposed as that canon (a first seed flags nothing); the write-path hint asks the same question in-band for the shapes the hook names. list_shapes(flag=divergence|recheck), shape_history(project_id, path, symbol) (read-only), coverage line/payload/ card carry divergent + recheck. Backup v8 carries the history. Plugin 0.1.36. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,6 @@ from scribe.models.rulebook import ( # noqa: E402, F401
|
||||
)
|
||||
from scribe.models.repo_binding import RepoBinding # noqa: E402, F401
|
||||
from scribe.models.forge_connection import ForgeConnection # noqa: E402, F401
|
||||
from scribe.models.code_shape import CodeShape # noqa: E402, F401
|
||||
from scribe.models.code_shape import CodeShape, CodeShapeEvent # noqa: E402, F401
|
||||
from scribe.models.system import System, RecordSystem # noqa: E402, F401
|
||||
from scribe.models.design_system import DesignSystem, DesignToken # noqa: E402, F401
|
||||
|
||||
@@ -56,6 +56,15 @@ class CodeShape(Base, TimestampMixin):
|
||||
matches on and what a later drift recheck compares against; the ledger
|
||||
still never stores code bodies.
|
||||
|
||||
`classified_sha` remembers the fingerprint a judgment was made at;
|
||||
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
|
||||
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.
|
||||
|
||||
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"` +
|
||||
@@ -74,6 +83,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"),
|
||||
Index("ix_code_shapes_diverges", "project_id", "diverges_from"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
@@ -110,6 +120,13 @@ class CodeShape(Base, TimestampMixin):
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
proposed_sha: Mapped[str] = mapped_column(Text, default="")
|
||||
classified_sha: Mapped[str] = mapped_column(Text, default="")
|
||||
recheck_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
diverges_from: Mapped[int | None] = mapped_column(
|
||||
BigInteger, ForeignKey("notes.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
|
||||
@property
|
||||
def proposal(self) -> dict | None:
|
||||
@@ -143,6 +160,66 @@ class CodeShape(Base, TimestampMixin):
|
||||
"signature": self.signature,
|
||||
"body_sha": self.body_sha,
|
||||
"proposal": self.proposal,
|
||||
"classified_sha": self.classified_sha,
|
||||
"recheck_at": self.recheck_at.isoformat() if self.recheck_at else None,
|
||||
"diverges_from": self.diverges_from,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
# What a shape's history records (#2793). Not "appeared" — first_seen and
|
||||
# created_at already say that on the row; history is for what CHANGED:
|
||||
SHAPE_EVENTS = ("classified", "vanished", "reappeared", "drifted")
|
||||
|
||||
|
||||
class CodeShapeEvent(Base):
|
||||
"""One state change in a shape's life — the what-was-used-when record.
|
||||
|
||||
"We used #N here from <date>, #M replaced it at commit C, reason R" is a
|
||||
question the ledger row alone cannot answer once it has moved on; this
|
||||
table keeps each judgment (status, snippet, who, why, at which commit)
|
||||
and each presence change (vanished / reappeared / drifted) as it
|
||||
happened. Denormalised path/symbol/kind so a directory's history reads
|
||||
without joining; `snippet_id` is deliberately FK-free — history outlives
|
||||
the snippet it names, which is the point.
|
||||
"""
|
||||
|
||||
__tablename__ = "code_shape_events"
|
||||
__table_args__ = (
|
||||
Index("ix_code_shape_events_shape", "shape_id", "at"),
|
||||
Index("ix_code_shape_events_project_path", "project_id", "path"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
shape_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("code_shapes.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
project_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
path: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
symbol: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
kind: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
event: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
status: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
snippet_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
||||
classified_by: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
reason: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
commit: Mapped[str] = mapped_column(Text, default="")
|
||||
at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"shape_id": self.shape_id,
|
||||
"project_id": self.project_id,
|
||||
"path": self.path,
|
||||
"symbol": self.symbol,
|
||||
"kind": self.kind,
|
||||
"event": self.event,
|
||||
"status": self.status,
|
||||
"snippet_id": self.snippet_id,
|
||||
"classified_by": self.classified_by,
|
||||
"reason": self.reason,
|
||||
"commit": self.commit,
|
||||
"at": self.at.isoformat(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user