super-ux 0.59.1 → 0.59.2
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.
- package/CHANGELOG.md +21 -0
- package/bin/super-ux.js +6 -2
- package/package.json +1 -1
- package/plugins/super-ux/scripts/brand_lint.py +46 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
## 0.59.2 — brand-lint names the line, and a release refuses a lightweight tag
|
|
2
|
+
|
|
3
|
+
- **`B022` reports the line.** An unregistered literal was reported at line 0 —
|
|
4
|
+
`B022 bin/super-ux.js:0` for a string on line 303 — so the finding named a file
|
|
5
|
+
and left the search to the reader. Code literals now carry the line their first
|
|
6
|
+
character is on (template-literal paragraphs included), HTML controls the line
|
|
7
|
+
they open on. The comment scanner keeps a block comment's newlines so a stripped
|
|
8
|
+
comment cannot shift the count. Fixture `b022_line_cases` in
|
|
9
|
+
`test/brand_lint_test.py`; three plants (line 0 back, comment newlines dropped,
|
|
10
|
+
HTML line pinned) each turned it red.
|
|
11
|
+
- **The probe string that fired it is retired, not registered.** `claude --version`
|
|
12
|
+
in `bin/super-ux.js` was the `command` field of the typed result returned when the
|
|
13
|
+
claude CLI is absent; nothing prints it. B022's doctrine is "agree it or retire
|
|
14
|
+
it", and a row in `strings.md` would register as interface copy a string no
|
|
15
|
+
reader sees, so the field is now derived from the probe's own argv, the way
|
|
16
|
+
`run()` derives every other `command`. `python3 docs/brand/lint.py` is clean.
|
|
17
|
+
- **The release refuses a lightweight tag.** A step right after checkout in
|
|
18
|
+
`release.yml` fails unless `git cat-file -t <tag>` is `tag`. `git describe` and
|
|
19
|
+
`git submodule status` see annotated tags only; v0.59.1 was cut lightweight and
|
|
20
|
+
the hub read super-ux as its previous release.
|
|
21
|
+
|
|
1
22
|
## 0.59.1 — inventories and audits work without delegation
|
|
2
23
|
|
|
3
24
|
Brand inventory, scenario inventory and UX audit now use the same areas, batches
|
package/bin/super-ux.js
CHANGED
|
@@ -294,13 +294,17 @@ function printUpdateLine() {
|
|
|
294
294
|
|
|
295
295
|
function installClaudePlugin() {
|
|
296
296
|
console.log(`\n--- Claude Code plugin ---`);
|
|
297
|
-
|
|
297
|
+
// The probe's argv is the one source of its `command`, exactly as run()
|
|
298
|
+
// derives it: the typed result carries it, nothing prints it, so it is not
|
|
299
|
+
// interface copy and gets no strings.md row (brand-lint B022, retired).
|
|
300
|
+
const probeArgv = ['claude', '--version'];
|
|
301
|
+
const probe = spawnSync(probeArgv[0], probeArgv.slice(1), { stdio: 'ignore' });
|
|
298
302
|
if (probe.error && probe.error.code === 'ENOENT') {
|
|
299
303
|
console.log(`claude CLI not found. Run inside Claude Code instead:
|
|
300
304
|
/plugin marketplace add ${REPO}
|
|
301
305
|
/plugin install super-ux@super-ux`);
|
|
302
306
|
return { status: 'unsupported', code: null, signal: null,
|
|
303
|
-
command: '
|
|
307
|
+
command: probeArgv.join(' ') };
|
|
304
308
|
}
|
|
305
309
|
if (run('claude', ['plugin', 'marketplace', 'add', REPO]).status !== 'ok') {
|
|
306
310
|
console.log('(marketplace may already be added, continuing)');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "super-ux",
|
|
3
|
-
"version": "0.59.
|
|
3
|
+
"version": "0.59.2",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"test": "python3 test/validate.py && python3 test/brand_lint_test.py && python3 test/display_copy_test.py && python3 test/ux_lint_test.py && python3 docs/ux/lint.py && python3 docs/brand/lint.py && node test/installer_test.js && npm run test:audit",
|
|
6
6
|
"test:audit": "for t in test/audit_regressions/*.py; do python3 \"$t\" || exit 1; done"
|
|
@@ -560,16 +560,38 @@ def code_literals(text: str) -> list[str]:
|
|
|
560
560
|
Comments must already be stripped by the caller -- `_strip_comments` leaves
|
|
561
561
|
literals untouched, which is what makes both patterns below safe to run.
|
|
562
562
|
"""
|
|
563
|
-
|
|
564
|
-
|
|
563
|
+
return [lit for lit, _line in code_literals_at(text)]
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def code_literals_at(text: str) -> list[tuple[str, int]]:
|
|
567
|
+
"""`code_literals`, each with the 1-based line its first character is on.
|
|
568
|
+
|
|
569
|
+
B022 reported every unregistered literal at line 0 -- `bin/super-ux.js:0`
|
|
570
|
+
for a string on line 303 -- so the finding named a file and left the reader
|
|
571
|
+
to search it. The line is counted in the comment-stripped text, which
|
|
572
|
+
`_strip_comments` keeps line-for-line with the source (a block comment
|
|
573
|
+
leaves its newlines behind), so the number is the source's own.
|
|
574
|
+
"""
|
|
575
|
+
def line_at(offset: int) -> int:
|
|
576
|
+
return text.count("\n", 0, offset) + 1
|
|
577
|
+
|
|
578
|
+
out: list[tuple[str, int]] = [
|
|
579
|
+
(m.group(2), line_at(m.start(2))) for m in LITERAL_RE.finditer(text)
|
|
580
|
+
]
|
|
581
|
+
for m in TEMPLATE_RE.finditer(text):
|
|
582
|
+
block = m.group(1)
|
|
565
583
|
if "\n" not in block:
|
|
566
584
|
continue # single-line backticks are already in `out`
|
|
585
|
+
cursor = 0
|
|
567
586
|
for para in re.split(r"\n\s*\n", block):
|
|
587
|
+
at = block.find(para, cursor)
|
|
588
|
+
cursor = at + len(para)
|
|
568
589
|
joined = " ".join(
|
|
569
590
|
line.strip() for line in para.splitlines() if line.strip()
|
|
570
591
|
)
|
|
571
592
|
if len(joined) >= 3:
|
|
572
|
-
|
|
593
|
+
lead = len(para) - len(para.lstrip())
|
|
594
|
+
out.append((joined, line_at(m.start(1) + at + lead)))
|
|
573
595
|
return out
|
|
574
596
|
|
|
575
597
|
# A template literal's interpolations split it into pieces, and the pieces are
|
|
@@ -742,10 +764,13 @@ def check_consistency(brand_dir: Path, sources: dict) -> list[Finding]:
|
|
|
742
764
|
body = read(target) or ""
|
|
743
765
|
html = html_copy(body) if target.suffix.lower() in HTML_SUFFIXES else None
|
|
744
766
|
source_copy = " ".join(html.blocks) if html is not None else body
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
767
|
+
located = list(zip(html.interface_texts, html.interface_lines)) \
|
|
768
|
+
if html is not None else [
|
|
769
|
+
(lit, line) for lit, line
|
|
770
|
+
in code_literals_at(_strip_comments(body, target.suffix))
|
|
771
|
+
if _looks_like_copy(lit)
|
|
772
|
+
]
|
|
773
|
+
literals = [lit for lit, _line in located]
|
|
749
774
|
if literals or html is not None:
|
|
750
775
|
# The registry records what a reader sees, so the built page is the
|
|
751
776
|
# authority whenever there is one. Source is the fallback and stays
|
|
@@ -781,10 +806,13 @@ def check_consistency(brand_dir: Path, sources: dict) -> list[Finding]:
|
|
|
781
806
|
swept.add(file_part)
|
|
782
807
|
known = {r["text"] for r in rows}
|
|
783
808
|
trimmed = {t.strip() for t in known}
|
|
784
|
-
|
|
809
|
+
first_line: dict[str, int] = {}
|
|
810
|
+
for lit, line in located: # one report per literal, at its first line
|
|
811
|
+
first_line.setdefault(lit, line)
|
|
812
|
+
for lit, line in first_line.items():
|
|
785
813
|
if lit not in known and lit.strip() not in trimmed:
|
|
786
814
|
findings.append(Finding(
|
|
787
|
-
"B022", SEVERITY_WARN, f"{file_part}",
|
|
815
|
+
"B022", SEVERITY_WARN, f"{file_part}", line,
|
|
788
816
|
f"\"{lit}\" is in the code with no registry row -- "
|
|
789
817
|
f"agree it or retire it",
|
|
790
818
|
))
|
|
@@ -927,6 +955,7 @@ class HTMLCopy(HTMLParser):
|
|
|
927
955
|
self.last_anchor_end = None
|
|
928
956
|
self.controls = []
|
|
929
957
|
self.interface_texts = []
|
|
958
|
+
self.interface_lines = [] # parallel to interface_texts: the control's opening line
|
|
930
959
|
|
|
931
960
|
@property
|
|
932
961
|
def blocked(self):
|
|
@@ -939,7 +968,7 @@ class HTMLCopy(HTMLParser):
|
|
|
939
968
|
attrs.get("style", ""), re.I))
|
|
940
969
|
if not hidden:
|
|
941
970
|
if tag in self.CONTROLS or attrs.get("role", "").lower() in self.INTERFACE_ROLES:
|
|
942
|
-
self.controls.append((tag, len(self.stack), []))
|
|
971
|
+
self.controls.append((tag, len(self.stack), [], self.getpos()[0]))
|
|
943
972
|
if tag == "br" or tag in self.BLOCK or re.fullmatch(r"h[1-6]", tag):
|
|
944
973
|
for control in self.controls:
|
|
945
974
|
control[2].append(" ")
|
|
@@ -978,6 +1007,7 @@ class HTMLCopy(HTMLParser):
|
|
|
978
1007
|
text = " ".join("".join(control[2]).split())
|
|
979
1008
|
if text:
|
|
980
1009
|
self.interface_texts.append(text)
|
|
1010
|
+
self.interface_lines.append(control[3])
|
|
981
1011
|
self.controls.remove(control)
|
|
982
1012
|
if tag == "a" and not self.blocked:
|
|
983
1013
|
self.last_anchor_end = len(self.parts)
|
|
@@ -1106,7 +1136,12 @@ def _strip_comments(text: str, suffix: str) -> str:
|
|
|
1106
1136
|
continue
|
|
1107
1137
|
if not hashed and text.startswith("/*", i):
|
|
1108
1138
|
end = text.find("*/", i + 2)
|
|
1109
|
-
|
|
1139
|
+
stop = n if end == -1 else end + 2
|
|
1140
|
+
# Keep the comment's newlines: B022 reports a literal by its line
|
|
1141
|
+
# in THIS text, so a stripped multi-line comment must not shift
|
|
1142
|
+
# every line after it.
|
|
1143
|
+
out.append("\n" * text.count("\n", i, stop))
|
|
1144
|
+
i = stop
|
|
1110
1145
|
continue
|
|
1111
1146
|
out.append(ch)
|
|
1112
1147
|
i += 1
|