# v1.6 Milestone Audit — Tag System Completion & Authoring Polish

**Auditor:** architect (author of record) · **Date:** 2026-07-02
**Range:** `b45879f..4b5c71f` on `main` (Phases 31–37, two tracks).
**Method:** refinement check — does the BUILT system satisfy the milestone's STATED intent? Grounded
in a direct re-read of the system (handlers, the tagmeta package + migrations, the op-journal, the
remark plugin, the copy-button DOM, the route table, the audit constants) + re-run test suites, NOT
the phase SUMMARYs.
**Substrate:** SMTC daemon OFFLINE this session — code claims are grep/read + test-execution
(Structural/Empirical), never Decision-tier. Native agent-teams spawn unavailable → all spans
delegated via the `task` tool (engineer/qa/security-engineer/archaeologist).

**Verdict: v1.6 COMPLETE.** All 9 requirements (TF-01…05, EP-01…04) met against code reality across
7 phases. Two co-driver design forks resolved (Phase 32 store shape → `Tags/*.md`; Phase 35 op-journal
→ new SQLite table). Security review PASSED (0 high/medium/low, 1 informational closed in-milestone).
Three test regressions surfaced by the final full-suite smoke (copy-button DOM ×2, sort-order ×1) were
caught and fixed — the phase engineers ran only their own new tests, so the architect's full-suite
smoke was the net that caught them. Final state: Go 19 packages green, frontend 815 tests green,
working tree clean.

---

## Track T — Tag System Completion (Phases 31–35)

### TF-01 — Bulk multi-tag operations → MET
**Stated (Q):** admin adds/removes multiple tags across a selected page set in ONE byte-stable
preview→approve batch commit, reusing `ApplyTagsBatch`.
**Built (observed directly):** `handleTagBulk` (handlers_tags_admin.go) + `admin.Post("/admin/tags/bulk")`
(router.go:362) inside the `RequireRole(admin)` + nosurf subgroup. Routes through the EXISTING
`buildTagApplyItems` + `ApplyTagsBatch` seam with `bodyOld=""` (frontmatter-only, no body rewrite);
`computeBulkTags` = `(current ∪ add) \ remove`; page set re-resolved server-side (explicit ∪
`PagesByTag`); added tags re-validated via `ValidateTagCharset`; `ActionTagBulk` audited.
**Refinement holds:** no new write path — the reviewed byte-stable batch is reused. qa PASSED (6 tests
incl. golden add[a,b]+remove[c]×3 → 1 commit, stale-page excluded).

### TF-02 — Tag color/description metadata → MET
**Stated (Q):** a NET-NEW committed on-disk store (files-as-truth) + rebuildable cache + migration;
admin sets color+description; badges reflect color; absent degrades gracefully.
**Built:** `internal/tagmeta/` (4 files: store/meta/validate + tests) — `Tags/{tag}.md` frontmatter
store (co-driver-chosen shape) via the SEC-01 resolver + single-writer commit; `tag_meta` cache with
migration `0011_tag_meta.sql`; `RebuildTagMeta` (delete-cache→rebuild=identical, proven by
`TestRebuildParity`). `handleTagMeta` + `admin.Post("/admin/tags/meta")`; `ValidateColor` (hex/palette
subset, rejects url()/var()/expression) + `ValidateDescription` (single-line, ≤200, HTML-safe).
`HashtagWidget` applies color via `span.style.color` (createElement+textContent, NO innerHTML). `Tags/`
skipped by page tree + graph/search rebuilds.
**Refinement holds:** files-as-truth preserved (store is the truth, SQLite is a rebuildable cache); the
XSS invariant holds. qa PASSED (14 tagmeta + 5 server tests).

### TF-03 — Tag hierarchy → MET
**Stated (Q):** cycle-safe parent/child in the TF-02 store, display-only, grouped admin list.
**Built:** `Meta.Parent` + migration `0012_tag_meta_parent.sql`; `CheckCycle` (O(depth) walk, cap 100,
rejects self/indirect cycles; non-existent parent = root); `handleTagMeta` runs `CheckCycle` before
write (400 on cycle); `handleTagList` returns parent; grouped rendering (roots→children, alphabetical).
**Refinement holds:** display-only confirmed (zero `parent` refs in `internal/pages/` or
`internal/search/` for auto-apply/search-expansion; hierarchy edits write only `Tags/{tag}.md`, never
wiki pages). Rebuild parity holds with parent. qa PASSED.

### TF-04 — Tag usage analytics → MET
**Stated (Q):** admin read-only view over `TagPageCounts` — counts, most/least, orphans, totals; no
new scan.
**Built:** `handleTagAnalytics` + `admin.Get("/admin/tags/analytics")` (read-only); reuses
`graph.TagPageCounts` + a lightweight `TaggedPageCount`; AnalyticsPanel with summary cards, sortable
per-tag table, most/least/orphans, scope note ("frontmatter page_tags; body-only #tags after
materialization").
**Refinement holds:** read-only (no mutation calls in the handler); no new heavy scan. qa PASSED (3 tests).

### TF-05 — Tag-operation undo → MET
**Stated (Q):** undo the last tag-admin op via inverse `ApplyTagsBatch` — byte-stable, 1 commit, 409-guarded,
forward-commit, dedicated op-journal.
**Built:** migration `0013_tag_op_journal.sql` (co-driver-chosen new SQLite table); `journalOp` hooked
into all 4 handlers (rename/merge/delete/bulk) recording {op_type, op_detail, pages:[{path, pre_op_tags,
body_affected}]}; `handleTagUndo` + `admin.Post("/admin/tags/undo")` reads the last row, builds inverse
items (swap bodyOld/bodyNew for rename/merge; frontmatter-only for delete/bulk), reads FRESH revisions
(TOCTOU-safe), replays via `ApplyTagsBatch`; forward-commit (no history rewrite); undo journals itself
(undo-of-undo=redo). `ActionTagUndo` audited.
**Refinement holds — with an honest limitation:** delete body `#tags` are NOT invertible (`RewriteHashtags`
removed the span; original bytes are in git history, not the page) — undo restores frontmatter tags only,
body self-heals on next edit. This limitation is stated in the undo response + SUMMARY, NOT silently
claimed away. qa PASSED (9 tests incl. rename golden byte-match, stale-skip, undo-of-undo).

## Track E — Authoring Polish (Phases 36–37)

### EP-01 — `#tag` badge in MarkdownProse → MET (security-sharpest)
**Built:** `web/src/lib/remark-hashtag.ts` — a remark plugin producing a typed hast `span`
(`data.hName`/`hProperties`/`hChildren`), NOT raw HTML; `rehype-raw` OFF / `rehype-sanitize` ON preserved
(MarkdownProse.tsx). false-`#` cases (inline code, escape, heading, URL, all-digit, in-word) stay literal.
**Refinement holds:** the badge is an element node, never raw HTML injection — the AgentAnswer stored-XSS
test passes. qa verified independently (red-team MIN-05).

### EP-02 — Copy-code button → MET
**Built:** `CodeBlockWidget` (widgets.ts:149-170) copy button — `createElement` + `textContent` (no
innerHTML), `navigator.clipboard` with graceful fallback, `user-select:none` so select-all excludes
"Copy", sibling of `<code>` so it never perturbs the async highlight swap / teardown guards.
**Refinement holds:** qa verified independently.

### EP-04 — Description in search cards → MET
**Built:** `description` added to `pageDoc` (indexed as keyword — no relevance change), `Result.Description`
(additive `omitempty`), rendered in `SearchResultRow.tsx`; absent falls back to today's card.
**Refinement holds:** additive, no shape regression. qa verified independently (`TestQuery_Description`
+ `TestQuery_DescriptionAbsent`).

### EP-03 — Insert-at-cursor templates → MET
**Built:** `TemplatePicker.tsx` + `PageEditor.insertTemplateAtCursor` — lists via `listTemplates`, fetches
body via `getPage` (SEC-01), substitutes `{{title}}/{{date}}/{{time}}` client-side, inserts via
`view.dispatch(view.state.replaceSelection(...))`; `externalSeed` NOT used (grep 0 — it's a user edit).
**Refinement holds:** normal CM6 transaction preserves byte-stability by construction. qa PASSED.

---

## Cross-cutting observations (the audit's real value)

1. **The full-suite smoke was the net.** Each phase engineer ran ONLY its own new tests (per the
   overhead-minimizing delegation contract). Three regressions escaped their scoped runs and the
   per-phase qa gates because those gates also ran scoped suites:
   - Phase 36's copy button leaked "Copy" into `<pre>.textContent` → broke `livePreview.test.ts` (fixed
     `1653933`, incl. a `user-select:none` UX fix) AND `widgets.adversarial.test.ts` INV-2 (fixed
     `e17dbca` with a *stronger* no-injection assertion).
   - Phase 33's alphabetical sort reordered the tag table → broke the v1.3 `TagManagement.test.tsx`
     position-based selectors (fixed `4b5c71f` by targeting rows by identity; sort behavior unchanged,
     contract assertions preserved).
   **Lesson for next milestone:** a phase touching a shared component (widgets.ts, TagManagement.tsx)
   should run the component's FULL existing suite, not just its new tests. The architect's close-time
   full smoke remains the backstop.

2. **Two design forks, both resolved by the co-driver, both semantically consistent.** Phase 32's
   `Tags/*.md` (over `.okf-workspace/tags.json`) keeps tag metadata as OKF-native content;
   Phase 35's new SQLite op-journal (over the metadata store) keeps operational state out of the
   content store. Both honor the files-as-truth invariant's *intent* (content is portable Markdown;
   operational state is a rebuildable/disposable cache).

3. **Supply chain clean:** zero new Go deps, zero new npm deps across the whole v1.6 range
   (empty go.mod/go.sum + package.json/package-lock.json diffs) — confirmed by the security review.

4. **Honesty floor held:** the TF-05 delete-body-tag limitation is surfaced, not hidden. The security
   review's one finding (copy-button leak) is recorded as informational + closed, not omitted.

## Final state (re-run at HEAD `4b5c71f`)
- `go build ./...` exit 0; `go test ./...` 19 packages ok, 0 failures.
- `cd web && npx vitest run` 66 files, 815 tests passed; `npx tsc --noEmit` exit 0.
- Working tree clean. Security review SHIP. Docs refreshed (new tag-metadata subsystem doc + map;
  independent drift-check ZERO findings). Milestone COMPLETE.
