rename(547): the server is grimoire — package, image, DB defaults, backups
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 44s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 44s
Step 2 of the Scribe -> Grimoire rename. - src/scribe -> src/grimoire, and every import, patch target, module path, hypercorn entry point and `python -m grimoire.restore` with it; pyproject, uv.lock and the frontend package are named grimoire. - Image git.fabledsword.com/bvandeusen/fabledgrimoire; CI's Postgres service runs as grimoire / grimoire_test. - Compose files default the DB user, database and (prod) volume to grimoire, each overridable from .env, so an install created as Scribe keeps its data by setting POSTGRES_USER, POSTGRES_DB and PGDATA_VOLUME. - Backups are written as grimoire-<ts>.tar with format grimoire-backup-tar. Archives named scribe-* with the scribe-backup-tar manifest still restore and are still counted at the destination (folder and S3), with tests. - Exports, the MCP auth realm, the ASGI scope key and the persistent logger carry the new name. Kept on purpose: the secret-sealing key label stays "scribe.secrets.v1", because every stored secret was derived under it. Plugin identity (plugin name, hooks, env vars, .scribe marker, skills) is step 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+46
-46
@@ -6,8 +6,8 @@ surface. DB-backed behaviour is in the integration lane.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from scribe.models import Base
|
||||
from scribe.models.code_shape import SHAPE_CLASSIFIERS, SHAPE_STATUSES, CodeShape
|
||||
from grimoire.models import Base
|
||||
from grimoire.models.code_shape import SHAPE_CLASSIFIERS, SHAPE_STATUSES, CodeShape
|
||||
|
||||
|
||||
def test_identity_is_project_repo_path_symbol_kind():
|
||||
@@ -57,7 +57,7 @@ def test_status_queries_have_an_index():
|
||||
|
||||
|
||||
def test_batch_validation_names_the_failing_item():
|
||||
from scribe.services.shape_ledger import validate_classifications as v
|
||||
from grimoire.services.shape_ledger import validate_classifications as v
|
||||
|
||||
ok = {"path": "src/a.py", "symbol": "f", "status": "exempt", "reason": "one-off"}
|
||||
assert v([ok]) is None
|
||||
@@ -78,7 +78,7 @@ def test_batch_validation_names_the_failing_item():
|
||||
|
||||
|
||||
def test_classify_and_list_are_mounted_as_mcp_tools():
|
||||
from scribe.mcp.server import build_mcp_server
|
||||
from grimoire.mcp.server import build_mcp_server
|
||||
|
||||
mcp = build_mcp_server()
|
||||
for name in ("classify_shapes", "list_shapes", "refresh_pattern_coverage"):
|
||||
@@ -89,7 +89,7 @@ def test_classify_and_list_are_mounted_as_mcp_tools():
|
||||
|
||||
|
||||
def test_symbol_reference_is_word_bounded_and_kind_aware():
|
||||
from scribe.services.shape_ledger import references_symbol as ref
|
||||
from grimoire.services.shape_ledger import references_symbol as ref
|
||||
|
||||
code = "const ok = await confirmed({ title: 'x' });\nif (!ok) return;"
|
||||
assert ref(code, "confirmed", "sym")
|
||||
@@ -107,7 +107,7 @@ def test_symbol_reference_is_word_bounded_and_kind_aware():
|
||||
|
||||
|
||||
def test_snippet_kind_reads_the_symbol_then_the_language():
|
||||
from scribe.services.shape_ledger import snippet_kind
|
||||
from grimoire.services.shape_ledger import snippet_kind
|
||||
|
||||
assert snippet_kind(".btn-primary", "css") == "css"
|
||||
assert snippet_kind(".btn-primary", "") == "css"
|
||||
@@ -117,7 +117,7 @@ def test_snippet_kind_reads_the_symbol_then_the_language():
|
||||
|
||||
|
||||
def test_route_shapes_param_parses_capped_and_deduped():
|
||||
from scribe.routes.plugin import _SHAPES_CAP, _parse_shapes
|
||||
from grimoire.routes.plugin import _SHAPES_CAP, _parse_shapes
|
||||
|
||||
assert _parse_shapes("css:btn-primary,sym:onTrash") == [
|
||||
("css", "btn-primary"), ("sym", "onTrash"),
|
||||
@@ -132,7 +132,7 @@ def test_hook_is_a_server_internal_classifier():
|
||||
"""`hook` is in the status vocabulary but NOT a via a caller may claim —
|
||||
a classify_shapes call saying via="hook" would launder judgment as
|
||||
evidence (the reverse of the stamping rule's point)."""
|
||||
from scribe.services.shape_ledger import _CALLER_VIAS
|
||||
from grimoire.services.shape_ledger import _CALLER_VIAS
|
||||
|
||||
assert "hook" in SHAPE_CLASSIFIERS
|
||||
assert "hook" not in _CALLER_VIAS
|
||||
@@ -145,7 +145,7 @@ def test_proposal_columns_and_vocabulary_are_pinned():
|
||||
"""Fingerprints + the proposer's standing suggestion live on the row; the
|
||||
basis vocabulary is fixed, with `derive` the odd one out (a group, not a
|
||||
snippet)."""
|
||||
from scribe.models.code_shape import PROPOSAL_BASES
|
||||
from grimoire.models.code_shape import PROPOSAL_BASES
|
||||
|
||||
cols = CodeShape.__table__.c
|
||||
for name in ("signature", "body_sha", "proposed_snippet_id", "proposal_basis",
|
||||
@@ -168,7 +168,7 @@ def test_row_proposal_property_is_one_object_or_none():
|
||||
|
||||
|
||||
def test_signature_similarity_blanks_the_names():
|
||||
from scribe.services.shape_ledger import signature_similarity as sim
|
||||
from grimoire.services.shape_ledger import signature_similarity as sim
|
||||
|
||||
a = "def move_event(project_id: int, event_id: int, after_id: int | None):"
|
||||
b = "def move_beat(project_id: int, beat_id: int, after_id: int | None):"
|
||||
@@ -184,7 +184,7 @@ def test_a_model_with_a_different_mixin_list_is_proposed_against_the_model_canon
|
||||
whose mixins differ from the canon's scores ~0.795 against it — a true
|
||||
instance, and the proposal is read before it is confirmed, so the floor
|
||||
must admit it."""
|
||||
from scribe.services.shape_ledger import Canon, _norm_text, match_canon
|
||||
from grimoire.services.shape_ledger import Canon, _norm_text, match_canon
|
||||
|
||||
model = Canon(44, "sym", "Gadget", (("src/app/models/gadget.py", "Gadget"),),
|
||||
"class Gadget(Base, TimestampMixin, SoftDeleteMixin):",
|
||||
@@ -196,7 +196,7 @@ def test_a_model_with_a_different_mixin_list_is_proposed_against_the_model_canon
|
||||
|
||||
|
||||
def test_text_containment_is_whitespace_insensitive_with_a_floor():
|
||||
from scribe.services.shape_ledger import text_contains
|
||||
from grimoire.services.shape_ledger import text_contains
|
||||
|
||||
code = "const ok = await confirmed({ title: 'Delete?', confirmLabel: 'Delete' });\nif (!ok) return;"
|
||||
body = "async function onDelete() {\n const ok = await confirmed({\n title: 'Delete?',\n confirmLabel: 'Delete'\n });\n if (!ok) return;\n}"
|
||||
@@ -206,14 +206,14 @@ def test_text_containment_is_whitespace_insensitive_with_a_floor():
|
||||
|
||||
|
||||
def _canon(sid, kind="sym", symbol="", locations=(), signature="", code="", project_id=0):
|
||||
from scribe.services.shape_ledger import Canon, _norm_text
|
||||
from grimoire.services.shape_ledger import Canon, _norm_text
|
||||
return Canon(sid, kind, symbol, tuple(locations), signature, _norm_text(code), project_id)
|
||||
|
||||
|
||||
def test_match_canon_prefers_the_shapes_own_project_on_a_tie():
|
||||
"""The same helper recorded in two projects: the shape's own project's
|
||||
record is its canon; family canon elsewhere is the fallback."""
|
||||
from scribe.services.shape_ledger import match_canon
|
||||
from grimoire.services.shape_ledger import match_canon
|
||||
family = _canon(3, "sym", "slugify", [("lib/text.py", "slugify")], project_id=1)
|
||||
own = _canon(4, "sym", "slugify", [("src/util/text.py", "slugify")], project_id=2)
|
||||
assert match_canon("sym", "src/other.py", "slugify", "def slugify(t):", "",
|
||||
@@ -226,7 +226,7 @@ def test_match_canon_prefers_the_shapes_own_project_on_a_tie():
|
||||
|
||||
|
||||
def test_match_canon_orders_bases_strongest_first_and_respects_kind():
|
||||
from scribe.services.shape_ledger import match_canon
|
||||
from grimoire.services.shape_ledger import match_canon
|
||||
|
||||
confirmed = _canon(
|
||||
7, "sym", "confirmed", [("frontend/src/composables/useConfirm.ts", "confirmed")],
|
||||
@@ -269,8 +269,8 @@ def test_match_canon_gates_sym_bases_by_language_family():
|
||||
offered for every auth view's handleSubmit that calls authStore.register()
|
||||
and for a TS store's own `register`. Unknown language on either side →
|
||||
no gate (the canons recorded without a language keep proposing)."""
|
||||
from scribe.services.shape_ledger import Canon, _norm_text, match_canon, same_family
|
||||
py_register = Canon(46, "sym", "register", (("src/scribe/mcp/tools/notes.py", "register"),),
|
||||
from grimoire.services.shape_ledger import Canon, _norm_text, match_canon, same_family
|
||||
py_register = Canon(46, "sym", "register", (("src/grimoire/mcp/tools/notes.py", "register"),),
|
||||
"def register(mcp) -> None:", _norm_text("def register(mcp) -> None: ..."), 2, "python")
|
||||
ts_helper = Canon(53, "sym", "apiErrorMessage", (("frontend/src/api/client.ts", "apiErrorMessage"),),
|
||||
"export function apiErrorMessage(e: unknown, fallback: string): string {",
|
||||
@@ -285,7 +285,7 @@ def test_match_canon_gates_sym_bases_by_language_family():
|
||||
"async function register(u: string) {", "return apiPost('/api/auth/register', {u})",
|
||||
[py_register]) is None
|
||||
# Same family still proposes by symbol; unknown language still proposes.
|
||||
assert match_canon("sym", "src/scribe/mcp/tools/other.py", "register",
|
||||
assert match_canon("sym", "src/grimoire/mcp/tools/other.py", "register",
|
||||
"def register(mcp) -> None:", "pass", [py_register]) == (46, "symbol", 1.0)
|
||||
unknown = py_register._replace(language="")
|
||||
assert match_canon("sym", "frontend/src/stores/auth.ts", "register",
|
||||
@@ -298,7 +298,7 @@ def test_match_canon_gates_sym_bases_by_language_family():
|
||||
def test_match_canon_reference_skips_generic_verbs():
|
||||
"""A bare mention of `load`/`save`/`register` is not a call site of THIS
|
||||
canon; the symbol basis still catches a second definition of the name."""
|
||||
from scribe.services.shape_ledger import Canon, _norm_text, match_canon
|
||||
from grimoire.services.shape_ledger import Canon, _norm_text, match_canon
|
||||
loader = Canon(70, "sym", "load", (("frontend/src/components/A.vue", "load"),),
|
||||
"async function load() {", _norm_text("async function load() { await fetch() }"), 2, "vue")
|
||||
body = "async function refresh() {\n await load();\n}"
|
||||
@@ -309,13 +309,13 @@ def test_match_canon_reference_skips_generic_verbs():
|
||||
def test_match_canon_symbol_beats_everything_including_css_copies():
|
||||
"""The previous test's css `btn-primary`-elsewhere case, stated plainly:
|
||||
a second definition of the canon's own name is the symbol basis."""
|
||||
from scribe.services.shape_ledger import match_canon
|
||||
from grimoire.services.shape_ledger import match_canon
|
||||
btn = _canon(9, "css", ".btn-primary", [("web/buttons.css", ".btn-primary")], ".btn-primary {", ".btn-primary { color: red; padding: 4px 8px; border-radius: 4px; }")
|
||||
assert match_canon("css", "web/other.css", "btn-primary", ".btn-primary {", ".btn-primary { color: blue }", [btn]) == (9, "symbol", 1.0)
|
||||
|
||||
|
||||
def test_derive_groups_copy_before_name_with_floors():
|
||||
from scribe.services.shape_ledger import derive_groups
|
||||
from grimoire.services.shape_ledger import derive_groups
|
||||
|
||||
rows = [
|
||||
("a.py", "sym", "helper", "sha1"), ("b.py", "sym", "helper", "sha1"), # identical copies
|
||||
@@ -348,8 +348,8 @@ def test_derive_new_summary_counts_copies_first_seen_since_the_previous_refresh(
|
||||
first seed (since=None) never count."""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from scribe.models.code_shape import CodeShape
|
||||
from scribe.services.shape_ledger import derive_new_summary
|
||||
from grimoire.models.code_shape import CodeShape
|
||||
from grimoire.services.shape_ledger import derive_new_summary
|
||||
|
||||
t0 = datetime(2026, 8, 22, 12, 0, tzinfo=timezone.utc)
|
||||
|
||||
@@ -377,8 +377,8 @@ def test_derive_new_summary_counts_copies_first_seen_since_the_previous_refresh(
|
||||
def test_proposal_summary_ranks_body_identical_groups_first_and_sees_scoped_rows():
|
||||
"""Dup groups (the real copies) outrank name groups (usually
|
||||
convention), wider spread first; scoped rows are in the readout."""
|
||||
from scribe.models.code_shape import CodeShape
|
||||
from scribe.services.shape_ledger import proposal_summary
|
||||
from grimoire.models.code_shape import CodeShape
|
||||
from grimoire.services.shape_ledger import proposal_summary
|
||||
|
||||
def row(path, symbol, group, kind="css", status="scoped"):
|
||||
return CodeShape(project_id=2, repo_key="r", path=path, symbol=symbol, kind=kind,
|
||||
@@ -414,7 +414,7 @@ def test_proposal_summary_ranks_body_identical_groups_first_and_sees_scoped_rows
|
||||
def test_confirm_requires_a_named_scope():
|
||||
import asyncio
|
||||
|
||||
from scribe.services.shape_ledger import confirm_proposals
|
||||
from grimoire.services.shape_ledger import confirm_proposals
|
||||
|
||||
with pytest.raises(ValueError) as err:
|
||||
asyncio.run(confirm_proposals(1, 2))
|
||||
@@ -422,7 +422,7 @@ def test_confirm_requires_a_named_scope():
|
||||
|
||||
|
||||
def test_proposer_tools_are_mounted():
|
||||
from scribe.mcp.server import build_mcp_server
|
||||
from grimoire.mcp.server import build_mcp_server
|
||||
|
||||
mcp = build_mcp_server()
|
||||
assert mcp._tool_manager.get_tool("confirm_shape_proposals") is not None
|
||||
@@ -442,7 +442,7 @@ def test_proposer_tools_are_mounted():
|
||||
def test_compact_row_carries_identity_standing_and_the_proposers_word_only():
|
||||
"""A 500-row compact page must fit the tool budget: no commits, shas or
|
||||
timestamps; optional fields only when set."""
|
||||
from scribe.models.code_shape import CodeShape
|
||||
from grimoire.models.code_shape import CodeShape
|
||||
row = CodeShape(project_id=2, repo_key="r", path="src/a.py", symbol="f", kind="sym",
|
||||
status="unclassified", signature="def f(x):", body_sha="abc",
|
||||
first_seen_commit="c1", last_seen_commit="c2")
|
||||
@@ -464,7 +464,7 @@ def test_resolve_consumers_prefers_the_own_file_and_fans_out_for_shared_names():
|
||||
OWN row when it defines the class (a scoped rule, consumed by its own
|
||||
markup); otherwise to every other definition of the name — one shared
|
||||
sheet, or all of several (the map fans out rather than guessing)."""
|
||||
from scribe.services.shape_ledger import resolve_consumers
|
||||
from grimoire.services.shape_ledger import resolve_consumers
|
||||
css_rows = [
|
||||
(1, "v/A.vue", "error-msg"), # scoped, defined + used in A
|
||||
(2, "v/B.vue", "error-msg"), # scoped, defined in B, used in B and C
|
||||
@@ -494,7 +494,7 @@ def test_resolve_consumers_credits_every_row_a_prefix_could_have_built():
|
||||
the head — each under the same own-file-else-fan-out rule. Crediting all
|
||||
of them is the honest reading: the alternative is calling live rules
|
||||
unused, which is what the flag existed to avoid."""
|
||||
from scribe.services.shape_ledger import resolve_consumers
|
||||
from grimoire.services.shape_ledger import resolve_consumers
|
||||
css_rows = [
|
||||
(1, "assets/app.css", "status-done"),
|
||||
(2, "assets/app.css", "status-todo"),
|
||||
@@ -523,8 +523,8 @@ def test_resolve_consumers_credits_every_row_a_prefix_could_have_built():
|
||||
|
||||
|
||||
def test_consumer_edges_table_cascades_with_the_shape():
|
||||
from scribe.models import Base
|
||||
from scribe.models.code_shape import CONSUMER_BASES, CodeShapeConsumer
|
||||
from grimoire.models import Base
|
||||
from grimoire.models.code_shape import CONSUMER_BASES, CodeShapeConsumer
|
||||
assert "code_shape_consumers" in Base.metadata.tables
|
||||
cols = CodeShapeConsumer.__table__.c
|
||||
assert next(iter(cols.shape_id.foreign_keys)).ondelete == "CASCADE"
|
||||
@@ -534,9 +534,9 @@ def test_consumer_edges_table_cascades_with_the_shape():
|
||||
def test_uses_edges_table_and_validation():
|
||||
"""Consumption is its own relation — a table that cascades with
|
||||
both ends, and `uses` on a classification must be a list of ids."""
|
||||
from scribe.models import Base
|
||||
from scribe.models.code_shape import USE_BASES, CodeShapeUse
|
||||
from scribe.services.shape_ledger import validate_classifications
|
||||
from grimoire.models import Base
|
||||
from grimoire.models.code_shape import USE_BASES, CodeShapeUse
|
||||
from grimoire.services.shape_ledger import validate_classifications
|
||||
assert "code_shape_uses" in Base.metadata.tables
|
||||
cols = CodeShapeUse.__table__.c
|
||||
assert next(iter(cols.shape_id.foreign_keys)).ondelete == "CASCADE"
|
||||
@@ -549,12 +549,12 @@ def test_uses_edges_table_and_validation():
|
||||
|
||||
|
||||
def test_reference_canons_names_every_used_canon_not_just_the_best():
|
||||
from scribe.services.shape_ledger import Canon, _norm_text, reference_canons
|
||||
from grimoire.services.shape_ledger import Canon, _norm_text, reference_canons
|
||||
a = Canon(1, "sym", "hash_token", (("src/x.py", "hash_token"),), "def hash_token(raw):", _norm_text("x"), 2, "python")
|
||||
b = Canon(2, "sym", "rules_payload", (("src/y.py", "rules_payload"),), "def rules_payload(r):", _norm_text("y"), 2, "python")
|
||||
ts = Canon(3, "sym", "fmtDate", (("f/d.ts", "fmtDate"),), "export function fmtDate(iso: string): string {", _norm_text("z"), 2, "typescript")
|
||||
body = "def create_invitation(email):\n h = hash_token(raw)\n return rules_payload(h)\n"
|
||||
assert reference_canons("sym", "src/scribe/services/auth.py", "create_invitation", body, [a, b, ts]) == [1, 2]
|
||||
assert reference_canons("sym", "src/grimoire/services/auth.py", "create_invitation", body, [a, b, ts]) == [1, 2]
|
||||
# the shape's own name and the other language family are never "uses"
|
||||
assert reference_canons("sym", "src/x.py", "hash_token", body, [a]) == []
|
||||
assert reference_canons("sym", "f/v.vue", "show", "fmtDate(x); hash_token(y)", [a, ts]) == [3]
|
||||
@@ -563,8 +563,8 @@ def test_reference_canons_names_every_used_canon_not_just_the_best():
|
||||
def test_reason_codes_are_a_fixed_catalogue_and_validated():
|
||||
"""An optional index beside the prose reason; unknown codes are a
|
||||
structural error (the batch applies nothing)."""
|
||||
from scribe.models.code_shape import REASON_CODES
|
||||
from scribe.services.shape_ledger import validate_classifications
|
||||
from grimoire.models.code_shape import REASON_CODES
|
||||
from grimoire.services.shape_ledger import validate_classifications
|
||||
assert set(REASON_CODES) == {
|
||||
"scoped-css", "one-off-handler", "test-helper", "convention-plumbing",
|
||||
"pure-helper", "generated", "script", "typed-record",
|
||||
@@ -577,8 +577,8 @@ def test_reason_codes_are_a_fixed_catalogue_and_validated():
|
||||
|
||||
|
||||
def test_rule_matches_is_directory_glob_and_kind_aware():
|
||||
from scribe.models.code_shape import CodeShape
|
||||
from scribe.services.shape_ledger import rule_matches
|
||||
from grimoire.models.code_shape import CodeShape
|
||||
from grimoire.services.shape_ledger import rule_matches
|
||||
|
||||
def row(path, symbol, kind="sym"):
|
||||
return CodeShape(project_id=2, repo_key="r", path=path, symbol=symbol, kind=kind, status="unclassified")
|
||||
@@ -591,8 +591,8 @@ def test_rule_matches_is_directory_glob_and_kind_aware():
|
||||
assert rule_matches(r, path="frontend/src/views/LoginView.vue", pattern="", kind="")
|
||||
# CSS symbols compare without the leading dot, like everywhere else.
|
||||
assert rule_matches(row("w/a.css", ".btn-primary", "css"), path="w", pattern="btn-*", kind="css")
|
||||
assert rule_matches(row("src/scribe/services/backup.py", "_note_rows"), path="src/scribe/services", pattern="_*_rows", kind="")
|
||||
assert not rule_matches(row("src/scribe/services/backup.py", "export_full_backup"), path="src/scribe/services", pattern="_*_rows", kind="")
|
||||
assert rule_matches(row("src/grimoire/services/backup.py", "_note_rows"), path="src/grimoire/services", pattern="_*_rows", kind="")
|
||||
assert not rule_matches(row("src/grimoire/services/backup.py", "export_full_backup"), path="src/grimoire/services", pattern="_*_rows", kind="")
|
||||
|
||||
|
||||
# --- step 7: the divergence readout (pure) ----------------------------------
|
||||
@@ -605,7 +605,7 @@ def _row(path, kind="sym", status="unclassified", snippet_id=None):
|
||||
|
||||
|
||||
def test_dominant_canon_needs_enough_judged_siblings_and_a_clear_majority():
|
||||
from scribe.services.shape_ledger import dominant_canon
|
||||
from grimoire.services.shape_ledger import dominant_canon
|
||||
|
||||
dense = [_row(f"c/{i}", status="instance", snippet_id=7) for i in range(4)] + [
|
||||
_row("c/x", status="instance", snippet_id=8), _row("c/y")]
|
||||
@@ -623,7 +623,7 @@ def test_dominant_canon_needs_enough_judged_siblings_and_a_clear_majority():
|
||||
|
||||
|
||||
def test_history_and_readout_tools_are_mounted_and_shape_history_is_read_only():
|
||||
from scribe.mcp.server import _READ_ONLY_TOOLS, build_mcp_server
|
||||
from grimoire.mcp.server import _READ_ONLY_TOOLS, build_mcp_server
|
||||
|
||||
mcp = build_mcp_server()
|
||||
assert mcp._tool_manager.get_tool("shape_history") is not None
|
||||
@@ -632,7 +632,7 @@ def test_history_and_readout_tools_are_mounted_and_shape_history_is_read_only():
|
||||
|
||||
|
||||
def test_history_and_divergence_columns_are_pinned():
|
||||
from scribe.models.code_shape import SHAPE_EVENTS, CodeShapeEvent
|
||||
from grimoire.models.code_shape import SHAPE_EVENTS, CodeShapeEvent
|
||||
|
||||
cols = CodeShape.__table__.c
|
||||
for name in ("classified_sha", "recheck_at", "diverges_from"):
|
||||
|
||||
Reference in New Issue
Block a user