Skip to content

Commit 2bafec0

Browse files
committed
Remove lower-priority figure placeholder rule
1 parent 46dad10 commit 2bafec0

14 files changed

Lines changed: 89 additions & 89 deletions

SKILL.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,13 @@ Local-library-first rule (applies only when the Zotero check above succeeds):
160160
- The note must include figure/table placeholders for all major visuals rather than silently skipping them.
161161
- Every kept figure/table placeholder must appear directly under the most relevant analytical section named by its `建议位置`; do not collect unresolved placeholders in catch-all sections such as `剩余图表占位` or `Remaining figures`.
162162
- Every kept figure/table placeholder must use the standard `> [!figure]` callout format with `建议位置`, `放置原因`, and `当前状态`; do not use ordinary paragraph markers such as `[图表占位 | Fig. 1]`, `图表占位:Table 2`, or `Figure Placeholder | Fig. 3`.
163-
- Real images may replace some placeholders, but only if they clearly match the corresponding paper figure/table.
163+
- Real images replace placeholders when they clearly match the corresponding paper figure/table and pass the visual-usability gate.
164164
- When inserting a real image, use the `relative_markdown_embed` from `figure_table_decisions.json`; final save with `scripts/write_obsidian_note.py --figure-decisions ...` copies the image into the paper-local `images/` directory.
165165
- When a real image is inserted, render it as the Obsidian embed or Markdown image embed followed immediately by one italic caption line.
166166
- Do not keep a redundant `> [!figure]` placeholder callout for the same inserted real figure.
167167
- Figure captions in the note must preserve the original paper numbering such as `Fig. 1` or `Table 2`.
168-
- If a figure/table candidate is marked usable, resolve it explicitly: insert the real image, keep the placeholder because of a concrete visual defect, keep it as lower-priority because a more central related figure is already inserted, or keep it because materialization failed.
168+
- If a figure/table candidate is marked usable and has a real image path, insert the real image. Do not keep a placeholder merely because the figure/table is lower priority, supplemental, already summarized in text, or less central than another inserted figure.
169+
- A kept placeholder is valid only when the image cannot be safely inserted because of a concrete visual defect, missing candidate, unresolved visual review, identity mismatch, contamination, or materialization/copy/write failure.
169170
- For `usable_candidate` or `needs_visual_quality_check` / `review` candidates, make the visual decision only after inspecting the actual candidate image file exposed by the pipeline. Record the concrete visual observation behind the decision. Do not claim manual visual review, visual inspection, or "no reliable insertable candidate" unless the candidate image was actually opened and inspected.
170171
- `reject_visual_quality` and `asset_candidate_missing` are fail-closed script states. They do not require manual visual review before keeping a placeholder or skipping insertion; treat them as automatic extraction outcomes unless you explicitly inspect or re-extract the source asset.
171172
- Do not misreport missing candidates as materialization failures: `asset_candidate_missing`, empty `source_image_path`, or no independent crop means the placeholder status should say no high-confidence image candidate was extracted. Use materialization/copy/write failure language only after a real chosen image asset failed to copy or write.

references/figure-placement.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,14 @@ Do not let scripts make the final semantic choice; scripts should only prepare c
4747

4848
## Usable Candidate Decision Contract
4949

50-
`usable_candidate` means the candidate is eligible for final judgment; it is not an automatic insertion command.
50+
`usable_candidate` means the pipeline found a candidate that is visually eligible to insert.
5151
For every usable figure/table candidate, resolve the final note into one of these states:
5252
- `insert`: materialize the image and replace the placeholder with the real image plus one italic caption line
5353
- `kept_placeholder_visual_defect`: keep the placeholder because manual review found a concrete visual defect, such as contamination, truncation, missing table body, partial subfigure, or caption loss
54-
- `kept_placeholder_lower_priority`: keep the placeholder because a more central or more direct related figure/table is already inserted
5554
- `kept_placeholder_materialization_blocked`: keep the placeholder because `materialize_figure_asset.py` or file copy/write permission failed
5655

56+
Do not keep a usable candidate as a placeholder merely because it is lower priority, supplemental, already summarized in text, or less central than another inserted figure/table. If a usable candidate is important enough to appear as a callout in the final note, insert the real image. If it is not important enough to appear, omit it or summarize it in prose rather than leaving a placeholder.
57+
5758
For `usable_candidate` and `needs_visual_quality_check` / `review` candidates, final visual judgment requires opening and inspecting the actual candidate image file.
5859
Do not write that manual visual review found no reliable insertable candidate unless that inspection actually happened.
5960
If no manual inspection happened, name the state as an unresolved visual review requirement or an automatic script outcome, not as a reviewed visual defect.
@@ -63,7 +64,7 @@ Keep missing-candidate cases separate from materialization failures:
6364
- reserve `materialization blocked` only for a real chosen image asset that failed during `materialize_figure_asset.py`, final copy, permission, or `write_obsidian_note.py`
6465
- if a crop includes another Figure/Table caption or another figure body, treat it as a visual defect or missing independent candidate; do not call it usable just because it contains the target label
6566

66-
`plan_figure_table_decisions.py` may preselect high-priority usable figure crops as `insert`.
67+
`plan_figure_table_decisions.py` preselects planned usable figure/table crops as `insert`.
6768
That does not mean `run_pipeline.py` writes into the vault; materialization happens at the final save step, where `write_obsidian_note.py --figure-decisions ...` copies the selected images and refuses a note that does not reference the selected image path.
6869

6970
Do not use soft reasons such as keeping the note light, values already transcribed, future lookup, or convenient back-reference as the standalone reason for keeping a usable candidate as a placeholder.

references/final-writing.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,9 @@ If the bundle contains candidate figure pages or candidate image files:
242242
- still make the final decision yourself rather than trusting the candidate ranking blindly
243243
- for `usable_candidate` or `needs_visual_quality_check` / `review`, make that final decision only after opening and inspecting the actual candidate image file; do not say manual visual review found no reliable candidate unless that inspection actually happened
244244
- treat `reject_visual_quality` and `asset_candidate_missing` as automatic fail-closed script outcomes that do not require manual visual review
245-
- if a candidate is usable, resolve it explicitly as inserted, kept for a concrete visual defect, kept as lower-priority because a more central related figure is already inserted, or kept because materialization/copy/write failed
245+
- if a candidate is usable and has a real image path, insert it
246+
- do not keep a usable candidate as a placeholder merely because it is lower priority, supplemental, already summarized in text, or less central than another inserted figure/table
247+
- keep a placeholder only when there is a concrete visual defect, missing candidate, unresolved visual review, identity mismatch, contamination, or materialization/copy/write failure
246248
- never describe a missing image asset, empty `source_image_path`, `asset_candidate_missing`, or absent independent crop as a materialization/copy failure
247249
- if the crop contains a different Figure/Table caption or another figure body, describe that as contamination/visual defect or lack of an independent crop, not as a usable clean candidate
248250
- do not keep a usable candidate as a placeholder only because the note should stay light, the values were transcribed, the figure can be checked later, or it is convenient as a back-reference

references/workflow.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ For a normal single-paper note request, the pipeline below is a required executi
123123
8. `plan_figure_table_decisions`
124124
Build a decision row for every detected figure/table caption.
125125
Each item should have one explicit state: `insert`, `placeholder`, `low_priority`, `visual_defect`, or `skip` with reason.
126-
High-priority usable figure crops may be marked `insert`, but this is still a final-save obligation, not a pipeline-time vault write.
126+
Planned usable figure/table crops with real image paths should be marked `insert`; this is still a final-save obligation, not a pipeline-time vault write.
127127
Completion condition:
128128
- every major detected figure/table has a recorded decision or skip reason
129129

@@ -163,7 +163,7 @@ For a normal single-paper note request, the pipeline below is a required executi
163163
Completion condition:
164164
- every substantive planned section cites either a valid `section_id` or a valid page range
165165
- every substantive planned section has a paper-specific `focus`/reading goal, not only a valid anchor
166-
- high-priority usable figure candidates are not left as vague placeholders
166+
- usable figure/table candidates with real image paths are not left as vague placeholders or low-priority placeholders
167167
- old broad bundle references such as `synthesis_bundle.evidence.method_evidence` are rejected
168168
- truncated source text blocks full-read drafting unless partial reading was explicitly accepted
169169

@@ -240,8 +240,8 @@ For a normal single-paper note request, the pipeline below is a required executi
240240
- create a new domain only when no existing domain fits well
241241
- do not save directly into the bare papers root
242242
Complete the figure decision before this step:
243-
- replace high-confidence placeholders with real images
244-
- keep lower-confidence items as placeholders
243+
- replace usable matched placeholders with real images
244+
- keep lower-confidence, missing, contaminated, or blocked items as placeholders
245245
- pass the figure decision table to `write_obsidian_note.py --figure-decisions ...` so `insert` rows are copied into the paper-local `images/` folder and must be referenced by the final Markdown
246246
- do not split text writing and figure handling into two separate user turns by default
247247
If the configured vault or its paper-local `images/` directory cannot currently be written:

scripts/build_synthesis_bundle.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ def compact_writing_contract() -> dict:
274274
depth_requirements["generic_focus_phrases"]
275275
)
276276
usable_insert_candidate = dict(WRITING_CONTRACT_RULES["usable_insert_candidate"])
277-
usable_insert_candidate["plan_kinds"] = list(usable_insert_candidate["plan_kinds"])
277+
usable_insert_candidate["kinds"] = list(usable_insert_candidate["kinds"])
278278
analysis_coverage = dict(WRITING_CONTRACT_RULES["analysis_coverage_contract"])
279279
analysis_coverage["central_claim_fields"] = list(
280280
analysis_coverage["central_claim_fields"]

scripts/contracts.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -278,14 +278,12 @@
278278
),
279279
"figure_decision_values": ("insert", "placeholder", "low_priority", "visual_defect", "skip"),
280280
"usable_insert_candidate": {
281-
"kind": "figure",
282-
"max_priority": 2,
281+
"kinds": ("figure", "table"),
283282
"visual_quality_status": "usable_candidate",
284-
"plan_kinds": ("method_overview", "data_or_task_overview", "main_result"),
283+
"requires_source_image_path": True,
285284
},
286285
"allowed_usable_placeholder_reasons": (
287286
"visual_defect",
288-
"lower_priority_related_inserted",
289287
"materialization_blocked",
290288
),
291289
"manual_visual_review_required_statuses": (

scripts/lint_grounding.py

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -340,11 +340,7 @@ def validate_figure_decisions(
340340
return [issue("figure_table_decisions_invalid")]
341341
valid_decisions = set(WRITING_CONTRACT_RULES["figure_decision_values"])
342342
usable_insert = WRITING_CONTRACT_RULES["usable_insert_candidate"]
343-
max_insert_priority = int(usable_insert["max_priority"])
344-
auto_insert_plan_kinds = set(usable_insert.get("plan_kinds", ()))
345-
allowed_placeholder_reasons = set(
346-
WRITING_CONTRACT_RULES["allowed_usable_placeholder_reasons"]
347-
)
343+
insertable_kinds = set(usable_insert.get("kinds", ()))
348344
decision_ids = {
349345
normalize_whitespace(
350346
str(item.get("source_id") or item.get("label") or item.get("item_id") or "")
@@ -397,29 +393,21 @@ def validate_figure_decisions(
397393
source_id=item.get("source_id") or item.get("label") or "",
398394
)
399395
)
400-
try:
401-
priority = int(item.get("priority", 99) or 99)
402-
except (TypeError, ValueError):
403-
priority = 99
404396
is_required_insert_candidate = (
405-
normalize_whitespace(str(item.get("kind", ""))) == usable_insert["kind"]
397+
normalize_whitespace(str(item.get("kind", ""))) in insertable_kinds
406398
and normalize_whitespace(str(item.get("visual_quality_status", "")))
407399
== usable_insert["visual_quality_status"]
408-
and priority <= max_insert_priority
400+
and normalize_whitespace(str(item.get("source_image_path", "")))
409401
)
410-
plan_kind = normalize_whitespace(str(item.get("plan_kind", "")))
411-
if plan_kind not in auto_insert_plan_kinds:
412-
is_required_insert_candidate = False
413-
if is_required_insert_candidate and decision == "placeholder":
402+
if is_required_insert_candidate and decision != "insert":
414403
skip_reason = normalize_whitespace(str(item.get("skip_reason", "")))
415-
if skip_reason not in allowed_placeholder_reasons:
416-
issues.append(
417-
issue(
418-
"usable_insert_candidate_left_placeholder",
419-
source_id=item.get("source_id") or item.get("label") or "",
420-
skip_reason=skip_reason,
421-
)
404+
issues.append(
405+
issue(
406+
"usable_insert_candidate_left_placeholder",
407+
source_id=item.get("source_id") or item.get("label") or "",
408+
skip_reason=skip_reason,
422409
)
410+
)
423411
return issues
424412

425413

scripts/lint_note.py

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@
117117
|
118118
(?:图像|图片|表格|图|表)?\s*裁剪[^。;,\n>]{0,12}(?<!不)(?:可用|可读|清晰)
119119
|
120+
(?:图像|图片|表格|图|表)[^。;,\n>]{0,12}(?<!不)(?:可用|可读|清晰)
121+
|
122+
图号\s*匹配
123+
|
120124
匹配度\s*高
121125
|
122126
高\s*置信(?:度)?[^。;,\n>]{0,12}候选
@@ -150,24 +154,6 @@
150154
flags=re.IGNORECASE | re.VERBOSE,
151155
)
152156

153-
USABLE_CANDIDATE_LOWER_PRIORITY_RE = re.compile(
154-
r"""
155-
(?:
156-
低优先级|优先级较低|次要|补充性
157-
|
158-
(?:已插入|已有|already\s+inserted)[^。;\n]{0,40}
159-
(?:更核心|更直接|低优先级|同一(?:机制|概念|结论|任务|流程))
160-
|
161-
(?:Fig(?:ure)?|Table|图|表)\s*[\w.-]*[^。;\n]{0,40}(?:更核心|更直接)
162-
|
163-
lower[-\s]*priority
164-
|
165-
more\s+(?:central|direct|core)
166-
)
167-
""",
168-
flags=re.IGNORECASE | re.VERBOSE,
169-
)
170-
171157
USABLE_CANDIDATE_MATERIALIZATION_BLOCKED_RE = re.compile(
172158
r"""
173159
(?:
@@ -625,14 +611,9 @@ def figure_status_text(line: str) -> str:
625611
return stripped.removeprefix("> 当前状态:").strip()
626612

627613

628-
def has_lower_priority_placeholder_reason(status_text: str) -> bool:
629-
return bool(USABLE_CANDIDATE_LOWER_PRIORITY_RE.search(status_text))
630-
631-
632614
def has_accepted_usable_placeholder_reason(status_text: str) -> bool:
633615
return bool(
634616
USABLE_CANDIDATE_VISUAL_DEFECT_RE.search(status_text)
635-
or USABLE_CANDIDATE_LOWER_PRIORITY_RE.search(status_text)
636617
or USABLE_CANDIDATE_MATERIALIZATION_BLOCKED_RE.search(status_text)
637618
)
638619

@@ -773,7 +754,7 @@ def figure_callout_real_image_status_issues(text: str) -> list[dict[str, object]
773754
if not nxt.startswith(">"):
774755
break
775756
status_text = figure_status_text(nxt)
776-
if status_text and REAL_IMAGE_STATUS_RE.search(nxt) and not has_lower_priority_placeholder_reason(status_text):
757+
if status_text and REAL_IMAGE_STATUS_RE.search(nxt):
777758
issues.append(
778759
{
779760
"line_number": j + 1,

scripts/plan_figure_table_decisions.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616
)
1717

1818
DECISION_VALUES = {"insert", "placeholder", "low_priority", "visual_defect", "skip"}
19-
INSERTABLE_KIND = "figure"
20-
INSERTABLE_PRIORITY_MAX = 2
21-
AUTO_INSERT_PLAN_KINDS = {"method_overview", "data_or_task_overview", "main_result"}
19+
INSERTABLE_KINDS = {"figure", "table"}
2220

2321

2422
def parser() -> argparse.ArgumentParser:
@@ -137,16 +135,9 @@ def source_image_filename(plan_item: dict[str, Any]) -> str:
137135
def should_insert(caption: dict[str, Any], plan_item: dict[str, Any], status: str) -> bool:
138136
if status != "usable_candidate":
139137
return False
140-
if caption.get("kind") != INSERTABLE_KIND:
138+
if caption.get("kind") not in INSERTABLE_KINDS:
141139
return False
142-
plan_kind = normalize_whitespace(str(plan_item.get("kind", "")))
143-
if plan_kind not in AUTO_INSERT_PLAN_KINDS:
144-
return False
145-
try:
146-
priority = int(plan_item.get("priority", 99) or 99)
147-
except (TypeError, ValueError):
148-
priority = 99
149-
return priority <= INSERTABLE_PRIORITY_MAX and bool(source_image_path(plan_item))
140+
return bool(source_image_path(plan_item))
150141

151142

152143
def decide(caption: dict[str, Any], plan_item: dict[str, Any] | None) -> dict[str, Any]:
@@ -193,7 +184,7 @@ def decide(caption: dict[str, Any], plan_item: dict[str, Any] | None) -> dict[st
193184
else:
194185
base["decision"] = "placeholder"
195186
if status == "usable_candidate":
196-
base["skip_reason"] = "not_auto_insertable_by_kind_or_priority"
187+
base["skip_reason"] = "asset_candidate_missing"
197188
elif status:
198189
base["skip_reason"] = "visual_quality_requires_review"
199190
else:

tests/test_build_synthesis_bundle_contract.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,13 @@ def test_bundle_compact_writing_contract_keeps_depth_rules_without_old_bundle_fi
4848
),
4949
}
5050
assert contract["figure_table_contract"]["usable_insert_candidate"] == {
51-
"kind": WRITING_CONTRACT_RULES["usable_insert_candidate"]["kind"],
52-
"max_priority": WRITING_CONTRACT_RULES["usable_insert_candidate"]["max_priority"],
51+
"kinds": list(WRITING_CONTRACT_RULES["usable_insert_candidate"]["kinds"]),
5352
"visual_quality_status": WRITING_CONTRACT_RULES["usable_insert_candidate"][
5453
"visual_quality_status"
5554
],
56-
"plan_kinds": list(WRITING_CONTRACT_RULES["usable_insert_candidate"]["plan_kinds"]),
55+
"requires_source_image_path": WRITING_CONTRACT_RULES["usable_insert_candidate"][
56+
"requires_source_image_path"
57+
],
5758
}
5859
assert contract["figure_table_contract"]["manual_visual_review_required_statuses"] == list(
5960
WRITING_CONTRACT_RULES["manual_visual_review_required_statuses"]

0 commit comments

Comments
 (0)