bantamkit-mcp 0.29.0 → 0.29.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/dist/docread.d.ts +82 -8
- package/dist/docread.js +348 -96
- package/dist/mcp/pyargs.d.ts +19 -0
- package/dist/mcp/pyargs.js +12 -1
- package/dist/mcp/server.js +128 -34
- package/dist/memory/cli.js +43 -1
- package/dist/memory/factfile.d.ts +13 -1
- package/dist/memory/factfile.js +7 -17
- package/dist/memory/pyfs.d.ts +20 -12
- package/dist/memory/pyfs.js +21 -51
- package/dist/memory/store.d.ts +43 -0
- package/dist/memory/store.js +61 -3
- package/dist/pysem.d.ts +81 -0
- package/dist/pysem.js +126 -0
- package/dist/skillaudit.js +31 -38
- package/package.json +2 -2
package/dist/memory/store.js
CHANGED
|
@@ -742,28 +742,86 @@ export class MemoryStore {
|
|
|
742
742
|
throw error;
|
|
743
743
|
}
|
|
744
744
|
}
|
|
745
|
+
/**
|
|
746
|
+
* `NAME_RE` ENFORCED BEFORE ANY SYSCALL, matching `archive` (job44, z). `restore` has
|
|
747
|
+
* built `facts/<name>.md` out of whatever it was handed since it was written; measured on
|
|
748
|
+
* macOS before this check, both runtimes: `restore ALPHA` against a live
|
|
749
|
+
* `archive/ALPHA.md` exited 0 and wrote `facts/ALPHA.md` — the case-insensitive
|
|
750
|
+
* filesystem matched the archived source, and nothing asked the store's own naming rule
|
|
751
|
+
* about the destination it was about to create.
|
|
752
|
+
*
|
|
753
|
+
* THE OCCUPIED-DESTINATION GUARD NOW ALSO CATCHES A DANGLING SYMLINK (job44, z). `reachable`
|
|
754
|
+
* is `pyExists`, `Path.exists()`'s equivalent, which FOLLOWS symlinks — so a dangling
|
|
755
|
+
* symlink at `facts/<name>.md` is an occupied directory entry the guard alone cannot see:
|
|
756
|
+
* measured, `pyLexists` true and `pyExists` false, and without the check below the guard
|
|
757
|
+
* passed and execution reached the pre-move `facts()` parse, which then raised a raw
|
|
758
|
+
* `FileNotFoundError` reading the dangling link it was about to parse as a fact. `pyLexists`
|
|
759
|
+
* is only consulted when `reachable` returned `false` WITHOUT raising — an EACCES on the
|
|
760
|
+
* same stat already escaped out of `reachable` first, so this never masks that distinction.
|
|
761
|
+
*
|
|
762
|
+
* TWO SENTENCES, NOT ONE, on job44's reconciliation with the reference. The first two
|
|
763
|
+
* answers tried here were both wrong: treating a dangling link the same as `reachable` said
|
|
764
|
+
* "already live", and a live fact is exactly what a dangling link is NOT; letting it fall
|
|
765
|
+
* through to `facts()`'s pre-read dressed an unrelated `FileNotFoundError` as a move
|
|
766
|
+
* failure when no move had been attempted (both runtimes made one of these two mistakes).
|
|
767
|
+
* The link occupies the path — refusing is still correct — but the REASON is that it
|
|
768
|
+
* cannot be read as a fact, not that a fact is already there, so the second branch below
|
|
769
|
+
* says that instead and never reaches `facts()` at all.
|
|
770
|
+
*
|
|
771
|
+
* THIS IS THE OPPOSITE OUTCOME FROM `archive`'s mirror of the same defect: `archive`'s
|
|
772
|
+
* forward move is `pyReplace`, which overwrites a dangling link identically on every
|
|
773
|
+
* platform, so the guard there is deliberately left as `reachable` alone (see the test
|
|
774
|
+
* pinning that). Closing the guard here removes the ONLY path that could reach an occupied
|
|
775
|
+
* destination, on both runtimes, rather than picking a side of what would happen there.
|
|
776
|
+
*
|
|
777
|
+
* AND THE FORWARD MOVE ITSELF NO LONGER DIVERGES — this sentence used to say it did, and
|
|
778
|
+
* was stale against its own commit. `restore`'s forward move is `pyReplace` on this side
|
|
779
|
+
* and `source.replace(destination)` on the reference (`store.py`, `restore`), changed in
|
|
780
|
+
* the SAME commit that added the guard above; at its parent `f484c70` the reference still
|
|
781
|
+
* called `source.rename(destination)`, which is what this comment described.
|
|
782
|
+
* `d239480`'s `rename`-versus-`pyReplace` difference is therefore REMOVED here, not merely
|
|
783
|
+
* made unreachable, and the same is true of `archive` (fixed by review round 5) and of
|
|
784
|
+
* `compact` (fixed by `d239480` itself). Every forward move in this store is `replace` on
|
|
785
|
+
* both sides; the only `rename` calls left on the reference are the two ROLLBACKS, which
|
|
786
|
+
* move back onto a path the forward move has just emptied — see `docs/porting.md`.
|
|
787
|
+
*/
|
|
745
788
|
restore(name) {
|
|
789
|
+
if (!NAME_RE.test(name || '')) {
|
|
790
|
+
throw new MemoryValidationError(`invalid name '${name}'; must match ${NAME_PATTERN}`);
|
|
791
|
+
}
|
|
746
792
|
const archive = pyJoin(this.root, 'archive');
|
|
747
793
|
const source = pyJoin(archive, `${name}.md`);
|
|
748
794
|
if (!this.reachable(source, archive, ARCHIVE_UNREACHABLE)) {
|
|
749
795
|
throw new MemoryValidationError(`no archived fact '${name}' under ${archive}`);
|
|
750
796
|
}
|
|
751
797
|
const destination = this.factPath(name);
|
|
752
|
-
|
|
798
|
+
const facts = pyJoin(this.root, 'facts');
|
|
799
|
+
if (this.reachable(destination, facts, FACTS_UNREACHABLE)) {
|
|
753
800
|
throw new MemoryValidationError(`fact '${name}' is already live; refusing to overwrite it from archive`);
|
|
754
801
|
}
|
|
802
|
+
if (pyLexists(destination)) {
|
|
803
|
+
throw new MemoryValidationError(`facts/${name}.md already exists but cannot be read as a fact; refusing to restore over it`);
|
|
804
|
+
}
|
|
755
805
|
this.facts(); // parse BEFORE the move, not after it
|
|
756
|
-
pyMkdirParents(
|
|
806
|
+
pyMkdirParents(facts);
|
|
757
807
|
pyReplace(source, destination);
|
|
808
|
+
// THE FINAL `rebuildIndex()` IS INSIDE THIS TRY, and job44's reconciliation is why: it
|
|
809
|
+
// used to sit AFTER this block, uncovered, so a failure there — measured by driving the
|
|
810
|
+
// real fault (`index.md` a directory) rather than reading the code shape — left the fact
|
|
811
|
+
// STUCK in `facts/`, GONE from `archive/`, with no rollback attempted at all, because
|
|
812
|
+
// `checkIndexBudget` never touches disk and passes cleanly first. `archive`'s single try
|
|
813
|
+
// already covered its own write-then-check in one block; this brings restore's rollback
|
|
814
|
+
// up to the same coverage `runtime-py` gave `_check_index_budget` and the closing
|
|
815
|
+
// `_rebuild_index` together, in the same reconciliation.
|
|
758
816
|
try {
|
|
759
817
|
this.checkIndexBudget();
|
|
818
|
+
this.rebuildIndex();
|
|
760
819
|
}
|
|
761
820
|
catch (error) {
|
|
762
821
|
pyReplace(destination, source);
|
|
763
822
|
this.rebuildIndex();
|
|
764
823
|
throw error;
|
|
765
824
|
}
|
|
766
|
-
this.rebuildIndex();
|
|
767
825
|
}
|
|
768
826
|
/**
|
|
769
827
|
* The index as it should be on disk, derived from `facts/` and nothing else.
|
package/dist/pysem.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CPython string semantics this runtime needs in more than one place, written ONCE.
|
|
3
|
+
*
|
|
4
|
+
* `docs/roadmap-toolbox.md` row 8, entry (n): `pyStrip`, `cmpCodepoint` and `pyRepr` had each
|
|
5
|
+
* been ported twice — once in `docread.ts` and once under `memory/` — as independent
|
|
6
|
+
* re-derivations of the same three CPython behaviours. Two implementations of one semantic are
|
|
7
|
+
* two chances to be right and one certainty of drifting, and neither copy had a test that
|
|
8
|
+
* compared it to the other.
|
|
9
|
+
*
|
|
10
|
+
* **The pairs were diffed before they were merged**, because unifying two functions that are
|
|
11
|
+
* not the same function is a behaviour change wearing a refactor's clothes:
|
|
12
|
+
*
|
|
13
|
+
* * `pyStrip` — the two agreed on the set of characters `str.isspace()` is true for (29 each,
|
|
14
|
+
* with an empty symmetric difference over all 1,114,112 codepoints) and on 200,000 random
|
|
15
|
+
* strings built from that alphabet. Kept: the REGEX spelling, because the set-based one
|
|
16
|
+
* spreads the whole string into an array of characters first, and this is called once per
|
|
17
|
+
* rendered row.
|
|
18
|
+
* * `pyRepr` — byte-identical output on every one of the 1,114,112 codepoints and on a pool of
|
|
19
|
+
* quoting edge cases. `docread`'s explicit `Cc|Cf|Cs|Co|Cn|Zl|Zp|Zs` class and `memory`'s
|
|
20
|
+
* `\p{C}|\p{Z}` are the same set spelled two ways. Kept: the precompiled class.
|
|
21
|
+
* * `cmpCodepoint` — the same ORDER on every pair (0 sign disagreements), and different
|
|
22
|
+
* NUMBERS: `docread`'s returned the codepoint difference and the `memory` copy returns
|
|
23
|
+
* -1/0/1, so `cmp('', 'ab')` was -1 there and -2 here. Kept: the `memory` spelling to the
|
|
24
|
+
* digit, because every importer outside `docread.ts` already sees those numbers, while
|
|
25
|
+
* `docread`'s copy had no importer at all and one use — `Array.prototype.sort`, which reads
|
|
26
|
+
* the sign and nothing else.
|
|
27
|
+
*
|
|
28
|
+
* **A THIRD COPY SURVIVED THAT CLOSURE AND WAS FOLDED IN ON 2026-09-06 (job44, unit F4).**
|
|
29
|
+
* "written ONCE" was not true when it was written: entry (n) named `docread.ts` and `memory/`
|
|
30
|
+
* and nothing else, so `skillaudit.ts` — product surface, the eleventh MCP tool — kept its own
|
|
31
|
+
* `pyStrip` on a 29-codepoint Set and its own `cmpCodepoint` in the `cx - cy` spelling, the two
|
|
32
|
+
* this module had just rejected. Both now import from here, gated first by the same method:
|
|
33
|
+
* an empty symmetric difference between the two whitespace sets over every codepoint, 0
|
|
34
|
+
* disagreements on `pyStrip` over 200,000 random strings, 0 disagreements on a
|
|
35
|
+
* `` `^${PY_WS_CLASS}` `` regex against that module's Set-membership `startsWithSpace` over
|
|
36
|
+
* every codepoint, and — the reason the gate is not optional — `cmpCodepoint` agreeing on SIGN
|
|
37
|
+
* in all of 324 exhaustive plus 200,000 random pairs while disagreeing on the NUMBER in
|
|
38
|
+
* 167,851 of them. `skillaudit.ts`'s six uses read the sign alone, which is what made the
|
|
39
|
+
* substitution safe; its header carries the reading and the caveat.
|
|
40
|
+
*
|
|
41
|
+
* Nothing here is re-exported from `index.ts`: the names stay exported from the modules that
|
|
42
|
+
* always exported them, so this is a delegation and not a surface change.
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* The characters `str.isspace()` is true for — what `strip()` and `split()` consume.
|
|
46
|
+
*
|
|
47
|
+
* NOT `String.prototype.trim()`'s set. Measured over all 0x110000 codepoints: Python strips 29,
|
|
48
|
+
* JS strips 25, and the two disagree on six — Python also strips U+001C..U+001F and U+0085, JS
|
|
49
|
+
* also strips U+FEFF. `_write_fact` ends with `fact.body.strip() + "\n"`, so each of those six
|
|
50
|
+
* is a byte of difference in a file the memory store then compares, hashes and diffs; in
|
|
51
|
+
* `docread` the same six decide whether a row is blank.
|
|
52
|
+
*/
|
|
53
|
+
export declare const PY_WS = "\t\n\v\f\r\u001C\u001D\u001E\u001F \u0085\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000";
|
|
54
|
+
/** `PY_WS` as a character class, with the four characters a class gives meaning escaped. */
|
|
55
|
+
export declare const PY_WS_CLASS: string;
|
|
56
|
+
/** `str.strip()` with no argument. */
|
|
57
|
+
export declare function pyStrip(s: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* Python's `<` on `str`, which is CODEPOINT order. JS compares UTF-16 code units.
|
|
60
|
+
*
|
|
61
|
+
* They disagree for every astral character: `['\u{1F414}.md', '!.md'].sort()` puts the chicken
|
|
62
|
+
* first because its lead surrogate is 0xD83D, and Python puts it last because 0x1F414 > 0xFF01.
|
|
63
|
+
* `_fact_paths` sorts, `recall` breaks score ties on the name, and the index is written in that
|
|
64
|
+
* order — so a single astral fact name is enough to make two `index.md` files that differ. In
|
|
65
|
+
* `docread` the same order decides which omission reason is rendered first.
|
|
66
|
+
*/
|
|
67
|
+
export declare function cmpCodepoint(a: string, b: string): number;
|
|
68
|
+
/**
|
|
69
|
+
* `repr(str)` — the refusal sentences' quoting, and `_pinned_store`'s `{raw!r}`.
|
|
70
|
+
*
|
|
71
|
+
* Python quotes with `'` unless the value contains a `'` and no `"`; it escapes `\\`, the
|
|
72
|
+
* quote, `\t`, `\n`, `\r`, and every codepoint that is not `str.isprintable()` — that is,
|
|
73
|
+
* everything in a `C*` category and every separator except the space itself. The escapes are
|
|
74
|
+
* `\xNN` below U+0100, `\uNNNN` below U+10000, `\UNNNNNNNN` above.
|
|
75
|
+
*
|
|
76
|
+
* The one place it can drift is a codepoint whose category changed between CPython 3.12's
|
|
77
|
+
* Unicode 15.0 and the ICU this Node was built against: a newly assigned character is `Cn`
|
|
78
|
+
* (not printable, escaped) for Python and assigned (printable, raw) here. A path made of
|
|
79
|
+
* brand-new codepoints is the only input that reaches it.
|
|
80
|
+
*/
|
|
81
|
+
export declare function pyRepr(s: string): string;
|
package/dist/pysem.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CPython string semantics this runtime needs in more than one place, written ONCE.
|
|
3
|
+
*
|
|
4
|
+
* `docs/roadmap-toolbox.md` row 8, entry (n): `pyStrip`, `cmpCodepoint` and `pyRepr` had each
|
|
5
|
+
* been ported twice — once in `docread.ts` and once under `memory/` — as independent
|
|
6
|
+
* re-derivations of the same three CPython behaviours. Two implementations of one semantic are
|
|
7
|
+
* two chances to be right and one certainty of drifting, and neither copy had a test that
|
|
8
|
+
* compared it to the other.
|
|
9
|
+
*
|
|
10
|
+
* **The pairs were diffed before they were merged**, because unifying two functions that are
|
|
11
|
+
* not the same function is a behaviour change wearing a refactor's clothes:
|
|
12
|
+
*
|
|
13
|
+
* * `pyStrip` — the two agreed on the set of characters `str.isspace()` is true for (29 each,
|
|
14
|
+
* with an empty symmetric difference over all 1,114,112 codepoints) and on 200,000 random
|
|
15
|
+
* strings built from that alphabet. Kept: the REGEX spelling, because the set-based one
|
|
16
|
+
* spreads the whole string into an array of characters first, and this is called once per
|
|
17
|
+
* rendered row.
|
|
18
|
+
* * `pyRepr` — byte-identical output on every one of the 1,114,112 codepoints and on a pool of
|
|
19
|
+
* quoting edge cases. `docread`'s explicit `Cc|Cf|Cs|Co|Cn|Zl|Zp|Zs` class and `memory`'s
|
|
20
|
+
* `\p{C}|\p{Z}` are the same set spelled two ways. Kept: the precompiled class.
|
|
21
|
+
* * `cmpCodepoint` — the same ORDER on every pair (0 sign disagreements), and different
|
|
22
|
+
* NUMBERS: `docread`'s returned the codepoint difference and the `memory` copy returns
|
|
23
|
+
* -1/0/1, so `cmp('', 'ab')` was -1 there and -2 here. Kept: the `memory` spelling to the
|
|
24
|
+
* digit, because every importer outside `docread.ts` already sees those numbers, while
|
|
25
|
+
* `docread`'s copy had no importer at all and one use — `Array.prototype.sort`, which reads
|
|
26
|
+
* the sign and nothing else.
|
|
27
|
+
*
|
|
28
|
+
* **A THIRD COPY SURVIVED THAT CLOSURE AND WAS FOLDED IN ON 2026-09-06 (job44, unit F4).**
|
|
29
|
+
* "written ONCE" was not true when it was written: entry (n) named `docread.ts` and `memory/`
|
|
30
|
+
* and nothing else, so `skillaudit.ts` — product surface, the eleventh MCP tool — kept its own
|
|
31
|
+
* `pyStrip` on a 29-codepoint Set and its own `cmpCodepoint` in the `cx - cy` spelling, the two
|
|
32
|
+
* this module had just rejected. Both now import from here, gated first by the same method:
|
|
33
|
+
* an empty symmetric difference between the two whitespace sets over every codepoint, 0
|
|
34
|
+
* disagreements on `pyStrip` over 200,000 random strings, 0 disagreements on a
|
|
35
|
+
* `` `^${PY_WS_CLASS}` `` regex against that module's Set-membership `startsWithSpace` over
|
|
36
|
+
* every codepoint, and — the reason the gate is not optional — `cmpCodepoint` agreeing on SIGN
|
|
37
|
+
* in all of 324 exhaustive plus 200,000 random pairs while disagreeing on the NUMBER in
|
|
38
|
+
* 167,851 of them. `skillaudit.ts`'s six uses read the sign alone, which is what made the
|
|
39
|
+
* substitution safe; its header carries the reading and the caveat.
|
|
40
|
+
*
|
|
41
|
+
* Nothing here is re-exported from `index.ts`: the names stay exported from the modules that
|
|
42
|
+
* always exported them, so this is a delegation and not a surface change.
|
|
43
|
+
*/
|
|
44
|
+
// ------------------------------------------------------------------------- str.strip()
|
|
45
|
+
/**
|
|
46
|
+
* The characters `str.isspace()` is true for — what `strip()` and `split()` consume.
|
|
47
|
+
*
|
|
48
|
+
* NOT `String.prototype.trim()`'s set. Measured over all 0x110000 codepoints: Python strips 29,
|
|
49
|
+
* JS strips 25, and the two disagree on six — Python also strips U+001C..U+001F and U+0085, JS
|
|
50
|
+
* also strips U+FEFF. `_write_fact` ends with `fact.body.strip() + "\n"`, so each of those six
|
|
51
|
+
* is a byte of difference in a file the memory store then compares, hashes and diffs; in
|
|
52
|
+
* `docread` the same six decide whether a row is blank.
|
|
53
|
+
*/
|
|
54
|
+
export const PY_WS = '\t\n\x0b\x0c\r\x1c\x1d\x1e\x1f \x85\xa0
';
|
|
55
|
+
/** `PY_WS` as a character class, with the four characters a class gives meaning escaped. */
|
|
56
|
+
export const PY_WS_CLASS = `[${PY_WS.replace(/[\]\\^-]/g, '\\$&')}]`;
|
|
57
|
+
const PY_STRIP = new RegExp(`^${PY_WS_CLASS}+|${PY_WS_CLASS}+$`, 'g');
|
|
58
|
+
/** `str.strip()` with no argument. */
|
|
59
|
+
export function pyStrip(s) {
|
|
60
|
+
return s.replace(PY_STRIP, '');
|
|
61
|
+
}
|
|
62
|
+
// ------------------------------------------------------------------------ sorted() on str
|
|
63
|
+
/**
|
|
64
|
+
* Python's `<` on `str`, which is CODEPOINT order. JS compares UTF-16 code units.
|
|
65
|
+
*
|
|
66
|
+
* They disagree for every astral character: `['\u{1F414}.md', '!.md'].sort()` puts the chicken
|
|
67
|
+
* first because its lead surrogate is 0xD83D, and Python puts it last because 0x1F414 > 0xFF01.
|
|
68
|
+
* `_fact_paths` sorts, `recall` breaks score ties on the name, and the index is written in that
|
|
69
|
+
* order — so a single astral fact name is enough to make two `index.md` files that differ. In
|
|
70
|
+
* `docread` the same order decides which omission reason is rendered first.
|
|
71
|
+
*/
|
|
72
|
+
export function cmpCodepoint(a, b) {
|
|
73
|
+
const x = [...a];
|
|
74
|
+
const y = [...b];
|
|
75
|
+
const n = Math.min(x.length, y.length);
|
|
76
|
+
for (let i = 0; i < n; i += 1) {
|
|
77
|
+
const p = x[i].codePointAt(0);
|
|
78
|
+
const q = y[i].codePointAt(0);
|
|
79
|
+
if (p !== q)
|
|
80
|
+
return p < q ? -1 : 1;
|
|
81
|
+
}
|
|
82
|
+
return x.length - y.length;
|
|
83
|
+
}
|
|
84
|
+
// ---------------------------------------------------------------------------- repr(str)
|
|
85
|
+
// `str.isprintable()` is false for these categories (space itself excepted). Precompiled: the
|
|
86
|
+
// refusal sentences build one of these per character of a path.
|
|
87
|
+
const NONPRINTABLE = /[\p{Cc}\p{Cf}\p{Cs}\p{Co}\p{Cn}\p{Zl}\p{Zp}\p{Zs}]/u;
|
|
88
|
+
/**
|
|
89
|
+
* `repr(str)` — the refusal sentences' quoting, and `_pinned_store`'s `{raw!r}`.
|
|
90
|
+
*
|
|
91
|
+
* Python quotes with `'` unless the value contains a `'` and no `"`; it escapes `\\`, the
|
|
92
|
+
* quote, `\t`, `\n`, `\r`, and every codepoint that is not `str.isprintable()` — that is,
|
|
93
|
+
* everything in a `C*` category and every separator except the space itself. The escapes are
|
|
94
|
+
* `\xNN` below U+0100, `\uNNNN` below U+10000, `\UNNNNNNNN` above.
|
|
95
|
+
*
|
|
96
|
+
* The one place it can drift is a codepoint whose category changed between CPython 3.12's
|
|
97
|
+
* Unicode 15.0 and the ICU this Node was built against: a newly assigned character is `Cn`
|
|
98
|
+
* (not printable, escaped) for Python and assigned (printable, raw) here. A path made of
|
|
99
|
+
* brand-new codepoints is the only input that reaches it.
|
|
100
|
+
*/
|
|
101
|
+
export function pyRepr(s) {
|
|
102
|
+
const quote = s.includes("'") && !s.includes('"') ? '"' : "'";
|
|
103
|
+
let out = quote;
|
|
104
|
+
for (const ch of s) {
|
|
105
|
+
const code = ch.codePointAt(0);
|
|
106
|
+
if (ch === quote || ch === '\\')
|
|
107
|
+
out += '\\' + ch;
|
|
108
|
+
else if (ch === '\t')
|
|
109
|
+
out += '\\t';
|
|
110
|
+
else if (ch === '\n')
|
|
111
|
+
out += '\\n';
|
|
112
|
+
else if (ch === '\r')
|
|
113
|
+
out += '\\r';
|
|
114
|
+
else if (ch !== ' ' && NONPRINTABLE.test(ch)) {
|
|
115
|
+
if (code < 0x100)
|
|
116
|
+
out += '\\x' + code.toString(16).padStart(2, '0');
|
|
117
|
+
else if (code < 0x10000)
|
|
118
|
+
out += '\\u' + code.toString(16).padStart(4, '0');
|
|
119
|
+
else
|
|
120
|
+
out += '\\U' + code.toString(16).padStart(8, '0');
|
|
121
|
+
}
|
|
122
|
+
else
|
|
123
|
+
out += ch;
|
|
124
|
+
}
|
|
125
|
+
return out + quote;
|
|
126
|
+
}
|
package/dist/skillaudit.js
CHANGED
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
73
73
|
import { basename, dirname, join } from 'node:path';
|
|
74
74
|
import { BantamError } from './errors.js';
|
|
75
|
+
import { cmpCodepoint, PY_WS_CLASS, pyStrip } from './pysem.js';
|
|
75
76
|
import { reprValue } from './pyjson.js';
|
|
76
77
|
// The file a skill IS. Case-sensitive: the host loads this name and so does this reader.
|
|
77
78
|
export const SKILL_FILE = 'SKILL.md';
|
|
@@ -168,47 +169,39 @@ export class SkillAuditError extends BantamError {
|
|
|
168
169
|
}
|
|
169
170
|
// -------------------------------------------------------------- Python string semantics
|
|
170
171
|
/**
|
|
171
|
-
* `str.strip()
|
|
172
|
-
*
|
|
172
|
+
* `str.strip()` and `sorted()`'s order are `pysem.ts`'s, and no longer a third copy.
|
|
173
|
+
*
|
|
174
|
+
* This module carried its own `pyStrip` (a `PY_SPACE` Set of 29 codepoints, spread over
|
|
175
|
+
* `[...text]`) and its own `cmpCodepoint` (paired iterators returning `cx - cy`) —
|
|
176
|
+
* independently re-derived, and the two spellings `pysem.ts` explicitly REJECTED when it
|
|
177
|
+
* unified `docread.ts`'s and `memory/`'s copies for `docs/roadmap-toolbox.md` row 8, entry
|
|
178
|
+
* (n). That entry named only those two homes, so this third copy survived its closure while
|
|
179
|
+
* `pysem.ts`'s header claimed the semantics were written ONCE.
|
|
180
|
+
*
|
|
181
|
+
* **The pairs were gated before they were merged** (2026-09-06, job44 unit F4, the method U2
|
|
182
|
+
* used, against `dist/pysem.js` with this module's two functions transcribed beside it):
|
|
183
|
+
*
|
|
184
|
+
* * the SET — 29 codepoints on each side, and an EMPTY symmetric difference walking every
|
|
185
|
+
* codepoint from 0 to 0x10FFFF, surrogates skipped;
|
|
186
|
+
* * `pyStrip` — 0 disagreements over 200,000 random strings built from the union alphabet
|
|
187
|
+
* plus `a Z 0 U+1F414 U+FEFF U+200B U+FF1A` as non-space padding (the last three are the
|
|
188
|
+
* characters `String.prototype.trim()` and `str.strip()` argue about);
|
|
189
|
+
* * `startsWithSpace` — 0 disagreements against `` new RegExp(`^${PY_WS_CLASS}`) `` over
|
|
190
|
+
* eight hand-picked shapes AND over `String.fromCodePoint(cp) + 'x'` for every codepoint,
|
|
191
|
+
* which is what lets the Set membership test below become a regex test;
|
|
192
|
+
* * `cmpCodepoint` — **0 sign disagreements** over 324 exhaustive pairs and 200,000 random
|
|
193
|
+
* astral-bearing pairs, and **167,851 NUMBER disagreements** in the same run, the first
|
|
194
|
+
* being `cmp('', 'ab')`: `-1` in the spelling deleted here, `-2` in `pysem.ts`'s. Same
|
|
195
|
+
* order, different function. Safe only because every one of this module's six uses reads
|
|
196
|
+
* the sign alone — `cmpParts` (`!== 0`, and itself only ever a `.sort` comparator), the
|
|
197
|
+
* `> 0` version pick, and four bare `.sort()` calls — so the numbers were never observable
|
|
198
|
+
* from here. A future use that subtracts or scales one of these values is NOT covered by
|
|
199
|
+
* that argument and would have to re-establish it.
|
|
173
200
|
*/
|
|
174
|
-
const PY_SPACE = new Set([
|
|
175
|
-
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x85, 0xa0, 0x1680, 0x2000,
|
|
176
|
-
0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007, 0x2008, 0x2009, 0x200a, 0x2028,
|
|
177
|
-
0x2029, 0x202f, 0x205f, 0x3000,
|
|
178
|
-
]);
|
|
179
|
-
function pyStrip(text) {
|
|
180
|
-
const chars = [...text];
|
|
181
|
-
let start = 0;
|
|
182
|
-
let end = chars.length;
|
|
183
|
-
while (start < end && PY_SPACE.has(chars[start].codePointAt(0)))
|
|
184
|
-
start += 1;
|
|
185
|
-
while (end > start && PY_SPACE.has(chars[end - 1].codePointAt(0)))
|
|
186
|
-
end -= 1;
|
|
187
|
-
return chars.slice(start, end).join('');
|
|
188
|
-
}
|
|
189
201
|
/** `line[:1].isspace()`: whether the line is a FOLD rather than a key. */
|
|
202
|
+
const STARTS_WITH_PY_SPACE = new RegExp(`^${PY_WS_CLASS}`);
|
|
190
203
|
function startsWithSpace(line) {
|
|
191
|
-
|
|
192
|
-
return first !== undefined && PY_SPACE.has(first);
|
|
193
|
-
}
|
|
194
|
-
/** `sorted()` on strings: by code point, which is not what `Array.sort` does above U+FFFF. */
|
|
195
|
-
function cmpCodepoint(a, b) {
|
|
196
|
-
const ia = a[Symbol.iterator]();
|
|
197
|
-
const ib = b[Symbol.iterator]();
|
|
198
|
-
for (;;) {
|
|
199
|
-
const x = ia.next();
|
|
200
|
-
const y = ib.next();
|
|
201
|
-
if (x.done && y.done)
|
|
202
|
-
return 0;
|
|
203
|
-
if (x.done)
|
|
204
|
-
return -1;
|
|
205
|
-
if (y.done)
|
|
206
|
-
return 1;
|
|
207
|
-
const cx = x.value.codePointAt(0);
|
|
208
|
-
const cy = y.value.codePointAt(0);
|
|
209
|
-
if (cx !== cy)
|
|
210
|
-
return cx - cy;
|
|
211
|
-
}
|
|
204
|
+
return STARTS_WITH_PY_SPACE.test(line);
|
|
212
205
|
}
|
|
213
206
|
/** `sorted(paths, key=lambda p: p.parts)`: a tuple comparison, segment by segment. */
|
|
214
207
|
function cmpParts(a, b) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bantamkit-mcp",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.2",
|
|
4
4
|
"description": "bantamkit MCP server — pure Node, runnable with npx",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"assets"
|
|
21
21
|
],
|
|
22
22
|
"engines": {
|
|
23
|
-
"node": ">=
|
|
23
|
+
"node": ">=20"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "tsc -p tsconfig.json",
|