Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,4 +231,6 @@ footer: |
| 2606241814 | ✅ | sonnet | [Add unit tests for lsp/rename dispatch helpers and workspace adapter methods](plan/2606241814_arch-fix-lsp-rename-dispatch-tests.md) |
| 2606241815 | ✅ | sonnet | [Add unit tests for three remaining unexported helpers in internal/index/locate.go](plan/2606241815_arch-fix-locate-remaining-helper-tests.md) |
| 2606260211 | ✅ | sonnet | [Add dedicated unit tests for layer0_html.go helpers](plan/2606260211_arch-fix-layer0-html-helper-tests.md) |
| 2606260614 | ✅ | sonnet | [arch-fix: add dedicated unit tests for lineclass_scan.go HTML-scanning helpers](plan/2606260614_arch-fix-lineclass-scan-helper-tests.md) |
| 2606260615 | 🔲 | sonnet | [arch-fix: add dedicated unit tests for cue/cuelite/engine.go unexported helpers](plan/2606260615_arch-fix-cuelite-engine-helper-tests.md) |
<?/catalog?>
97 changes: 34 additions & 63 deletions docs/development/architecture-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,14 @@ three packages; export from `internal/mdtext` —

### plan/153 non-goal superseded

Plan 153 kept the workspace symbol
index at `internal/lsp/index`. Its
stated non-goal: "only link/edge
extraction is in scope." Plan 174
supersedes that. The package is now
`internal/index`, a peer support
package.

The move is a pure `git mv`; no logic
changed. Two forces drove it.
`internal/schema` already imported the
index from outside `internal/lsp`. The
new `mdsmith rename` and `mdsmith deps`
surfaces need it too, and the layering
map forbids `cmd/mdsmith` →
`internal/lsp`. A peer package removes
the conflict. `internal/index` must
never import `internal/lsp`.
Plan 174 moved the workspace symbol index
from `internal/lsp/index` to `internal/index`.
Pure `git mv`; no logic changed.
`internal/schema` already imported it from
outside `internal/lsp`. `mdsmith rename` and
`mdsmith deps` need it. The layering map
forbids `cmd/mdsmith` → `internal/lsp`.
`internal/index` must never import `internal/lsp`.

## Audit 2026-05-19 (range: 7464d273..41e61a5)

Expand Down Expand Up @@ -135,59 +125,33 @@ Tax: [new-pkg-docs](../../plan/2606162213_arch-fix-new-pkg-docs.md),

## Audit 2026-06-21 (range: 7793b97..e701b94)

Parity + Layer-0 parse-skip series; symlink
containment; engine panic recovery; VS Code
`kinds` and `rule-doc` commands; security
hardening batch. 270 Go/TS sources outside
fixtures.

No blockers. New rule / convention packages
follow the OCP barrel pattern correctly; no
rule-to-rule imports added; no DIP violations
in the new `internal/rules/listscan` helper
(it follows the established `astutil` /
`fencepos` pattern).
Parity + Layer-0 parse-skip series.
Symlink containment; engine panic recovery.
VS Code `kinds` and `rule-doc` commands.
270 Go/TS sources. No blockers,
rule-to-rule imports, or DIP violations.

### tax (2026-06-21)

- `internal/engine/runner.go` (1 290 lines) —
SRP violation. Seven concerns in one file:
file dispatch, Layer-0 skip gate,
config-resolution cache, source-mode lint
path, front-matter parsing, config-target
rules, and logging. Go arch doc
§"Common violations to flag" names engine
as a dumping-ground risk. Fixed this cycle:
split into `runner_layer0.go`,
`runner_cache.go`, `runner_log.go` —
[plan/2606211907][2606211907].
SRP: 7 concerns. Fixed this cycle: split into
`runner_layer0.go`, `runner_cache.go`,
`runner_log.go` — [plan/2606211907][2606211907].

- `internal/lint/layer0.go` (1 203 lines) —
the full Layer-0 block scanner in one file:
types, scanner state machine, HTML-block
detection (types 1–7), fence handling, ATX
heading, indented code, paragraph. Maintenance
risk at this size. Fix: split along
full Layer-0 block scanner. Fix: split along
block-type sub-parsers —
[plan/2606211908][2606211908].

- `internal/lsp/server.go` (1 007 lines) —
plan 203 was green but the file has crept
back over 1 000 lines with the new `kinds`
and `rule-doc` capability wiring. Checklist
names it explicitly. Fix: apply the same
dispatch-group split plan 203 described —
[plan/2606211909][2606211909].
crept back over 1 000 lines. Dispatch-group
split — [plan/2606211909][2606211909].

### nice-to-have (2026-06-21)

- `pkg/mdsmith/workspace.go` trivial methods
(`memFile.Close`, `memDir.Close`,
`memDirEntry.Name`, `memDirEntry.IsDir`,
`memFileInfo.Name`, `memFileInfo.Size`) lack
the one-line "no test by design" exemption
comment the audit policy requires. Tests doc
§"Exemptions" — [plan/2606211910][2606211910].
lack "// no test by design" exemptions —
[plan/2606211910][2606211910].

[2606211907]: ../../plan/2606211907_arch-fix-runner-srp-split.md
[2606211908]: ../../plan/2606211908_arch-fix-layer0-split.md
Expand Down Expand Up @@ -289,13 +253,20 @@ line-count violations.
### tax (2026-06-26)

- `internal/lint/layer0_html.go` — seven
helpers lack dedicated tests; file entered
touched set via perf commit:
`openHTMLBlock`, `tagName.lowerInto`,
`type7TagIsRawText`, `type7TagBytes`,
`isTagByte`, `htmlBlockCloses`,
`scanner.tryHTMLBlock`. Tests doc
helpers lack dedicated tests. File entered
the touched set via a perf commit. Tests doc
§"every function by name" —
[plan/2606260211][2606260211].

- `internal/lint/lineclass_scan.go` — 9
unexported helpers lack tests.
Sub-functions of `htmlType7Start` —
[plan/2606260614][2606260614].

- `cue/cuelite/engine.go` — 7 unexported
helpers lack tests —
[plan/2606260615][2606260615].

[2606260211]: ../../plan/2606260211_arch-fix-layer0-html-helper-tests.md
[2606260614]: ../../plan/2606260614_arch-fix-lineclass-scan-helper-tests.md
[2606260615]: ../../plan/2606260615_arch-fix-cuelite-engine-helper-tests.md
4 changes: 2 additions & 2 deletions docs/development/architecture/audit-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,8 @@ explicit mention here:
- **A rule package importing another
rule package** — always a DIP
blocker. Helpers belong in
`internal/mdtext` or
`internal/rules/astutil`.
`internal/mdtext`, `internal/rules/astutil`,
or `internal/rules/listscan`.
- **`cmd/mdsmith/main.go` past ~1000
lines** — handler bodies have crept
in; relocate to `internal/engine` or
Expand Down
4 changes: 2 additions & 2 deletions docs/development/architecture/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,8 @@ caught and the reasons we reject them:
attracts unrelated code.
- **A rule package importing another rule
package.** Rules share helpers via
`internal/mdtext` or
`internal/rules/astutil`; reaching
`internal/mdtext`, `internal/rules/astutil`,
or `internal/rules/listscan`; reaching
sideways into a sibling rule binds
release cycles that should stay
independent.
Expand Down
184 changes: 184 additions & 0 deletions internal/lint/lineclass_scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,16 @@ func TestHTMLType6AndClose(t *testing.T) {
assert.False(t, containsType1Close([]byte("short")), "shorter than any closer")
}

func TestContainsFold(t *testing.T) {
needle := []byte("</pre>")
assert.True(t, containsFold([]byte("</PRE>"), needle), "match at position 0")
assert.True(t, containsFold([]byte("x</Pre>y"), needle), "match in middle")
assert.True(t, containsFold([]byte("x</PRE>"), needle), "match at last window")
assert.False(t, containsFold([]byte("x</em>y"), needle), "no match")
assert.False(t, containsFold([]byte("</pr"), needle), "needle longer than line")
assert.False(t, containsFold([]byte(""), needle), "empty line")
}

func TestContainerMarkerScanners(t *testing.T) {
assert.Equal(t, 2, blockquoteMarker([]byte("> x")))
assert.Equal(t, 1, blockquoteMarker([]byte(">x")), "marker with no following space")
Expand Down Expand Up @@ -292,3 +302,177 @@ func TestHTMLType6Tags_CommonMarkComplete(t *testing.T) {
}
}
}

// TestScanHTMLTag pins the dispatch between open and closing tags.
func TestScanHTMLTag(t *testing.T) {
n, ok := scanHTMLTag([]byte("<img>"))
assert.True(t, ok)
assert.Equal(t, 5, n)

n, ok = scanHTMLTag([]byte("</div>"))
assert.True(t, ok)
assert.Equal(t, 6, n)

// Does not start with '<'.
_, ok = scanHTMLTag([]byte("img>"))
assert.False(t, ok)

// Fewer than 3 bytes.
_, ok = scanHTMLTag([]byte("<a"))
assert.False(t, ok)
}

// TestScanClosingTag pins the closing-tag scanner.
func TestScanClosingTag(t *testing.T) {
s := []byte("</div>")
n, ok := scanClosingTag(s, 2)
assert.True(t, ok)
assert.Equal(t, 6, n)

// Optional whitespace before '>'.
n, ok = scanClosingTag([]byte("</div >"), 2)
assert.True(t, ok)
assert.Equal(t, 7, n)

// Missing '>'.
_, ok = scanClosingTag([]byte("</div"), 2)
assert.False(t, ok)

// Name starts with a digit.
_, ok = scanClosingTag([]byte("</1tag>"), 2)
assert.False(t, ok)
}

// TestScanOpenTag pins the open-tag scanner: tag name, attributes, self-close.
func TestScanOpenTag(t *testing.T) {
n, ok := scanOpenTag([]byte("<img>"), 1)
assert.True(t, ok)
assert.Equal(t, 5, n)

n, ok = scanOpenTag([]byte("<br/>"), 1)
assert.True(t, ok)
assert.Equal(t, 5, n)

n, ok = scanOpenTag([]byte("<br />"), 1)
assert.True(t, ok)
assert.Equal(t, 6, n)

n, ok = scanOpenTag([]byte(`<img src="x">`), 1)
assert.True(t, ok)
assert.Equal(t, 13, n)

// '=' with no value.
_, ok = scanOpenTag([]byte("<img src=>"), 1)
assert.False(t, ok)

// No closing '>'.
_, ok = scanOpenTag([]byte("<img"), 1)
assert.False(t, ok)
}

// TestScanTagName pins the tag-name scanner.
func TestScanTagName(t *testing.T) {
i, ok := scanTagName([]byte("img>"), 0)
assert.True(t, ok)
assert.Equal(t, 3, i)

// Hyphens allowed inside a name.
i, ok = scanTagName([]byte("a-b>"), 0)
assert.True(t, ok)
assert.Equal(t, 3, i)

// Digit at start → reject.
_, ok = scanTagName([]byte("1tag"), 0)
assert.False(t, ok)

// Empty input.
_, ok = scanTagName([]byte(""), 0)
assert.False(t, ok)

// Name runs to end of input (no terminator byte).
i, ok = scanTagName([]byte("img"), 0)
assert.True(t, ok)
assert.Equal(t, 3, i)
}

// TestScanAttribute pins the attribute scanner.
func TestScanAttribute(t *testing.T) {
// Valueless attribute.
i, ok := scanAttribute([]byte(" disabled>"), 0)
assert.True(t, ok)
assert.Equal(t, 9, i)

// Unquoted value.
i, ok = scanAttribute([]byte(" src=x>"), 0)
assert.True(t, ok)
assert.Equal(t, 6, i)

// Double-quoted value.
i, ok = scanAttribute([]byte(` src="x">`), 0)
assert.True(t, ok)
assert.Equal(t, 8, i)

// '=' with no value.
_, ok = scanAttribute([]byte(" src=>"), 0)
assert.False(t, ok)

// No leading whitespace → reject.
_, ok = scanAttribute([]byte("src=x>"), 0)
assert.False(t, ok)
}

// TestScanAttrValue pins the attribute-value scanner.
func TestScanAttrValue(t *testing.T) {
// Unquoted value (stops at '>').
i, ok := scanAttrValue([]byte("value>"), 0)
assert.True(t, ok)
assert.Equal(t, 5, i)

// Single-quoted.
i, ok = scanAttrValue([]byte("'value'"), 0)
assert.True(t, ok)
assert.Equal(t, 7, i)

// Double-quoted.
i, ok = scanAttrValue([]byte(`"value"`), 0)
assert.True(t, ok)
assert.Equal(t, 7, i)

// Unclosed single quote.
_, ok = scanAttrValue([]byte("'value"), 0)
assert.False(t, ok)

// Empty unquoted value (stop byte at position 0).
_, ok = scanAttrValue([]byte(">"), 0)
assert.False(t, ok)
}

// TestSkipHTMLWS pins the whitespace-skipper.
func TestSkipHTMLWS(t *testing.T) {
assert.Equal(t, 2, skipHTMLWS([]byte(" x"), 0))
assert.Equal(t, 1, skipHTMLWS([]byte("\tx"), 0))
assert.Equal(t, 2, skipHTMLWS([]byte(" \tx"), 0))
assert.Equal(t, 0, skipHTMLWS([]byte("x"), 0))
assert.Equal(t, 2, skipHTMLWS([]byte(" "), 2))
}

// TestIsUnquotedStop pins which bytes end an unquoted attribute value.
func TestIsUnquotedStop(t *testing.T) {
for _, b := range []byte{' ', '\t', '"', '\'', '=', '<', '>', '`'} {
assert.Truef(t, isUnquotedStop(b), "expected stop for 0x%02x", b)
}
for _, b := range []byte{'a', '0', '-', '_', '/'} {
assert.Falsef(t, isUnquotedStop(b), "expected non-stop for 0x%02x", b)
}
}

// TestEqualFoldASCII pins the case-insensitive ASCII byte comparison.
// b is always lowercase; a may be any case. Callers always pass same-length
// slices.
func TestEqualFoldASCII(t *testing.T) {
assert.True(t, equalFoldASCII([]byte("pre"), []byte("pre")))
assert.True(t, equalFoldASCII([]byte("PRE"), []byte("pre")))
assert.True(t, equalFoldASCII([]byte("ScRiPt"), []byte("script")))
assert.False(t, equalFoldASCII([]byte("pre"), []byte("div")))
assert.False(t, equalFoldASCII([]byte("foo"), []byte("bar")))
}
Loading
Loading