feat(lessons): both records show the link in the web UI (milestone 440 step 7, #4635)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / integration (push) Successful in 1m9s
CI & Build / Python tests (push) Successful in 1m53s
CI & Build / Build & push image (push) Successful in 41s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / integration (push) Successful in 1m9s
CI & Build / Python tests (push) Successful in 1m53s
CI & Build / Build & push image (push) Successful in 41s
The lesson page gains an "Instance of" panel that holds one of three answers. Unjudged is the fall-through, so it is stated rather than left blank: - the rule(s) it was judged an instance of; - "No rule — <why>"; - "Not yet judged". Suggested links show what they rest on (distinct situations, and projects when more than one), with Confirm / Not an instance for a reader who can write. Rejected links stay listed with their reason. The rule slide-over lists the lessons that are instances of it, plus the suggestions waiting on a judgment. Each entry links through to the other record, and kind and state wear the existing .rule-chip. The write check moves to utils/permission.ts. The copy on the snippet page looked for "edit", which the server never sends, so shared editors saw a read-only page (#4640 "The snippet page hid its edit controls from shared editors"). Guards pin the client's unions and write levels to the service's, the model's and access.py's own values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,34 @@
|
||||
import type { RecordUsage } from "@/types/usage";
|
||||
|
||||
import { apiGet, apiPost, apiPatch, apiDelete } from "@/api/client";
|
||||
import { apiGet, apiPost, apiPut, apiPatch, apiDelete } from "@/api/client";
|
||||
import type { RuleKind } from "@/api/rulebooks";
|
||||
|
||||
/** Where a lesson stands on "which rule is this an instance of?"
|
||||
* (`services/lesson_rules.py`: LINKED / NO_RULE / UNJUDGED). Three answers,
|
||||
* not two: "looked at and found to stand alone" and "nobody has looked" are
|
||||
* different facts, and the second is the one worth acting on. A rejected link
|
||||
* alone leaves a lesson unjudged — "not that rule" does not say whether
|
||||
* another one fits. */
|
||||
export type RuleJudgment = "linked" | "no_rule" | "unjudged";
|
||||
|
||||
/** A link's state (`models/lesson_rule_link.py` LINK_STATES). */
|
||||
export type LinkState = "suggested" | "confirmed" | "rejected";
|
||||
|
||||
/** One link from a lesson to a rule, in the reader's view: only rules the
|
||||
* reader owns are listed (`rules_for_lessons`). */
|
||||
export interface LessonRuleLink {
|
||||
id: number;
|
||||
title: string;
|
||||
kind: RuleKind;
|
||||
/** `suggested` carries nothing in retrieval until it is judged; only
|
||||
* `confirmed` changes what surfaces; `rejected` is kept so the pair is
|
||||
* never proposed again. */
|
||||
state: LinkState;
|
||||
/** Why it was confirmed or rejected. */
|
||||
note: string;
|
||||
/** What a suggestion rests on — sent on suggested links only. */
|
||||
evidence?: { situations: number; projects: number; co_surfaced: number };
|
||||
}
|
||||
|
||||
/** A lesson: a transferable insight, retrievable by the SITUATION it applies
|
||||
* to rather than by its topic.
|
||||
@@ -51,6 +79,13 @@ export interface Lesson {
|
||||
updated_at: string | null;
|
||||
systems?: { id: number; name: string }[];
|
||||
usage?: RecordUsage;
|
||||
/** The rules this lesson points at, confirmed first, then suggested, then
|
||||
* rejected. Absent (with `rule_judgment`) when the links could not be read
|
||||
* — which is "not attached", never "no rule". */
|
||||
rules?: LessonRuleLink[];
|
||||
rule_judgment?: RuleJudgment;
|
||||
/** The "no rule fits" answer, present when that is the judgment. */
|
||||
no_rule?: { why: string; judged_at: string | null };
|
||||
/** Set when another user owns this record. */
|
||||
shared?: boolean;
|
||||
owner?: string | null;
|
||||
@@ -128,6 +163,17 @@ export function updateLesson(
|
||||
return apiPatch<Lesson>(`/api/lessons/${id}`, payload);
|
||||
}
|
||||
|
||||
/** Confirm or reject one lesson→rule link. Confirming also clears a "no rule
|
||||
* fits" answer: the two cannot both be the current answer. */
|
||||
export function judgeLessonLink(
|
||||
lessonId: number,
|
||||
ruleId: number,
|
||||
verdict: "confirm" | "reject",
|
||||
note = "",
|
||||
): Promise<unknown> {
|
||||
return apiPut(`/api/lessons/${lessonId}/rules/${ruleId}`, { verdict, note });
|
||||
}
|
||||
|
||||
/** Trash, not erase — recoverable. `apiDelete` discards the body, which is the
|
||||
* established shape here (snippets delete the same way): the batch id is in
|
||||
* the response, but no caller has needed it and inventing a second delete
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { RecordUsage } from "@/types/usage";
|
||||
|
||||
import { apiGet, apiPost, apiPatch, apiDelete } from "@/api/client";
|
||||
import type { LinkState } from "@/api/lessons";
|
||||
|
||||
/** How a rule reaches a session (milestone 307). */
|
||||
|
||||
@@ -82,6 +83,10 @@ export interface Rule {
|
||||
/** Present only when the rule has them (the server omits empty keys). */
|
||||
systems?: { id: number; name: string }[];
|
||||
relations?: RuleRelation[];
|
||||
/** The lessons that point at this rule (milestone 440) — the concrete
|
||||
* situations judged instances of it, plus any suggested and awaiting a
|
||||
* judgment. Readable lessons only; omitted when there are none. */
|
||||
lessons?: { id: number; title: string; state: LinkState; note: string }[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user