feat(moments): mount the corpus by proposal - a pass and an open-after-moment signal, both stopping at the operator (milestone 458 step 7, #4925)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 19s
CI & Build / integration (push) Failing after 43s
CI & Build / Python tests (push) Failing after 46s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 19s
CI & Build / integration (push) Failing after 43s
CI & Build / Python tests (push) Failing after 46s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / Build & push image (push) Skipped
A rule written before moments existed is mounted on nothing. Step 7 records, per (rule, moment), whether it belongs there and who said so: - rule_moment_judgments (migration 0118, backup v23): suggested / confirmed / rejected, from a pass, the signal, or an edit. Moment "" is "no moment fits". - The pass: rules_to_mount lists unjudged rules; propose_rule_moments records suggestions that mount nothing; rule_moment_proposals and judge_rule_moments put them to the operator. A confirm mounts, a reject is kept so the pair is never proposed again. Same service behind REST and a "Waiting on you" panel in Settings > Moments. - Edits are judgments: set_rule_moments, the one mount write path, confirms what was added and rejects what was removed in the same transaction. - The signal: scribe_moment.sh keeps a per-session acts ledger; when a rule is opened, scribe_record_opened.sh sends the last three minutes of it to /api/plugin/rule-opened. The acts resolve through the install's mappings; work.run and work.change are not evidence. Counted per distinct session with lesson_rules' evidence model, and once due the open returns one line asking the reader to offer the mount. - scribe_session_end.sh removes the session's scribe-moment files. Plugin 2026.10.05.2003. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { getProposals, judgeProposals } from "@/api/moments";
|
||||
import type { MomentProposal, ProposalsPayload, RuleProposals, Verdict } from "@/api/moments";
|
||||
import { apiErrorMessage } from "@/api/client";
|
||||
import { useMomentsStore } from "@/stores/moments";
|
||||
import { useToastStore } from "@/stores/toast";
|
||||
|
||||
/**
|
||||
* Proposals that a rule belongs on a moment, waiting on a person (milestone
|
||||
* 458 step 7). They come from a pass that read the rule, or from the rule
|
||||
* being opened just after the moment fired in several sessions. Nothing here
|
||||
* is mounted until "Mount" is pressed — the same judgment `judge_rule_moments`
|
||||
* makes in a session, through the same service.
|
||||
*/
|
||||
const store = useMomentsStore();
|
||||
const toast = useToastStore();
|
||||
|
||||
const data = ref<ProposalsPayload | null>(null);
|
||||
const failed = ref(false);
|
||||
const busy = ref<string | null>(null);
|
||||
|
||||
const rules = computed<RuleProposals[]>(() => data.value?.rules ?? []);
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
data.value = await getProposals();
|
||||
failed.value = false;
|
||||
} catch {
|
||||
failed.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function key(rule: RuleProposals, p: MomentProposal): string {
|
||||
return `${rule.id}:${p.moment}`;
|
||||
}
|
||||
|
||||
function sourceLabel(p: MomentProposal): string {
|
||||
if (p.source === "signal") {
|
||||
return `opened just after this moment in ${p.evidence.situations} sessions`;
|
||||
}
|
||||
return "proposed from the rule's text";
|
||||
}
|
||||
|
||||
async function decide(rule: RuleProposals, p: MomentProposal, verdict: Verdict) {
|
||||
busy.value = key(rule, p);
|
||||
try {
|
||||
const out = await judgeProposals([{
|
||||
rule_id: rule.id, moment: p.moment, verdict,
|
||||
note: verdict === "confirm" ? "Mounted in Settings." : "Declined in Settings.",
|
||||
}]);
|
||||
if (out.refused.length) {
|
||||
toast.show(out.refused[0].error, "error");
|
||||
return;
|
||||
}
|
||||
await load();
|
||||
// A mount changes the per-moment counts the list below shows.
|
||||
if (verdict === "confirm") await store.load(true);
|
||||
} catch (e) {
|
||||
toast.show(apiErrorMessage(e, "Could not record that"), "error");
|
||||
} finally {
|
||||
busy.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-if="failed || rules.length" class="proposals" aria-labelledby="proposals-title">
|
||||
<h3 id="proposals-title" class="sub-title">Waiting on you</h3>
|
||||
<p v-if="failed" class="error-msg">
|
||||
The proposals could not be loaded.
|
||||
<button type="button" class="btn-text" @click="load">Try again</button>
|
||||
</p>
|
||||
<template v-else>
|
||||
<p class="field-hint">
|
||||
Moments these rules may belong on. Mounting one makes the rule arrive whenever that
|
||||
moment happens, whatever the work is about; declining keeps it from being proposed again.
|
||||
</p>
|
||||
<ul class="proposal-list">
|
||||
<li v-for="rule in rules" :key="rule.id" class="proposal-rule">
|
||||
<div class="rule-head">
|
||||
<span class="rule-title">#{{ rule.id }} {{ rule.title }}</span>
|
||||
<span v-if="rule.mounted.length" class="rule-mounted">
|
||||
on <code v-for="m in rule.mounted" :key="m" class="moment-name">{{ m }}</code>
|
||||
</span>
|
||||
</div>
|
||||
<p class="rule-statement">{{ rule.statement }}</p>
|
||||
<ul class="proposal-items">
|
||||
<li v-for="p in rule.proposals" :key="key(rule, p)" class="proposal-item">
|
||||
<code class="moment-name">{{ p.moment }}</code>
|
||||
<span class="proposal-why">
|
||||
{{ p.why || sourceLabel(p) }}
|
||||
<span v-if="p.why" class="proposal-source">· {{ sourceLabel(p) }}</span>
|
||||
</span>
|
||||
<span class="proposal-actions">
|
||||
<button
|
||||
type="button" class="btn-primary btn-sm"
|
||||
:disabled="busy !== null"
|
||||
@click="decide(rule, p, 'confirm')"
|
||||
>Mount</button>
|
||||
<button
|
||||
type="button" class="btn-text"
|
||||
:disabled="busy !== null"
|
||||
@click="decide(rule, p, 'reject')"
|
||||
>Not this</button>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.proposals { display: flex; flex-direction: column; gap: var(--fs-space-2); }
|
||||
.sub-title { margin: 0; font-size: 0.95rem; color: var(--fs-text-primary); }
|
||||
.proposal-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--fs-space-2); }
|
||||
.proposal-rule {
|
||||
padding: var(--fs-space-3);
|
||||
background: var(--fs-surface-page);
|
||||
border: 1px dashed var(--fs-border-color);
|
||||
border-radius: var(--fs-radius-md);
|
||||
}
|
||||
.rule-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--fs-space-2); }
|
||||
.rule-title { font-size: var(--fs-size-body-sm); color: var(--fs-text-primary); font-weight: var(--fs-weight-medium); }
|
||||
.rule-mounted { display: inline-flex; flex-wrap: wrap; gap: var(--fs-space-1); font-size: var(--fs-size-tiny); color: var(--fs-text-tertiary); }
|
||||
.rule-statement { margin: var(--fs-space-1) 0 var(--fs-space-2); font-size: var(--fs-size-body-sm); color: var(--fs-text-secondary); line-height: var(--fs-leading-body); }
|
||||
.proposal-items { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--fs-space-2); }
|
||||
.proposal-item { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--fs-space-2); font-size: var(--fs-size-body-sm); }
|
||||
.proposal-why { flex: 1 1 16rem; color: var(--fs-text-secondary); }
|
||||
.proposal-source { color: var(--fs-text-tertiary); font-size: var(--fs-size-tiny); }
|
||||
.proposal-actions { display: inline-flex; gap: var(--fs-space-2); margin-left: auto; }
|
||||
</style>
|
||||
|
||||
<style src="@/assets/moments-shared.css" />
|
||||
Reference in New Issue
Block a user