Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53b010a92e | ||
|
|
1fbbf6fd0c |
No files matched your search
@@ -0,0 +1,126 @@
|
||||
# Session State
|
||||
|
||||
**Last Updated:** 2026-04-29 (overnight + early-morning autonomous run, paused for cross-machine handoff)
|
||||
|
||||
## TL;DR for the next session (any machine)
|
||||
|
||||
`feature/issue-30-warn-unknown-tags` is open with WIP code that needs parser tests added before it ships. Everything else is clean. PRs #65 (#17), #66 (#15), #67 (#31), #68 (#16), #69 (#5), #70 (#33) all merged to develop. Two backlog items remain in the original "fix them all" pass: **#30 (in progress, WIP committed)** and **#14 maxTokens cap (not started)**.
|
||||
|
||||
## What shipped overnight (all merged to develop)
|
||||
|
||||
| Issue | PR | Subject |
|
||||
|---|---|---|
|
||||
| #17 | #65 | SSE error events surfaced across all providers |
|
||||
| #15 | #66 | Retry on 429 / transient 5xx with backoff |
|
||||
| #31 | #67 | `.state.json.bak` snapshot before overwrite |
|
||||
| #16 | #68 | Preserve whitespace in LLM responses |
|
||||
| #5 | #69 | Correct dialogue regex; support all quote styles |
|
||||
| #33 | #70 | Broaden detectCharacters regex (two-word, hyphen, apostrophe, all-caps names) |
|
||||
|
||||
## Active branch: `feature/issue-30-warn-unknown-tags`
|
||||
|
||||
Current commit: `1fbbf6f` (marked WIP). Adds `parseAnnotationsWithWarnings()` to `src/parser.ts`, plus `ParserWarning` / `ParsedAnnotations` types, and seeds `flags` in `pipeline.ts` from parser warnings so unknown tags / empty instructions / unclosed multi-line blocks surface in the review note's "Flags for Author" section.
|
||||
|
||||
`src/types.ts` extends `ReviewFlag.type` with three new variants:
|
||||
- `unknown_tag`
|
||||
- `empty_instruction`
|
||||
- `unclosed_annotation`
|
||||
|
||||
### What's still needed before this can ship
|
||||
|
||||
Coverage gates currently fail. Floors are 91 / 90 / 97 / 79 (lines / statements / functions / branches); current measurement is 90.77 / 89.7 / 96.96 / 78.27. Per-file `src/parser.ts` is at lines 79.36% (floor 88) and functions 92.85% (floor 100). The new code paths are untested.
|
||||
|
||||
**Add to `test/parser.test.ts`:**
|
||||
|
||||
```ts
|
||||
import { parseAnnotationsWithWarnings } from "../src/parser";
|
||||
|
||||
describe("parseAnnotationsWithWarnings", () => {
|
||||
it("warns on unknown tag", () => {
|
||||
const { annotations, warnings } = parseAnnotationsWithWarnings("%% REWIRTE: foo %%");
|
||||
expect(annotations).toHaveLength(0);
|
||||
expect(warnings).toHaveLength(1);
|
||||
expect(warnings[0]).toMatchObject({ type: "unknown_tag", line: 1 });
|
||||
expect(warnings[0].description).toContain("REWIRTE");
|
||||
});
|
||||
|
||||
it("warns on empty instruction", () => {
|
||||
const { warnings } = parseAnnotationsWithWarnings("%% REWRITE: %%");
|
||||
expect(warnings).toHaveLength(1);
|
||||
expect(warnings[0]).toMatchObject({ type: "empty_instruction", line: 1 });
|
||||
});
|
||||
|
||||
it("warns on unclosed multi-line annotation", () => {
|
||||
const { warnings } = parseAnnotationsWithWarnings("%% REWRITE: open\nfoo\nbar");
|
||||
expect(warnings).toHaveLength(1);
|
||||
expect(warnings[0]).toMatchObject({ type: "unclosed_annotation", line: 1 });
|
||||
});
|
||||
|
||||
it("returns empty warnings when all annotations are valid", () => {
|
||||
const content = '%% REWRITE: tighten this %%\n%% TONE: lighter %%';
|
||||
const { annotations, warnings } = parseAnnotationsWithWarnings(content);
|
||||
expect(annotations.length).toBeGreaterThan(0);
|
||||
expect(warnings).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("returns valid annotations alongside warnings for invalid ones", () => {
|
||||
const content = '%% REWRITE: keep this %%\n%% NOPE: drop this %%';
|
||||
const { annotations, warnings } = parseAnnotationsWithWarnings(content);
|
||||
expect(annotations).toHaveLength(1);
|
||||
expect(warnings).toHaveLength(1);
|
||||
expect(warnings[0].type).toBe("unknown_tag");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
After tests pass: amend the WIP commit (or new commit), push, open PR.
|
||||
|
||||
## Remaining backlog from the "fix them all" pass
|
||||
|
||||
- [ ] **#30** Warn on unknown annotation tags — WIP on `feature/issue-30-warn-unknown-tags`, needs the tests above.
|
||||
- [ ] **#14** Cap `maxTokens` against per-model output limits — not started. Plan: extend `ModelInfo.maxOutputTokens`, populate for ANTHROPIC_MODELS / GOOGLE_MODELS / OPENAI_MODELS, clamp in `pipeline.ts:299` (`maxTokens: Math.min(settings.maxTokens, modelInfo?.maxOutputTokens ?? settings.maxTokens)`), and log when clamping happens.
|
||||
|
||||
## Backlog NOT touched (need user direction)
|
||||
|
||||
These need UX or architectural decisions:
|
||||
|
||||
- **#46** Rollback command — UX choice: do we delete intermediate version files, or just rewind `.version`?
|
||||
- **#9** Parallel annotations — concurrency tradeoffs: shared rate-limit bucket vs. per-provider concurrency cap. Now unblocked by #15 retry.
|
||||
- **#13** Dry-run cost preview — modal UX choice.
|
||||
- **#7** `commands.ts` refactor — large architectural change; defer until time permits.
|
||||
- **47 parked items in #62** — community-store gating, CI hygiene, forward-leaning features.
|
||||
|
||||
## Tier overview after this run
|
||||
|
||||
| Tier | Done | Open |
|
||||
|---|---|---|
|
||||
| Tier 1 (block real writing) | #18, #17, #15 | #46 rollback |
|
||||
| Tier 2 (cost & speed) | #16 | #13, #9, #12, #14 |
|
||||
| Tier 3 (correctness drift) | #5, #31, #33 | **#30 (WIP)**, #14 |
|
||||
|
||||
## Resume instructions on another machine
|
||||
|
||||
```bash
|
||||
cd ~/Git.Local/kof22/obsidian-penny # or wherever you keep it
|
||||
git fetch origin
|
||||
git checkout feature/issue-30-warn-unknown-tags
|
||||
git pull
|
||||
npm install # in case lockfile bumped on the other machine
|
||||
npm run check # confirm the coverage failure described above
|
||||
```
|
||||
|
||||
Then add the tests in `test/parser.test.ts` per the snippet above, run `npm run check`, amend `1fbbf6f` (or add a new commit), push, open PR.
|
||||
|
||||
## Coverage thresholds (current floors on develop)
|
||||
|
||||
- `vitest.config.ts`: lines 91 / statements 90 / functions 97 / branches 79
|
||||
- `.munitor.yml`: `min_instruction: 90`
|
||||
- Per-file ratchets locked in vitest.config.ts: `node-stream.ts` lines 96, etc.
|
||||
|
||||
## Key Reference
|
||||
|
||||
- **Audit tracker (master):** https://github.com/KofTwentyTwo/obsidian-penny/issues/62
|
||||
- **Recent PRs:** #65–#70 all merged to develop
|
||||
- **Open PRs:** none currently. PR #66's session-state file may still be uncommitted on the prior machine.
|
||||
- **Local dev loop:** `cd /Users/james.maes/Git.Local/kof22/obsidian-penny && npm run dev` — Hot Reload picks up bundle changes automatically
|
||||
- **Books vault plugin path:** `/Users/james.maes/Git.Local/kof22/Books/.obsidian/plugins/penny/` (three files are symlinks → repo root)
|
||||
@@ -0,0 +1,38 @@
|
||||
# TODO
|
||||
|
||||
Personal-use sprint — issues that block daily writing use of PENNY.
|
||||
Full audit list and tracker: [#62](https://github.com/KofTwentyTwo/obsidian-penny/issues/62).
|
||||
|
||||
## Tier 1 — Will bite during real sessions
|
||||
|
||||
- [x] **#18** LLM call timeout (PR #64 merged)
|
||||
- [x] **#17** SSE error events surfaced (PR #65, ready to merge)
|
||||
- [ ] **#15** Retry on 429 / transient 5xx with `Retry-After` + exponential backoff
|
||||
- [ ] **#46** Roll-back command (revert to a previous chapter version cleanly)
|
||||
|
||||
## Tier 2 — Cost & speed quality of life
|
||||
|
||||
- [ ] **#13** Dry-run cost preview + confirmation gate above threshold
|
||||
- [ ] **#9** Process annotations in parallel (configurable concurrency) — *depends on #15 retry*
|
||||
- [ ] **#12** Multi-line annotation cap surfaces warning instead of silent truncate
|
||||
- [ ] **#16** Remove provider `.trim()` — preserve meaningful whitespace
|
||||
|
||||
## Tier 3 — Correctness drift
|
||||
|
||||
- [ ] **#5** Fix dialogue regex (single-quoted/British dialogue currently uncounted)
|
||||
- [ ] **#30** Warn on unknown annotation tags (typo'd `%% REWIRTE: ... %%` is silently dropped)
|
||||
- [ ] **#33** `detectCharacters` regex misses two-word names, apostrophes, all-caps shouted dialogue
|
||||
- [ ] **#31** Write `.bak` snapshot of `.state.json` before overwrite (recovery)
|
||||
- [ ] **#14** Cap `maxTokens` against per-model output limits (avoid opaque 400s)
|
||||
|
||||
## Out of scope for this sprint
|
||||
|
||||
The remaining 47 audit-derived issues are tracked in [#62](https://github.com/KofTwentyTwo/obsidian-penny/issues/62). They cover community-store gating (#4 quotes legal, #8 inline styles, #19 API key warnings, #26 mobile guard), the big `commands.ts` refactor (#7), CI hygiene (#40, #43), and forward-leaning features. Pick up after this sprint stabilizes.
|
||||
|
||||
## Workflow notes
|
||||
|
||||
- Branch from `develop` as `feature/issue-XX-short-name`.
|
||||
- TDD: write a failing test first, prove it fails on `develop`, then fix.
|
||||
- Each PR ratchets `vitest.config.ts` per-file thresholds up to lock in coverage gains.
|
||||
- `npm run check` mirrors CI: typecheck + tests + coverage gate.
|
||||
- Hot Reload watches `Books/.obsidian/plugins/penny/main.js` (symlinked to repo root); `npm run dev` keeps the bundle fresh on save.
|
||||
+66
-3
@@ -21,10 +21,24 @@
|
||||
import {
|
||||
type AnnotatedSection,
|
||||
type AnnotationTag,
|
||||
type ReviewFlag,
|
||||
ACTIONABLE_TAGS,
|
||||
ALL_TAGS,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Parser warnings raised during `parseAnnotationsWithWarnings`. Surfaced in
|
||||
* the review note's "Flags for Author" section so authors notice typo'd tags
|
||||
* (e.g. `REWIRTE`) and unclosed multi-line annotations that would otherwise
|
||||
* be silently dropped (issue #30).
|
||||
*/
|
||||
export type ParserWarning = ReviewFlag;
|
||||
|
||||
export interface ParsedAnnotations {
|
||||
annotations: AnnotatedSection[];
|
||||
warnings: ParserWarning[];
|
||||
}
|
||||
|
||||
/** Single-line annotation: `%% TAG: instruction %%` on one line. Captures tag and instruction. */
|
||||
const ANNOTATION_PATTERN = /%%\s*([A-Z]+)\s*:\s*(.*?)\s*%%/;
|
||||
|
||||
@@ -204,8 +218,19 @@ function resolveScope(
|
||||
* silently skipped.
|
||||
*/
|
||||
export function parseAnnotations(content: string): AnnotatedSection[] {
|
||||
return parseAnnotationsWithWarnings(content).annotations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same parse as `parseAnnotations`, but also returns warnings about malformed
|
||||
* annotations the parser dropped: unknown tags (typos), empty instructions,
|
||||
* and unclosed multi-line blocks. Surface these in the review note so the
|
||||
* author sees them instead of silently losing the annotation (issue #30).
|
||||
*/
|
||||
export function parseAnnotationsWithWarnings(content: string): ParsedAnnotations {
|
||||
const lines = content.split("\n");
|
||||
const results: AnnotatedSection[] = [];
|
||||
const warnings: ParserWarning[] = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
@@ -219,8 +244,21 @@ export function parseAnnotations(content: string): AnnotatedSection[] {
|
||||
const tag = match[1];
|
||||
const instruction = match[2].trim();
|
||||
|
||||
// Skip malformed: unrecognised tag or empty instruction.
|
||||
if (!isValidTag(tag) || instruction.length === 0) {
|
||||
// Surface malformed annotations as warnings instead of silently dropping.
|
||||
if (!isValidTag(tag)) {
|
||||
warnings.push({
|
||||
type: "unknown_tag",
|
||||
line: i + 1,
|
||||
description: `Unknown annotation tag "${tag}" -- did you mean one of: ${ALL_TAGS.slice(0, 6).join(", ")}, ...?`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (instruction.length === 0) {
|
||||
warnings.push({
|
||||
type: "empty_instruction",
|
||||
line: i + 1,
|
||||
description: `Empty instruction for tag ${tag}; nothing to process`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -283,6 +321,29 @@ export function parseAnnotations(content: string): AnnotatedSection[] {
|
||||
}
|
||||
|
||||
const instruction = instructionParts.join(" ").trim();
|
||||
// Detect unclosed multi-line annotations: closeLine is still i when
|
||||
// no closing %% was found within the 20-line scan window.
|
||||
if (closeLine === i) {
|
||||
warnings.push({
|
||||
type: "unclosed_annotation",
|
||||
line: i + 1,
|
||||
description: `Unclosed multi-line annotation opened with %% ${tag}: -- missing closing %% within 20 lines`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!isValidTag(tag)) {
|
||||
warnings.push({
|
||||
type: "unknown_tag",
|
||||
line: i + 1,
|
||||
description: `Unknown annotation tag "${tag}" -- did you mean one of: ${ALL_TAGS.slice(0, 6).join(", ")}, ...?`,
|
||||
});
|
||||
} else if (instruction.length === 0) {
|
||||
warnings.push({
|
||||
type: "empty_instruction",
|
||||
line: i + 1,
|
||||
description: `Empty instruction for tag ${tag}; nothing to process`,
|
||||
});
|
||||
}
|
||||
if (isValidTag(tag) && instruction.length > 0) {
|
||||
// Use the line AFTER the closing %% as the annotation target
|
||||
const annotationLineIndex = closeLine;
|
||||
@@ -325,5 +386,7 @@ export function parseAnnotations(content: string): AnnotatedSection[] {
|
||||
|
||||
// Sort by lineStart ascending (stable).
|
||||
results.sort((a, b) => a.lineStart - b.lineStart);
|
||||
return results;
|
||||
// Sort warnings by line so the review note presents them top-down.
|
||||
warnings.sort((a, b) => a.line - b.line);
|
||||
return { annotations: results, warnings };
|
||||
}
|
||||
+11
-4
@@ -25,7 +25,7 @@ import type { ContextFiles } from "./context";
|
||||
import type { CompletionRequest, CompletionResponse } from "./providers/service";
|
||||
import type { RouteConfig, ComplexityTier } from "./providers/router";
|
||||
import { TAG_COMPLEXITY } from "./providers/router";
|
||||
import { parseAnnotations } from "./parser";
|
||||
import { parseAnnotations, parseAnnotationsWithWarnings } from "./parser";
|
||||
import { assembleContext } from "./context";
|
||||
import { buildPrompt, callProvider } from "./drafter";
|
||||
import { assembleNewVersion } from "./assembler";
|
||||
@@ -176,8 +176,12 @@ export async function runPipeline(input: PipelineInput): Promise<PipelineResult
|
||||
const { content, settings, chapterId, bookId } = input;
|
||||
const onProgress = input.onProgress;
|
||||
|
||||
// (a) Parse annotations (use pre-parsed if provided to avoid redundant work)
|
||||
const allAnnotations = input.preParsedAnnotations ?? parseAnnotations(content);
|
||||
// (a) Parse annotations (use pre-parsed if provided to avoid redundant work).
|
||||
// Always also collect parser warnings (unknown tags, empty instructions,
|
||||
// unclosed multi-line annotations) so they surface in the review note.
|
||||
const parsed = parseAnnotationsWithWarnings(content);
|
||||
const allAnnotations = input.preParsedAnnotations ?? parsed.annotations;
|
||||
const parserWarnings = parsed.warnings;
|
||||
|
||||
// (b) Read version and state
|
||||
const currentVersion = readVersion(input.versionContent);
|
||||
@@ -209,7 +213,10 @@ export async function runPipeline(input: PipelineInput): Promise<PipelineResult
|
||||
// (e) Process each annotation via the LLM provider
|
||||
const revisions: Array<{ annotation: AnnotatedSection; revisedText: string }> = [];
|
||||
const changes: AnnotationChange[] = [];
|
||||
const flags: ReviewFlag[] = [];
|
||||
// Seed the review flags with any parser warnings (issue #30) so authors
|
||||
// see typo'd tags / empty instructions / unclosed blocks they would have
|
||||
// otherwise lost silently.
|
||||
const flags: ReviewFlag[] = [...parserWarnings];
|
||||
const processedTags: string[] = [];
|
||||
let hadErrors = false;
|
||||
let completedCount = 0;
|
||||
|
||||
+8
-1
@@ -507,7 +507,14 @@ export interface AnnotationChange {
|
||||
*/
|
||||
export interface ReviewFlag {
|
||||
/** Category of the flag. */
|
||||
type: "research" | "plot" | "vague" | "error";
|
||||
type:
|
||||
| "research"
|
||||
| "plot"
|
||||
| "vague"
|
||||
| "error"
|
||||
| "unknown_tag"
|
||||
| "empty_instruction"
|
||||
| "unclosed_annotation";
|
||||
/** Line number associated with the flag. */
|
||||
line: number;
|
||||
/** Human-readable description of what needs attention. */
|
||||
|
||||
Reference in new issue
Block a user