@zgeoff/chex 0.0.7 → 0.0.9
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/package.json
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { writeStdout } from '@zgeoff/std';
|
|
4
3
|
import { runProgram } from '../run-program.ts';
|
|
5
4
|
import type { DependencyGateSources } from './read-dependency-gate-findings.ts';
|
|
6
5
|
import { readDependencyGateFindings } from './read-dependency-gate-findings.ts';
|
|
@@ -41,7 +40,9 @@ export function runDependencyGatesCommand(): number {
|
|
|
41
40
|
|
|
42
41
|
return existsSync(fullPath) ? readFileSync(fullPath, 'utf8') : null;
|
|
43
42
|
},
|
|
44
|
-
writeOutput:
|
|
43
|
+
writeOutput: (text) => {
|
|
44
|
+
process.stdout.write(text);
|
|
45
|
+
},
|
|
45
46
|
writeError: (text) => {
|
|
46
47
|
process.stderr.write(text);
|
|
47
48
|
},
|
package/src/docs/check-skill.sh
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
# .gitattributes.
|
|
9
9
|
# Fails on: a missing name or description, a name that differs from the folder or breaks the name
|
|
10
10
|
# format, a description over 1,024 characters, a plain description that holds a colon YAML reads as a
|
|
11
|
-
# key, a link with a line anchor outside code, and a SKILL.md body over 1,700 words
|
|
12
|
-
#
|
|
13
|
-
# Warns on: a SKILL.md body over 1,500 words, or over 4,000 for a rule set, a SKILL.md over 500
|
|
11
|
+
# key, a link with a line anchor outside code, and a SKILL.md body over 1,700 words.
|
|
12
|
+
# Warns on: a SKILL.md body over 1,500 words, a SKILL.md over 500
|
|
14
13
|
# lines, a reference that SKILL.md does not list under ## References, a reference over 100 lines
|
|
15
|
-
# with two or more headings and no contents list near its top, a
|
|
16
|
-
#
|
|
14
|
+
# with two or more headings and no contents list near its top, a code-formatted span outside a table that names an existing repo file, and a bullet in
|
|
15
|
+
# a rule set's SKILL.md that opens with an article or "Each" and a noun, a code span and a verb, or a
|
|
16
|
+
# label and a colon.
|
|
17
17
|
set -uo pipefail
|
|
18
18
|
|
|
19
19
|
if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
|
|
@@ -119,6 +119,7 @@ sub check_skill {
|
|
|
119
119
|
warn_about($skill, scalar(@lines) . " lines, over 500; check that each section is one most tasks need")
|
|
120
120
|
if @lines > 500;
|
|
121
121
|
check_words($skill, @lines);
|
|
122
|
+
check_bullets($skill, @lines) if is_rule_set(@lines);
|
|
122
123
|
check_frontmatter($dir, $skill, @lines);
|
|
123
124
|
|
|
124
125
|
open my $list, "-|", "git", "-c", "core.quotePath=false", "ls-files", "--cached", "--others",
|
|
@@ -143,33 +144,17 @@ sub check_skill {
|
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
# Counts the words of the body after the frontmatter, code blocks included
|
|
147
|
-
#
|
|
148
|
-
#
|
|
147
|
+
# Counts the words of the body after the frontmatter, code blocks included. After a compaction,
|
|
148
|
+
# Claude Code brings back only the first 5,000 tokens of each loaded skill, about 1,900 words of a
|
|
149
|
+
# skill, so past the error limit the end of the skill is lost; past the warning limit an agent starts
|
|
150
|
+
# to skim. A rule set gets the same limits, because the agent needs every rule in it.
|
|
149
151
|
sub check_words {
|
|
150
152
|
my ($skill, @lines) = @_;
|
|
151
153
|
my ($warn, $limit) = (1500, 1700);
|
|
152
|
-
my
|
|
154
|
+
my $start = 0;
|
|
153
155
|
if (@lines && $lines[0] eq "---") {
|
|
154
156
|
for my $i (1 .. $#lines) {
|
|
155
|
-
|
|
156
|
-
if ($line eq "---") { $start = $i + 1; last }
|
|
157
|
-
if ($line =~ /^metadata:(?:\s+#.*)?\s*$/) { ($in_metadata, $indent) = (1, undef); next }
|
|
158
|
-
if ($line =~ /^metadata:\s*\{(.*)\}\s*(?:#.*)?$/) {
|
|
159
|
-
for my $entry (split_flow_entries($1)) {
|
|
160
|
-
($warn, $limit) = (4000, 5000) if $entry =~ /^\s*kind:\s*(.*?)\s*$/ && unquote($1) eq "rule-set";
|
|
161
|
-
}
|
|
162
|
-
$in_metadata = 0;
|
|
163
|
-
next;
|
|
164
|
-
}
|
|
165
|
-
# A comment-only line, at any indent, is neither a key nor the end of the mapping.
|
|
166
|
-
next if $line =~ /^\s*#/;
|
|
167
|
-
$in_metadata = 0 if $line =~ /^\S/;
|
|
168
|
-
next unless $in_metadata && $line =~ /^(\s+)(\S.*?)\s*$/;
|
|
169
|
-
# Only a key of metadata itself counts, never a deeper line, such as the text of a block value.
|
|
170
|
-
$indent //= length $1;
|
|
171
|
-
next unless length($1) == $indent && $2 =~ /^kind:\s*(.*)$/;
|
|
172
|
-
($warn, $limit) = (4000, 5000) if unquote($1) eq "rule-set";
|
|
157
|
+
if ($lines[$i] eq "---") { $start = $i + 1; last }
|
|
173
158
|
}
|
|
174
159
|
}
|
|
175
160
|
my $words = 0;
|
|
@@ -183,6 +168,73 @@ sub check_words {
|
|
|
183
168
|
}
|
|
184
169
|
}
|
|
185
170
|
|
|
171
|
+
# Returns true when the frontmatter sets kind: rule-set under metadata, in a block or a flow mapping.
|
|
172
|
+
sub is_rule_set {
|
|
173
|
+
my (@lines) = @_;
|
|
174
|
+
return 0 unless @lines && $lines[0] eq "---";
|
|
175
|
+
my ($in_metadata, $indent) = (0, undef);
|
|
176
|
+
for my $i (1 .. $#lines) {
|
|
177
|
+
my $line = $lines[$i];
|
|
178
|
+
return 0 if $line eq "---";
|
|
179
|
+
if ($line =~ /^metadata:(?:\s+#.*)?\s*$/) { ($in_metadata, $indent) = (1, undef); next }
|
|
180
|
+
if ($line =~ /^metadata:\s*\{(.*)\}\s*(?:#.*)?$/) {
|
|
181
|
+
for my $entry (split_flow_entries($1)) {
|
|
182
|
+
return 1 if $entry =~ /^\s*kind:\s*(.*?)\s*$/ && unquote($1) eq "rule-set";
|
|
183
|
+
}
|
|
184
|
+
$in_metadata = 0;
|
|
185
|
+
next;
|
|
186
|
+
}
|
|
187
|
+
# A comment-only line, at any indent, is neither a key nor the end of the mapping.
|
|
188
|
+
next if $line =~ /^\s*#/;
|
|
189
|
+
$in_metadata = 0 if $line =~ /^\S/;
|
|
190
|
+
next unless $in_metadata && $line =~ /^(\s+)(\S.*?)\s*$/;
|
|
191
|
+
# Only a key of metadata itself counts, never a deeper line, such as the text of a block value.
|
|
192
|
+
$indent //= length $1;
|
|
193
|
+
return 1 if length($1) == $indent && $2 =~ /^kind:\s*(.*)$/ && unquote($1) eq "rule-set";
|
|
194
|
+
}
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# A rule set is a list of bullets the agent applies while it does other work, so each bullet opens
|
|
199
|
+
# with what the agent does, a ban or a condition. A bullet that opens with a thing as its subject,
|
|
200
|
+
# or with a label and a colon, leaves the agent to work out the action.
|
|
201
|
+
sub check_bullets {
|
|
202
|
+
my ($skill, @lines) = @_;
|
|
203
|
+
my ($open, $line_number, $in_frontmatter) = ("", 0, @lines && $lines[0] eq "---");
|
|
204
|
+
for my $line (@lines) {
|
|
205
|
+
$line_number++;
|
|
206
|
+
if ($in_frontmatter) {
|
|
207
|
+
$in_frontmatter = 0 if $line_number > 1 && $line eq "---";
|
|
208
|
+
next;
|
|
209
|
+
}
|
|
210
|
+
# A fence may open inside a list item, after its marker, but closes only on a line of its own.
|
|
211
|
+
# The info string of a backtick fence never holds a backtick, so a line such as
|
|
212
|
+
# "```name``` runs" is an inline code span, not a fence.
|
|
213
|
+
my $marker_pattern = $open eq "" ? qr/^\s*(?:[-*+]\s+|\d+[.)]\s+)?(`{3,}|~{3,})(.*)$/
|
|
214
|
+
: qr/^\s*(`{3,}|~{3,})(.*)$/;
|
|
215
|
+
if ($line =~ $marker_pattern && !($open eq "" && substr($1, 0, 1) eq "`" && $2 =~ /`/)) {
|
|
216
|
+
my ($marker, $rest) = ($1, $2);
|
|
217
|
+
if ($open eq "") { $open = $marker }
|
|
218
|
+
elsif (substr($marker, 0, 1) eq substr($open, 0, 1) && length($marker) >= length($open)
|
|
219
|
+
&& $rest =~ /^\s*$/) { $open = "" }
|
|
220
|
+
next;
|
|
221
|
+
}
|
|
222
|
+
next if $open ne "";
|
|
223
|
+
next unless $line =~ /^\s*[-*+]\s+(.*)$/;
|
|
224
|
+
my $text = $1 =~ s/^\*\*//r;
|
|
225
|
+
my $problem;
|
|
226
|
+
if ($text =~ /^(A|An|The|Each|Every)\s+[\w`]/) {
|
|
227
|
+
$problem = "opens with \"$1\" and a noun";
|
|
228
|
+
} elsif (($text =~ /^`[^`]+`\s+([a-z]+)\b/ ? $1 : "") =~ /^(?!(?:as|this|its)\z)(?:are|have|can|may|must|do|[a-z]+s)\z/) {
|
|
229
|
+
$problem = "opens with a code span and a verb";
|
|
230
|
+
} elsif ($text =~ /^([A-Z][\w-]*(?:\s[\w-]+){0,2}):(?:\*\*)?\s/ && $1 !~ /^(?:When|If|In|Never|No)\b/) {
|
|
231
|
+
$problem = "opens with the label \"$1:\"";
|
|
232
|
+
}
|
|
233
|
+
warn_about("$skill:$line_number", "bullet $problem; open it with what the agent does, a ban such as \"Never\", or a condition such as \"When\"")
|
|
234
|
+
if $problem;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
186
238
|
# Splits the inside of a YAML flow mapping at each comma that sits outside a quoted value and a
|
|
187
239
|
# nested flow collection, so a comma inside a value never starts an entry.
|
|
188
240
|
sub split_flow_entries {
|
|
@@ -361,7 +413,6 @@ sub check_markdown {
|
|
|
361
413
|
$target =~ s/^<(.*)>$/$1/;
|
|
362
414
|
fail("$file:$line_number", "link $target has a line anchor; name the target in the link text instead")
|
|
363
415
|
if $target =~ /#L[0-9]+/;
|
|
364
|
-
check_reference_link($dir, $file, $line_number, $target) if $reference;
|
|
365
416
|
}
|
|
366
417
|
|
|
367
418
|
# A span inside link text is already a link, so only the spans left after links drop are read.
|
|
@@ -384,16 +435,6 @@ sub check_markdown {
|
|
|
384
435
|
}
|
|
385
436
|
}
|
|
386
437
|
|
|
387
|
-
sub check_reference_link {
|
|
388
|
-
my ($dir, $file, $line_number, $target) = @_;
|
|
389
|
-
return if $target eq "" || $target =~ m{^(?:[a-z][a-z0-9+.-]*:|//|#)}i;
|
|
390
|
-
my ($path) = split /[#?]/, $target, 2;
|
|
391
|
-
my $resolved = resolve_path($file, $path);
|
|
392
|
-
return unless defined $resolved && $resolved ne $file;
|
|
393
|
-
return unless $resolved =~ m{^\Q$dir\E/references/.*\.md\z};
|
|
394
|
-
warn_about("$file:$line_number", "links to another reference ($resolved); link it from SKILL.md instead");
|
|
395
|
-
}
|
|
396
|
-
|
|
397
438
|
sub resolve_path {
|
|
398
439
|
my ($file, $path) = @_;
|
|
399
440
|
return undef if $path eq "";
|