dotslot 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dotslot-0.2.1/LICENSE +21 -0
- dotslot-0.2.1/PKG-INFO +108 -0
- dotslot-0.2.1/README.md +89 -0
- dotslot-0.2.1/dotslot/__init__.py +3 -0
- dotslot-0.2.1/dotslot/__main__.py +3 -0
- dotslot-0.2.1/dotslot/cli.py +204 -0
- dotslot-0.2.1/dotslot/core.py +284 -0
- dotslot-0.2.1/dotslot/mcp.py +77 -0
- dotslot-0.2.1/dotslot/verify.py +58 -0
- dotslot-0.2.1/dotslot/width.py +120 -0
- dotslot-0.2.1/dotslot.egg-info/PKG-INFO +108 -0
- dotslot-0.2.1/dotslot.egg-info/SOURCES.txt +18 -0
- dotslot-0.2.1/dotslot.egg-info/dependency_links.txt +1 -0
- dotslot-0.2.1/dotslot.egg-info/entry_points.txt +3 -0
- dotslot-0.2.1/dotslot.egg-info/requires.txt +4 -0
- dotslot-0.2.1/dotslot.egg-info/top_level.txt +1 -0
- dotslot-0.2.1/pyproject.toml +29 -0
- dotslot-0.2.1/setup.cfg +4 -0
- dotslot-0.2.1/tests/test_dotslot.py +158 -0
- dotslot-0.2.1/tests/test_slot.py +77 -0
dotslot-0.2.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ao3575911
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
dotslot-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dotslot
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Let agents fill the blanks in a file, and prove they touched nothing else.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/ao3575911/dotslot
|
|
7
|
+
Project-URL: Changelog, https://github.com/ao3575911/dotslot/blob/main/CHANGELOG.md
|
|
8
|
+
Keywords: templates,fixed-width,ai-agents,mcp,pre-commit,lint
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: wcwidth>=0.2.6
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
[](https://m8ven.ai/mcp/ao3575911-dotslot-ia87ds?s=readme)
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
dotslot: let agents fill the blanks, and prove they touched nothing else.
|
|
24
|
+
|
|
25
|
+
Owner: `<./owner:____________>` Status: `<./status~:____________>`
|
|
26
|
+
|
|
27
|
+
$ dotslot fill release.md --set owner=Adam --set status="ready for tag" -i
|
|
28
|
+
$ dotslot check release.md --git-base HEAD
|
|
29
|
+
release.md: fill-safe (2 slot(s) changed)
|
|
30
|
+
|
|
31
|
+
CLI · pre-commit · GitHub Action · MCP server (Claude Code, Cursor, aider)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
[](https://github.com/ao3575911/dotslot/actions/workflows/ci.yml) · MIT · v0.2.1 · [Spec](SPEC.md) · [Experiments](EXPERIMENTS.md) · [Changelog](CHANGELOG.md)
|
|
35
|
+
|
|
36
|
+
## What it is
|
|
37
|
+
|
|
38
|
+
Mark the parts of a file that may change with `<./________>`. An agent or script fills those blanks and nothing else. `dotslot check` proves the diff touched only the blanks, and that each blank kept its width, so columns and line wraps stay put. It works in docs, configs and code comments.
|
|
39
|
+
|
|
40
|
+
A file with slots is **fill-only**. That turns "don't touch the rest" into a rule a machine can check.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
Install from PyPI:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install dotslot
|
|
48
|
+
dotslot --help
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Requires Python 3.10+ and `wcwidth`.
|
|
52
|
+
|
|
53
|
+
## Use
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
dotslot new notes --width 26 --lines 3 --comment '# ' # generate a slot or block
|
|
57
|
+
dotslot scan FILE # list slots, widths, capacity
|
|
58
|
+
dotslot fill FILE --set name=value -i # fill in place, width kept
|
|
59
|
+
dotslot check FILE --git-base HEAD [--allow a,b] # prove only slots changed
|
|
60
|
+
dotslot repad FILE --base OLD -i # restore padding after a hand edit
|
|
61
|
+
dotslot fit FILE --targets 80,100,120 # where will lines wrap?
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Slot forms: `<./______>` (unnamed), `<./owner:____>` (named), `<./status~:____>` (ellipsis on overflow). In Markdown, put slots in backticks. Full grammar: [SPEC.md](SPEC.md).
|
|
65
|
+
|
|
66
|
+
## Integrations
|
|
67
|
+
|
|
68
|
+
| tool | files | what it does |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| MCP server | `dotslot-mcp --root .` | `dotslot_scan`, `dotslot_fill`, `dotslot_check` over stdio; fill verifies before writing; paths confined to root |
|
|
71
|
+
| Claude Code | [`integrations/claude-code/`](integrations/claude-code) | `.mcp.json`, a `PostToolUse` hook that exits 2 on a violation, and a skill |
|
|
72
|
+
| Cursor | [`integrations/cursor/.cursor/`](integrations/cursor/.cursor) | `mcp.json` and a fill-only rule |
|
|
73
|
+
| aider | [`integrations/aider/`](integrations/aider) | `lint-cmd: dotslot check --git-base HEAD` with auto-lint |
|
|
74
|
+
| any agent | [`integrations/AGENTS.md`](integrations/AGENTS.md) | snippet to paste into your `AGENTS.md` |
|
|
75
|
+
| pre-commit | [`.pre-commit-hooks.yaml`](.pre-commit-hooks.yaml) | `dotslot-check`, `dotslot-lint` |
|
|
76
|
+
| GitHub Action | [`action.yml`](action.yml) | checks PR files against the base SHA, writes a JSON report to the step summary |
|
|
77
|
+
|
|
78
|
+
pre-commit:
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
- repo: https://github.com/ao3575911/dotslot
|
|
82
|
+
rev: v0.2.1
|
|
83
|
+
hooks: [{ id: dotslot-check }]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
GitHub Action:
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
- uses: actions/checkout@v4
|
|
90
|
+
with: { fetch-depth: 0 }
|
|
91
|
+
- uses: ao3575911/dotslot@v0.2.1
|
|
92
|
+
with: { files: "docs/**/*.md config/*.yaml", allow: "status,owner" }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Limits
|
|
96
|
+
|
|
97
|
+
dotslot is a check, not a sandbox. The comment finder is a heuristic until tree-sitter lands; formatters such as gofmt can move columns, so run dotslot after them; trailing whitespace counts as a change. See [CHANGELOG.md](CHANGELOG.md) for open items and [PLAN.md](PLAN.md) for the roadmap.
|
|
98
|
+
|
|
99
|
+
## Repository layout
|
|
100
|
+
|
|
101
|
+
- `dotslot/`: the v0.2 package (CLI, MCP server).
|
|
102
|
+
- `slot/`, `bin/slot`: the v0.1 reference, kept only so `experiments/run.sh` reproduces findings (a)–(g). Not installed.
|
|
103
|
+
- `tests/`: 45 tests (12 v0.1, 33 v0.2). Run `pip install -e ".[test]" && pytest -q`.
|
|
104
|
+
- `experiments/`: `run.sh` (a–g) and `run_v02.sh` (v02, h, i), with recorded output in `out/`.
|
|
105
|
+
|
|
106
|
+
## Licence
|
|
107
|
+
|
|
108
|
+
MIT. See [LICENSE](LICENSE). Releases are GPG-signed in CI and carry SHA256SUMS and build provenance; see [RELEASING.md](RELEASING.md).
|
dotslot-0.2.1/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
[](https://m8ven.ai/mcp/ao3575911-dotslot-ia87ds?s=readme)
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
dotslot: let agents fill the blanks, and prove they touched nothing else.
|
|
5
|
+
|
|
6
|
+
Owner: `<./owner:____________>` Status: `<./status~:____________>`
|
|
7
|
+
|
|
8
|
+
$ dotslot fill release.md --set owner=Adam --set status="ready for tag" -i
|
|
9
|
+
$ dotslot check release.md --git-base HEAD
|
|
10
|
+
release.md: fill-safe (2 slot(s) changed)
|
|
11
|
+
|
|
12
|
+
CLI · pre-commit · GitHub Action · MCP server (Claude Code, Cursor, aider)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
[](https://github.com/ao3575911/dotslot/actions/workflows/ci.yml) · MIT · v0.2.1 · [Spec](SPEC.md) · [Experiments](EXPERIMENTS.md) · [Changelog](CHANGELOG.md)
|
|
16
|
+
|
|
17
|
+
## What it is
|
|
18
|
+
|
|
19
|
+
Mark the parts of a file that may change with `<./________>`. An agent or script fills those blanks and nothing else. `dotslot check` proves the diff touched only the blanks, and that each blank kept its width, so columns and line wraps stay put. It works in docs, configs and code comments.
|
|
20
|
+
|
|
21
|
+
A file with slots is **fill-only**. That turns "don't touch the rest" into a rule a machine can check.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
Install from PyPI:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install dotslot
|
|
29
|
+
dotslot --help
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Requires Python 3.10+ and `wcwidth`.
|
|
33
|
+
|
|
34
|
+
## Use
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
dotslot new notes --width 26 --lines 3 --comment '# ' # generate a slot or block
|
|
38
|
+
dotslot scan FILE # list slots, widths, capacity
|
|
39
|
+
dotslot fill FILE --set name=value -i # fill in place, width kept
|
|
40
|
+
dotslot check FILE --git-base HEAD [--allow a,b] # prove only slots changed
|
|
41
|
+
dotslot repad FILE --base OLD -i # restore padding after a hand edit
|
|
42
|
+
dotslot fit FILE --targets 80,100,120 # where will lines wrap?
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Slot forms: `<./______>` (unnamed), `<./owner:____>` (named), `<./status~:____>` (ellipsis on overflow). In Markdown, put slots in backticks. Full grammar: [SPEC.md](SPEC.md).
|
|
46
|
+
|
|
47
|
+
## Integrations
|
|
48
|
+
|
|
49
|
+
| tool | files | what it does |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| MCP server | `dotslot-mcp --root .` | `dotslot_scan`, `dotslot_fill`, `dotslot_check` over stdio; fill verifies before writing; paths confined to root |
|
|
52
|
+
| Claude Code | [`integrations/claude-code/`](integrations/claude-code) | `.mcp.json`, a `PostToolUse` hook that exits 2 on a violation, and a skill |
|
|
53
|
+
| Cursor | [`integrations/cursor/.cursor/`](integrations/cursor/.cursor) | `mcp.json` and a fill-only rule |
|
|
54
|
+
| aider | [`integrations/aider/`](integrations/aider) | `lint-cmd: dotslot check --git-base HEAD` with auto-lint |
|
|
55
|
+
| any agent | [`integrations/AGENTS.md`](integrations/AGENTS.md) | snippet to paste into your `AGENTS.md` |
|
|
56
|
+
| pre-commit | [`.pre-commit-hooks.yaml`](.pre-commit-hooks.yaml) | `dotslot-check`, `dotslot-lint` |
|
|
57
|
+
| GitHub Action | [`action.yml`](action.yml) | checks PR files against the base SHA, writes a JSON report to the step summary |
|
|
58
|
+
|
|
59
|
+
pre-commit:
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
- repo: https://github.com/ao3575911/dotslot
|
|
63
|
+
rev: v0.2.1
|
|
64
|
+
hooks: [{ id: dotslot-check }]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
GitHub Action:
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
- uses: actions/checkout@v4
|
|
71
|
+
with: { fetch-depth: 0 }
|
|
72
|
+
- uses: ao3575911/dotslot@v0.2.1
|
|
73
|
+
with: { files: "docs/**/*.md config/*.yaml", allow: "status,owner" }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Limits
|
|
77
|
+
|
|
78
|
+
dotslot is a check, not a sandbox. The comment finder is a heuristic until tree-sitter lands; formatters such as gofmt can move columns, so run dotslot after them; trailing whitespace counts as a change. See [CHANGELOG.md](CHANGELOG.md) for open items and [PLAN.md](PLAN.md) for the roadmap.
|
|
79
|
+
|
|
80
|
+
## Repository layout
|
|
81
|
+
|
|
82
|
+
- `dotslot/`: the v0.2 package (CLI, MCP server).
|
|
83
|
+
- `slot/`, `bin/slot`: the v0.1 reference, kept only so `experiments/run.sh` reproduces findings (a)–(g). Not installed.
|
|
84
|
+
- `tests/`: 45 tests (12 v0.1, 33 v0.2). Run `pip install -e ".[test]" && pytest -q`.
|
|
85
|
+
- `experiments/`: `run.sh` (a–g) and `run_v02.sh` (v02, h, i), with recorded output in `out/`.
|
|
86
|
+
|
|
87
|
+
## Licence
|
|
88
|
+
|
|
89
|
+
MIT. See [LICENSE](LICENSE). Releases are GPG-signed in CI and carry SHA256SUMS and build provenance; see [RELEASING.md](RELEASING.md).
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""dotslot scan | fill | check | fit"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import argparse, json, subprocess, sys
|
|
4
|
+
from .core import scan_text, fill_text, SlotError
|
|
5
|
+
from .verify import verify
|
|
6
|
+
from .width import layout, preview, legacy_ends, display_width
|
|
7
|
+
|
|
8
|
+
def read(p): return sys.stdin.read() if p == '-' else open(p, encoding='utf-8').read()
|
|
9
|
+
|
|
10
|
+
def parse_sets(items):
|
|
11
|
+
out = {}
|
|
12
|
+
for it in items or []:
|
|
13
|
+
k, sep, v = it.partition('=')
|
|
14
|
+
if not sep: raise SystemExit(f'--set needs name=value, got {it!r}')
|
|
15
|
+
out[k] = v
|
|
16
|
+
return out
|
|
17
|
+
|
|
18
|
+
def cmd_scan(a):
|
|
19
|
+
rows = []
|
|
20
|
+
for p in a.files:
|
|
21
|
+
slots, issues = scan_text(read(p), p, a.mode)
|
|
22
|
+
for s in slots:
|
|
23
|
+
rows.append(dict(file=p, line=s.line + 1, col=s.col, index=s.index, name=s.name, role=s.role,
|
|
24
|
+
width=s.width, lines=1 + len(s.conts), capacity=s.capacity + sum(c.capacity for c in s.conts),
|
|
25
|
+
filled=s.filled, value=(s.block_value() if s.conts else s.value) if s.filled else None,
|
|
26
|
+
policy=s.policy, in_comment=s.in_comment))
|
|
27
|
+
for sev, l, c, m in issues: print(f'{p}:{l+1}:{c}: {sev}: {m}', file=sys.stderr)
|
|
28
|
+
if a.json: print(json.dumps(rows, ensure_ascii=False, indent=1)); return 0
|
|
29
|
+
for r in rows:
|
|
30
|
+
v = '' if r['value'] is None else repr(r['value'])
|
|
31
|
+
print(f"{r['file']}:{r['line']}:{r['col']} @{r['index']} {r['name'] or '-':12} {r['role']:6} "
|
|
32
|
+
f"w={r['width']:<3} x{r['lines']} cap={r['capacity']:<3} {'filled' if r['filled'] else 'empty ':6} "
|
|
33
|
+
f"{r['policy'] or '':8} {'#' if r['in_comment'] else ' '} {v}")
|
|
34
|
+
return 0
|
|
35
|
+
|
|
36
|
+
def cmd_fill(a):
|
|
37
|
+
vals = parse_sets(a.set)
|
|
38
|
+
if a.json_values: vals.update(json.load(open(a.json_values)))
|
|
39
|
+
rc = 0
|
|
40
|
+
allow = set(a.allow.split(',')) if a.allow else None
|
|
41
|
+
for p in a.files:
|
|
42
|
+
try:
|
|
43
|
+
out = fill_text(read(p), vals, p, a.mode, a.overflow, tuple(a.clear or ()), allow)
|
|
44
|
+
except SlotError as e:
|
|
45
|
+
print(f'{p}: error: {e}', file=sys.stderr); rc = 2; continue
|
|
46
|
+
if a.in_place and p != '-':
|
|
47
|
+
open(p, 'w', encoding='utf-8').write(out)
|
|
48
|
+
else:
|
|
49
|
+
sys.stdout.write(out)
|
|
50
|
+
return rc
|
|
51
|
+
|
|
52
|
+
def fit_report(text, path, mode, targets, tab, show_preview):
|
|
53
|
+
slots, _ = scan_text(text, path, mode)
|
|
54
|
+
lines = text.split('\n'); out = []; bad = {t: 0 for t in targets}
|
|
55
|
+
by_line = {}
|
|
56
|
+
for s in slots:
|
|
57
|
+
for x in [s] + s.conts: by_line.setdefault(x.line, []).append((s, x))
|
|
58
|
+
for li in sorted(by_line):
|
|
59
|
+
ln = lines[li]
|
|
60
|
+
cl = layout([(ln, 'line')], tab)
|
|
61
|
+
lw = cl[-1].col + cl[-1].width if cl else 0
|
|
62
|
+
leg = legacy_ends([(ln, 'line')], tab)[-1]
|
|
63
|
+
grows = any(h.policy == 'grow' for h, _ in by_line[li])
|
|
64
|
+
for head, x in by_line[li]:
|
|
65
|
+
end = display_width(ln[:x.end], 0, tab)
|
|
66
|
+
st = {}
|
|
67
|
+
for t in targets:
|
|
68
|
+
s_ = 'over' if end > t else 'pushes' if lw > t else 'maybe' if leg > t else 'ok'
|
|
69
|
+
st[t] = s_
|
|
70
|
+
if s_ != 'ok': bad[t] += 1
|
|
71
|
+
label = (head.name or f'@{head.index}') + ('' if x is head else f'[+{x.line - head.line}]')
|
|
72
|
+
out.append(f"{path}:{li+1}: slot {label} col {x.col}..{end} (line {lw}{'' if leg == lw else f', per-codepoint {leg}'})"
|
|
73
|
+
f" {'grow' if grows else 'static'} " + ' '.join(f'{t}:{v}' for t, v in st.items()))
|
|
74
|
+
if show_preview:
|
|
75
|
+
for t in targets:
|
|
76
|
+
if st[t] in ('over', 'pushes') and (p := preview(cl, t)):
|
|
77
|
+
out.append(f" @{t}: {p['note']}" + (' (2-cell glyph straddles edge)' if p['straddle'] else ''))
|
|
78
|
+
out.append(f" word │{p['word'][0]}\n │{p['word'][1]}")
|
|
79
|
+
return out, bad
|
|
80
|
+
|
|
81
|
+
def cmd_fit(a):
|
|
82
|
+
targets = sorted(int(t) for t in a.targets.split(','))
|
|
83
|
+
rc = 0
|
|
84
|
+
for p in a.files:
|
|
85
|
+
text = read(p)
|
|
86
|
+
if a.set:
|
|
87
|
+
try: text = fill_text(text, parse_sets(a.set), p, a.mode, a.overflow)
|
|
88
|
+
except SlotError as e: print(f'{p}: error: {e}', file=sys.stderr); rc = 2; continue
|
|
89
|
+
out, bad = fit_report(text, p, a.mode, targets, a.tab, a.preview)
|
|
90
|
+
print('\n'.join(out)) if out else None
|
|
91
|
+
if bad.get(a.fail_at or targets[0]): rc = max(rc, 1)
|
|
92
|
+
return rc
|
|
93
|
+
|
|
94
|
+
def base_text(a, p):
|
|
95
|
+
if a.base: return read(a.base)
|
|
96
|
+
if a.git_base:
|
|
97
|
+
r = subprocess.run(['git', 'show', f'{a.git_base}:./{p}'], capture_output=True, text=True,
|
|
98
|
+
cwd=None)
|
|
99
|
+
return r.stdout if r.returncode == 0 else None # new file: nothing to compare
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
def cmd_new(a):
|
|
103
|
+
pol = a.policy or ''
|
|
104
|
+
head = f'{a.name}{pol}:' if a.name else (f'{pol}:' if pol else '')
|
|
105
|
+
if a.width < len(head) + 1: raise SystemExit(f'width {a.width} leaves no room after {head!r}')
|
|
106
|
+
pre, post = a.comment, (' ' + CLOSE[a.comment.strip()] if a.comment.strip() in CLOSE else '')
|
|
107
|
+
lines = [pre + '<./' + head + a.pad * (a.width - len(head)) + '>' + post]
|
|
108
|
+
for _ in range(a.lines - 1):
|
|
109
|
+
lines.append(pre + '<./|' + a.pad * (a.width - 1) + '>' + post)
|
|
110
|
+
if a.md: lines = ['`' + l + '`' for l in lines]
|
|
111
|
+
print('\n'.join(lines)); return 0
|
|
112
|
+
|
|
113
|
+
CLOSE = {'<!--': '-->', '/*': '*/'}
|
|
114
|
+
|
|
115
|
+
def cmd_repad(a):
|
|
116
|
+
from .core import scan_text as sc, render
|
|
117
|
+
base, text = read(a.base), read(a.files[0])
|
|
118
|
+
bs, _ = sc(base, a.files[0], a.mode); ns, _ = sc(text, a.files[0], a.mode)
|
|
119
|
+
if len(bs) != len(ns): print('slot count differs; cannot repad', file=sys.stderr); return 2
|
|
120
|
+
lines = text.split('\n'); fixed = 0
|
|
121
|
+
for b, n in sorted(zip(bs, ns), key=lambda p: (p[1].line, -p[1].start)):
|
|
122
|
+
if b.width != n.width and not n.conts and b.policy != 'grow' and b.header == n.header:
|
|
123
|
+
if len(n.raw_value) and __import__('dotslot.width', fromlist=['cells']).cells(n.header + n.raw_value) <= b.width:
|
|
124
|
+
ln = lines[n.line]
|
|
125
|
+
lines[n.line] = ln[:n.start] + '<./' + render(n.header, n.raw_value, b.width, b.pad) + '>' + ln[n.end:]
|
|
126
|
+
fixed += 1
|
|
127
|
+
out = '\n'.join(lines)
|
|
128
|
+
if a.in_place: open(a.files[0], 'w', encoding='utf-8').write(out)
|
|
129
|
+
else: sys.stdout.write(out)
|
|
130
|
+
print(f'repadded {fixed} slot(s)', file=sys.stderr); return 0
|
|
131
|
+
|
|
132
|
+
def cmd_check(a):
|
|
133
|
+
rc = 0
|
|
134
|
+
req = set((a.require or '').split(',')) - {''}
|
|
135
|
+
allow = set(a.allow.split(',')) if a.allow else None
|
|
136
|
+
reports = []
|
|
137
|
+
for p in a.files:
|
|
138
|
+
text = read(p)
|
|
139
|
+
b = base_text(a, p)
|
|
140
|
+
if b is not None:
|
|
141
|
+
r = verify(b, text, p, a.mode, allow); r['file'] = p; reports.append(r)
|
|
142
|
+
if not a.json:
|
|
143
|
+
for c in r['changed']: print(f"{p}:{c['line']}: filled {c['slot']}: {c['before']!r} -> {c['after']!r}")
|
|
144
|
+
for x in r['violations']: print(f'{p}: VIOLATION: {x}')
|
|
145
|
+
print(f"{p}: {'fill-safe' if r['ok'] else 'NOT fill-safe'} ({len(r['changed'])} slot(s) changed)")
|
|
146
|
+
if not r['ok']: rc = 1
|
|
147
|
+
continue
|
|
148
|
+
slots, issues = scan_text(text, p, a.mode)
|
|
149
|
+
probs = [(sev, l, c, m) for sev, l, c, m in issues]
|
|
150
|
+
for s in slots:
|
|
151
|
+
if (a.require_filled or (s.name in req)) and not s.filled:
|
|
152
|
+
probs.append(('E', s.line, s.col, f'{s.name or "@%d" % s.index} is empty'))
|
|
153
|
+
names = {s.name for s in slots}
|
|
154
|
+
for n in req - names: probs.append(('E', 0, 0, f'required slot {n!r} not found'))
|
|
155
|
+
if a.fit:
|
|
156
|
+
out, bad = fit_report(text, p, a.mode, [a.fit], a.tab, False)
|
|
157
|
+
for o in out:
|
|
158
|
+
if f'{a.fit}:ok' not in o: probs.append(('E', 0, 0, 'fit: ' + o.split(': ', 1)[1]))
|
|
159
|
+
for sev, l, c, m in probs:
|
|
160
|
+
if not a.json: print(f'{p}:{l+1}:{c}: {sev}: {m}')
|
|
161
|
+
if sev == 'E' or (a.strict and sev == 'W'): rc = 1
|
|
162
|
+
if not probs and not a.json: print(f'{p}: ok ({len(slots)} slots)')
|
|
163
|
+
if a.json: print(json.dumps(reports, ensure_ascii=False, indent=1))
|
|
164
|
+
return rc
|
|
165
|
+
|
|
166
|
+
def main(argv=None):
|
|
167
|
+
ap = argparse.ArgumentParser(prog='dotslot', description='fixed-width <./____> slots (v0.2)')
|
|
168
|
+
sub = ap.add_subparsers(dest='cmd', required=True)
|
|
169
|
+
def common(sp):
|
|
170
|
+
sp.add_argument('files', nargs='+')
|
|
171
|
+
sp.add_argument('--mode', choices=['auto', 'prose', 'comments'], default='auto',
|
|
172
|
+
help='auto: bare slots only in prose files (.md .txt …); everything else comment-only')
|
|
173
|
+
s = sub.add_parser('scan'); common(s); s.add_argument('--json', action='store_true')
|
|
174
|
+
for name in ('fill', 'fit'):
|
|
175
|
+
f = sub.add_parser(name); common(f)
|
|
176
|
+
f.add_argument('--set', action='append', help='name=value or @N=value')
|
|
177
|
+
f.add_argument('--overflow', choices=['error', 'ellipsis', 'truncate', 'grow'], default='error',
|
|
178
|
+
help='policy for slots without a !/~/+ marker')
|
|
179
|
+
f.add_argument('--allow', help='comma list of slot names/@N that may be filled')
|
|
180
|
+
if name == 'fill':
|
|
181
|
+
f.add_argument('--json-values'); f.add_argument('-i', '--in-place', action='store_true')
|
|
182
|
+
f.add_argument('--clear', action='append', help='empty a slot again (name or @N)')
|
|
183
|
+
else:
|
|
184
|
+
f.add_argument('--targets', default='80,100,120'); f.add_argument('--tab', type=int, default=8)
|
|
185
|
+
f.add_argument('--preview', action='store_true'); f.add_argument('--fail-at', type=int)
|
|
186
|
+
n = sub.add_parser('new', help='print a correctly sized slot or block')
|
|
187
|
+
n.add_argument('name', nargs='?'); n.add_argument('--width', type=int, required=True)
|
|
188
|
+
n.add_argument('--lines', type=int, default=1); n.add_argument('--policy', choices=['!', '~', '+'])
|
|
189
|
+
n.add_argument('--comment', default='', help="prefix such as '# ' or '<!-- '"); n.add_argument('--pad', default='_')
|
|
190
|
+
n.add_argument('--md', action='store_true', help='wrap in backticks (Markdown code span)')
|
|
191
|
+
n.set_defaults(indent_only=False)
|
|
192
|
+
r = sub.add_parser('repad', help='restore slot widths from a base file (fixes agents that drop padding)')
|
|
193
|
+
r.add_argument('files', nargs=1); r.add_argument('--base', required=True); r.add_argument('-i', '--in-place', action='store_true')
|
|
194
|
+
r.add_argument('--mode', choices=['auto', 'prose', 'comments'], default='auto')
|
|
195
|
+
c = sub.add_parser('check'); common(c)
|
|
196
|
+
c.add_argument('--require', help='comma list of names that must be filled')
|
|
197
|
+
c.add_argument('--require-filled', action='store_true'); c.add_argument('--strict', action='store_true')
|
|
198
|
+
c.add_argument('--fit', type=int, help='also fail lines wider than N cells'); c.add_argument('--tab', type=int, default=8)
|
|
199
|
+
c.add_argument('--base', help='fill-safe check: compare against this earlier version of the file')
|
|
200
|
+
c.add_argument('--git-base', help='fill-safe check against a git revision (e.g. HEAD, origin/main)')
|
|
201
|
+
c.add_argument('--allow', help='with --base: only these slots may change')
|
|
202
|
+
c.add_argument('--json', action='store_true')
|
|
203
|
+
a = ap.parse_args(argv)
|
|
204
|
+
return dict(scan=cmd_scan, fill=cmd_fill, check=cmd_check, fit=cmd_fit, new=cmd_new, repad=cmd_repad)[a.cmd](a)
|