|
@@ -25,6 +25,20 @@ export interface HookDiagnostics {
|
|
|
readonly missingUpstream: ReadonlyArray<string>;
|
|
readonly missingUpstream: ReadonlyArray<string>;
|
|
|
readonly distance: number;
|
|
readonly distance: number;
|
|
|
readonly halfLife: number;
|
|
readonly halfLife: number;
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Phase 7 hotfix 3 — number of chapters this hook has been blocked.
|
|
|
|
|
+ *
|
|
|
|
|
+ * When blocked=false this is 0. When blocked=true it is computed as:
|
|
|
|
|
+ * max over unresolved upstream of (currentChapter - referenceChapter)
|
|
|
|
|
+ * where referenceChapter is:
|
|
|
|
|
+ * - upstream.startChapter, if upstream is planted (startChapter > 0)
|
|
|
|
|
+ * - hook.startChapter, if upstream is missing or not yet planted
|
|
|
|
|
+ * (blocked since this hook's own planting)
|
|
|
|
|
+ *
|
|
|
|
|
+ * Reviewer reads this as the "blocked Y chapters" token for threshold
|
|
|
|
|
+ * evaluation (5/6+ chapters → warning).
|
|
|
|
|
+ */
|
|
|
|
|
+ readonly blockedDistance: number;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
type HookLike = StoredHook | HookRecord;
|
|
type HookLike = StoredHook | HookRecord;
|
|
@@ -66,11 +80,17 @@ export function computeHookDiagnostics(params: {
|
|
|
&& distance > halfLife;
|
|
&& distance > halfLife;
|
|
|
|
|
|
|
|
// Blocked: depends_on references a hook that is unplanted or unresolved.
|
|
// Blocked: depends_on references a hook that is unplanted or unresolved.
|
|
|
|
|
+ // Hotfix 3: also track per-upstream reference chapter so we can compute
|
|
|
|
|
+ // how long this hook has been blocked (for reviewer threshold eval).
|
|
|
const missingUpstream: string[] = [];
|
|
const missingUpstream: string[] = [];
|
|
|
|
|
+ const upstreamReferenceChapters: number[] = [];
|
|
|
for (const upstreamId of hook.dependsOn ?? []) {
|
|
for (const upstreamId of hook.dependsOn ?? []) {
|
|
|
const upstream = byId.get(upstreamId);
|
|
const upstream = byId.get(upstreamId);
|
|
|
if (!upstream) {
|
|
if (!upstream) {
|
|
|
missingUpstream.push(upstreamId);
|
|
missingUpstream.push(upstreamId);
|
|
|
|
|
+ // Upstream missing from ledger entirely — we've been blocked since
|
|
|
|
|
+ // our own planting (or since chapter 0 if we're a pre-planting seed).
|
|
|
|
|
+ upstreamReferenceChapters.push(plantedChapter);
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
// Upstream is "planted but not delivered" if startChapter is non-zero
|
|
// Upstream is "planted but not delivered" if startChapter is non-zero
|
|
@@ -84,16 +104,32 @@ export function computeHookDiagnostics(params: {
|
|
|
// For a pure "must be planted first" relationship, "planted but not
|
|
// For a pure "must be planted first" relationship, "planted but not
|
|
|
// resolved" still counts as blocking the downstream from firing.
|
|
// resolved" still counts as blocking the downstream from firing.
|
|
|
missingUpstream.push(upstreamId);
|
|
missingUpstream.push(upstreamId);
|
|
|
|
|
+ // Blocked since upstream planting (if planted) or since our own
|
|
|
|
|
+ // planting (if upstream still unplanted).
|
|
|
|
|
+ const referenceChapter = upstreamPlanted
|
|
|
|
|
+ ? upstream.startChapter
|
|
|
|
|
+ : plantedChapter;
|
|
|
|
|
+ upstreamReferenceChapters.push(referenceChapter);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
const blocked = missingUpstream.length > 0 && !isResolved(hook);
|
|
const blocked = missingUpstream.length > 0 && !isResolved(hook);
|
|
|
|
|
|
|
|
|
|
+ // Blocked distance = chapters elapsed since the *earliest* reference
|
|
|
|
|
+ // chapter among unresolved upstream. Picking the min reference yields the
|
|
|
|
|
+ // max distance — the longest any of these gates has held us up.
|
|
|
|
|
+ let blockedDistance = 0;
|
|
|
|
|
+ if (blocked && upstreamReferenceChapters.length > 0) {
|
|
|
|
|
+ const earliestReference = Math.min(...upstreamReferenceChapters);
|
|
|
|
|
+ blockedDistance = Math.max(0, currentChapter - earliestReference);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
result.set(hook.hookId, {
|
|
result.set(hook.hookId, {
|
|
|
stale,
|
|
stale,
|
|
|
blocked,
|
|
blocked,
|
|
|
missingUpstream,
|
|
missingUpstream,
|
|
|
distance,
|
|
distance,
|
|
|
halfLife,
|
|
halfLife,
|
|
|
|
|
+ blockedDistance,
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -116,9 +152,17 @@ export function renderHookDiagnosticMarker(
|
|
|
}
|
|
}
|
|
|
if (diagnostics.blocked) {
|
|
if (diagnostics.blocked) {
|
|
|
const missing = diagnostics.missingUpstream.join(", ");
|
|
const missing = diagnostics.missingUpstream.join(", ");
|
|
|
|
|
+ // Hotfix 3: embed the blocked distance so reviewer can apply its 5/6-chapter
|
|
|
|
|
+ // threshold without guessing. Token format is load-bearing — it's read by
|
|
|
|
|
+ // the reviewer prompt verbatim.
|
|
|
|
|
+ const distanceToken = diagnostics.blockedDistance > 0
|
|
|
|
|
+ ? (language === "en"
|
|
|
|
|
+ ? ` (blocked ${diagnostics.blockedDistance} chapters)`
|
|
|
|
|
+ : ` (已阻 ${diagnostics.blockedDistance} 章)`)
|
|
|
|
|
+ : "";
|
|
|
tokens.push(language === "en"
|
|
tokens.push(language === "en"
|
|
|
- ? `blocked on ${missing}`
|
|
|
|
|
- : `受阻于 ${missing}`);
|
|
|
|
|
|
|
+ ? `blocked on ${missing}${distanceToken}`
|
|
|
|
|
+ : `受阻于 ${missing}${distanceToken}`);
|
|
|
}
|
|
}
|
|
|
return tokens.join("; ");
|
|
return tokens.join("; ");
|
|
|
}
|
|
}
|