vigiles 14.7.0 → 14.9.0

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.
Files changed (47) hide show
  1. package/dist/adapters/claude-code/agent-runtime.d.ts +2 -19
  2. package/dist/adapters/claude-code/agent-runtime.js +5 -30
  3. package/dist/adapters/claude-code/agent-tools.d.ts +20 -0
  4. package/dist/adapters/claude-code/agent-tools.js +40 -0
  5. package/dist/audit-report.d.ts +1 -1
  6. package/dist/audit-report.template.html +37 -32
  7. package/dist/audit-score.d.ts +1 -1
  8. package/dist/audit-score.js +20 -20
  9. package/dist/audit-verdict.d.ts +1 -1
  10. package/dist/audit-verdict.js +3 -3
  11. package/dist/core/assert-never.d.ts +9 -0
  12. package/dist/core/assert-never.js +14 -0
  13. package/dist/core/description-overlap.js +2 -2
  14. package/dist/core/effects.js +3 -3
  15. package/dist/core/hash.d.ts +1 -2
  16. package/dist/core/hash.js +6 -4
  17. package/dist/core/hook-block-ineffective.d.ts +55 -6
  18. package/dist/core/hook-block-ineffective.js +9 -14
  19. package/dist/core/mcp-contract-message.d.ts +22 -0
  20. package/dist/core/mcp-contract-message.js +29 -0
  21. package/dist/core/mcp.d.ts +4 -12
  22. package/dist/core/mcp.js +3 -14
  23. package/dist/core/ncd.d.ts +12 -0
  24. package/dist/core/ncd.js +50 -0
  25. package/dist/core/plugin-dir-layout.d.ts +5 -5
  26. package/dist/core/plugin-dir-layout.js +10 -22
  27. package/dist/core/proofs.d.ts +2 -11
  28. package/dist/core/proofs.js +4 -39
  29. package/dist/core/skill-resources.d.ts +3 -3
  30. package/dist/core/skill-resources.js +9 -8
  31. package/dist/leaderboard.d.ts +2 -51
  32. package/dist/leaderboard.js +20 -225
  33. package/dist/optimize.d.ts +1 -1
  34. package/dist/optimize.js +3 -3
  35. package/dist/posix-path.d.ts +40 -0
  36. package/dist/posix-path.js +293 -0
  37. package/dist/scan-core.d.ts +154 -0
  38. package/dist/scan-core.js +690 -0
  39. package/dist/scan-files.d.ts +28 -0
  40. package/dist/scan-files.js +489 -0
  41. package/dist/scan.d.ts +11 -34
  42. package/dist/scan.js +55 -668
  43. package/dist/score-core.d.ts +83 -0
  44. package/dist/score-core.js +236 -0
  45. package/dist/test-coverage-files.d.ts +11 -0
  46. package/dist/test-coverage-files.js +208 -0
  47. package/package.json +1 -1
@@ -0,0 +1,293 @@
1
+ "use strict";
2
+ /**
3
+ * A tiny, node-free POSIX `path` — the browser-safe substitute for `node:path`
4
+ * inside the in-browser audit engine (`scan-core.ts` / `scan-files.ts` /
5
+ * `test-coverage-files.ts` and the core detectors they reach). A Vite bundle of
6
+ * that engine must not pull `node:path`, so these are pure string ops.
7
+ *
8
+ * The audit engine's file-map keys are always POSIX (`/`-separated), and the
9
+ * disk-side `scanPlugin` feeds it absolute POSIX roots (`resolve(dir)` on Linux),
10
+ * so a faithful port of Node's `path.posix` algorithm is byte-identical to
11
+ * `node:path` for every input the engine passes — which is exactly what the
12
+ * parity firewall (`scan-files.test.ts`) proves. `resolve` deliberately falls
13
+ * back to `/` (never `process.cwd()`) so it stays pure and process-free; every
14
+ * call site passes an absolute first segment, so the fallback is never reached.
15
+ *
16
+ * NOTE — the functions below are VERBATIM ports of Node's `lib/path.js` POSIX
17
+ * implementations (charCode scan, `normalizeString`, `basename`, `relative`).
18
+ * Their branch depth / cyclomatic complexity is inherent to that battle-tested
19
+ * algorithm; rewriting it to satisfy the complexity linters would risk a subtle
20
+ * behavioural divergence from `node:path` (which the disk-vs-browser parity gate
21
+ * relies on), so the metric rules are disabled for this file only.
22
+ */
23
+ /* eslint-disable complexity, max-depth, sonarjs/cognitive-complexity, sonarjs/nested-control-flow */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.isAbsolute = isAbsolute;
26
+ exports.normalize = normalize;
27
+ exports.join = join;
28
+ exports.resolve = resolve;
29
+ exports.dirname = dirname;
30
+ exports.basename = basename;
31
+ exports.relative = relative;
32
+ const SLASH = 47; // '/'
33
+ const DOT = 46; // '.'
34
+ /**
35
+ * The core POSIX normalize pass (ported from Node's `normalizeString`): collapse
36
+ * `.`/`..`/`//` in a slash-separated path with no leading root. `allowAboveRoot`
37
+ * keeps leading `..` segments (used for a relative path).
38
+ */
39
+ function normalizeString(path, allowAboveRoot) {
40
+ let res = "";
41
+ let lastSegmentLength = 0;
42
+ let lastSlash = -1;
43
+ let dots = 0;
44
+ let code = 0;
45
+ for (let i = 0; i <= path.length; ++i) {
46
+ if (i < path.length)
47
+ code = path.charCodeAt(i);
48
+ else if (code === SLASH)
49
+ break;
50
+ else
51
+ code = SLASH;
52
+ if (code === SLASH) {
53
+ if (lastSlash === i - 1 || dots === 1) {
54
+ // no-op: a `//` or `.` segment
55
+ }
56
+ else if (dots === 2) {
57
+ if (res.length < 2 ||
58
+ lastSegmentLength !== 2 ||
59
+ res.charCodeAt(res.length - 1) !== DOT ||
60
+ res.charCodeAt(res.length - 2) !== DOT) {
61
+ if (res.length > 2) {
62
+ const lastSlashIndex = res.lastIndexOf("/");
63
+ if (lastSlashIndex === -1) {
64
+ res = "";
65
+ lastSegmentLength = 0;
66
+ }
67
+ else {
68
+ res = res.slice(0, lastSlashIndex);
69
+ lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
70
+ }
71
+ lastSlash = i;
72
+ dots = 0;
73
+ continue;
74
+ }
75
+ else if (res.length !== 0) {
76
+ res = "";
77
+ lastSegmentLength = 0;
78
+ lastSlash = i;
79
+ dots = 0;
80
+ continue;
81
+ }
82
+ }
83
+ if (allowAboveRoot) {
84
+ res += res.length > 0 ? "/.." : "..";
85
+ lastSegmentLength = 2;
86
+ }
87
+ }
88
+ else {
89
+ if (res.length > 0)
90
+ res += "/" + path.slice(lastSlash + 1, i);
91
+ else
92
+ res = path.slice(lastSlash + 1, i);
93
+ lastSegmentLength = i - lastSlash - 1;
94
+ }
95
+ lastSlash = i;
96
+ dots = 0;
97
+ }
98
+ else if (code === DOT && dots !== -1) {
99
+ ++dots;
100
+ }
101
+ else {
102
+ dots = -1;
103
+ }
104
+ }
105
+ return res;
106
+ }
107
+ /** POSIX `path.isAbsolute`. */
108
+ function isAbsolute(path) {
109
+ return path.length > 0 && path.charCodeAt(0) === SLASH;
110
+ }
111
+ /** POSIX `path.normalize`. */
112
+ function normalize(path) {
113
+ if (path.length === 0)
114
+ return ".";
115
+ const isAbs = path.charCodeAt(0) === SLASH;
116
+ const trailingSep = path.charCodeAt(path.length - 1) === SLASH;
117
+ path = normalizeString(path, !isAbs);
118
+ if (path.length === 0) {
119
+ if (isAbs)
120
+ return "/";
121
+ return trailingSep ? "./" : ".";
122
+ }
123
+ if (trailingSep)
124
+ path += "/";
125
+ return isAbs ? `/${path}` : path;
126
+ }
127
+ /** POSIX `path.join`. */
128
+ function join(...parts) {
129
+ if (parts.length === 0)
130
+ return ".";
131
+ let joined;
132
+ for (const part of parts) {
133
+ if (part.length > 0) {
134
+ if (joined === undefined)
135
+ joined = part;
136
+ else
137
+ joined += `/${part}`;
138
+ }
139
+ }
140
+ if (joined === undefined)
141
+ return ".";
142
+ return normalize(joined);
143
+ }
144
+ /**
145
+ * POSIX `path.resolve`. Right-to-left until an absolute segment is found, then
146
+ * normalize. The `i === -1` fallback is `/` (not `process.cwd()`) so this stays
147
+ * pure; every engine call site passes an absolute first segment.
148
+ */
149
+ function resolve(...parts) {
150
+ let resolvedPath = "";
151
+ let resolvedAbsolute = false;
152
+ for (let i = parts.length - 1; i >= -1 && !resolvedAbsolute; i--) {
153
+ const path = i >= 0 ? parts[i] : "/";
154
+ if (path.length === 0)
155
+ continue;
156
+ resolvedPath = `${path}/${resolvedPath}`;
157
+ resolvedAbsolute = path.charCodeAt(0) === SLASH;
158
+ }
159
+ resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute);
160
+ if (resolvedAbsolute)
161
+ return `/${resolvedPath}`;
162
+ return resolvedPath.length > 0 ? resolvedPath : ".";
163
+ }
164
+ /** POSIX `path.dirname`. */
165
+ function dirname(path) {
166
+ if (path.length === 0)
167
+ return ".";
168
+ const hasRoot = path.charCodeAt(0) === SLASH;
169
+ let end = -1;
170
+ let matchedSlash = true;
171
+ for (let i = path.length - 1; i >= 1; --i) {
172
+ if (path.charCodeAt(i) === SLASH) {
173
+ if (!matchedSlash) {
174
+ end = i;
175
+ break;
176
+ }
177
+ }
178
+ else {
179
+ matchedSlash = false;
180
+ }
181
+ }
182
+ if (end === -1)
183
+ return hasRoot ? "/" : ".";
184
+ if (hasRoot && end === 1)
185
+ return "//";
186
+ return path.slice(0, end);
187
+ }
188
+ /** POSIX `path.basename` (with an optional `suffix` to strip, like `node:path`). */
189
+ function basename(path, suffix) {
190
+ let start = 0;
191
+ let end = -1;
192
+ let matchedSlash = true;
193
+ if (suffix !== undefined &&
194
+ suffix.length > 0 &&
195
+ suffix.length <= path.length) {
196
+ if (suffix === path)
197
+ return "";
198
+ let extIdx = suffix.length - 1;
199
+ let firstNonSlashEnd = -1;
200
+ for (let i = path.length - 1; i >= 0; --i) {
201
+ const code = path.charCodeAt(i);
202
+ if (code === SLASH) {
203
+ if (!matchedSlash) {
204
+ start = i + 1;
205
+ break;
206
+ }
207
+ }
208
+ else {
209
+ if (firstNonSlashEnd === -1) {
210
+ matchedSlash = false;
211
+ firstNonSlashEnd = i + 1;
212
+ }
213
+ if (extIdx >= 0) {
214
+ if (code === suffix.charCodeAt(extIdx)) {
215
+ if (--extIdx === -1) {
216
+ end = i;
217
+ }
218
+ }
219
+ else {
220
+ extIdx = -1;
221
+ end = firstNonSlashEnd;
222
+ }
223
+ }
224
+ }
225
+ }
226
+ if (start === end)
227
+ end = firstNonSlashEnd;
228
+ else if (end === -1)
229
+ end = path.length;
230
+ return path.slice(start, end);
231
+ }
232
+ for (let i = path.length - 1; i >= 0; --i) {
233
+ if (path.charCodeAt(i) === SLASH) {
234
+ if (!matchedSlash) {
235
+ start = i + 1;
236
+ break;
237
+ }
238
+ }
239
+ else if (end === -1) {
240
+ matchedSlash = false;
241
+ end = i + 1;
242
+ }
243
+ }
244
+ if (end === -1)
245
+ return "";
246
+ return path.slice(start, end);
247
+ }
248
+ /** POSIX `path.relative`. */
249
+ function relative(from, to) {
250
+ if (from === to)
251
+ return "";
252
+ from = resolve(from);
253
+ to = resolve(to);
254
+ if (from === to)
255
+ return "";
256
+ const fromStart = 1;
257
+ const fromEnd = from.length;
258
+ const fromLen = fromEnd - fromStart;
259
+ const toStart = 1;
260
+ const toLen = to.length - toStart;
261
+ const length = fromLen < toLen ? fromLen : toLen;
262
+ let lastCommonSep = -1;
263
+ let i = 0;
264
+ for (; i < length; i++) {
265
+ const fromCode = from.charCodeAt(fromStart + i);
266
+ if (fromCode !== to.charCodeAt(toStart + i))
267
+ break;
268
+ else if (fromCode === SLASH)
269
+ lastCommonSep = i;
270
+ }
271
+ if (i === length) {
272
+ if (toLen > length) {
273
+ if (to.charCodeAt(toStart + i) === SLASH)
274
+ return to.slice(toStart + i + 1);
275
+ if (i === 0)
276
+ return to.slice(toStart + i);
277
+ }
278
+ else if (fromLen > length) {
279
+ if (from.charCodeAt(fromStart + i) === SLASH)
280
+ lastCommonSep = i;
281
+ else if (i === 0)
282
+ lastCommonSep = 0;
283
+ }
284
+ }
285
+ let out = "";
286
+ for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) {
287
+ if (i === fromEnd || from.charCodeAt(i) === SLASH) {
288
+ out += out.length === 0 ? ".." : "/..";
289
+ }
290
+ }
291
+ return `${out}${to.slice(toStart + lastCommonSep)}`;
292
+ }
293
+ //# sourceMappingURL=posix-path.js.map
@@ -0,0 +1,154 @@
1
+ import { type DescriptionOverlap } from "./core/description-overlap.js";
2
+ import { type DescriptionBudgetIssue } from "./core/skill-description-budget.js";
3
+ import type { PluginLayout } from "./core/layout.js";
4
+ import type { HarnessDialect } from "./core/dialect.js";
5
+ import type { HookRegistration } from "./core/hook-normalize.js";
6
+ import type { HookScriptEntry } from "./core/hook-block-ineffective.js";
7
+ import type { HookMatcherEntry } from "./core/hook-matcher.js";
8
+ import type { ScanSkill, ScanAgent, ScanHook, FrontmatterIssue, FrontmatterValueIssue, FrontmatterParseIssue, ScanTrifectaFinding, ScanSkillResourceFinding, ScanSkillFenceFinding, ScanDelegationFinding } from "./scan.js";
9
+ /**
10
+ * Per-kind surface classifiers, built from the harness `PluginLayout`'s
11
+ * `skillDir`/`agentDir`/`commandDir` — so adding a harness whose subagents live
12
+ * somewhere other than `agents/` (OpenCode's `.opencode/agent`) needs no change
13
+ * here. Each anchors on a real path boundary (start-of-path or a `/`), so a
14
+ * directory whose NAME merely ends in the keyword isn't misclassified — e.g. the
15
+ * skill `skills/dispatching-parallel-agents/SKILL.md` must NOT register as an
16
+ * agent named "SKILL" (the `-agents/` substring), which real plugins like
17
+ * obra/superpowers ship. See scan.test.ts for the regression cases.
18
+ */
19
+ export interface SurfaceClassifier {
20
+ readonly isSkill: (f: string) => boolean;
21
+ readonly isAgent: (f: string) => boolean;
22
+ readonly isCommand: (f: string) => boolean;
23
+ }
24
+ export declare function makeClassifier(layout: PluginLayout): SurfaceClassifier;
25
+ /** The plugin-root + materialize-root + dialect context skill scanning needs. */
26
+ export interface SkillScanContext {
27
+ readonly root: string;
28
+ readonly materializeRoot: string;
29
+ readonly dialect: HarnessDialect;
30
+ /**
31
+ * Materialized-key → real on-disk path (from `loadPlugin`). A surface can be
32
+ * materialized under a canonical key while living at a different root (repo-root
33
+ * `skills/` vs project-level `.claude/skills/`), so bundled-resource resolution
34
+ * uses the true dir from here rather than reverse-guessing from the key.
35
+ */
36
+ readonly sources?: Record<string, string>;
37
+ /** OPT-IN top-level shared-resource dirs (`.vigilesrc.json` `sharedDirs`). */
38
+ readonly sharedDirs?: readonly string[];
39
+ /**
40
+ * The REPO root the `sharedDirs` are declared relative to (where `.vigilesrc.json`
41
+ * lives) — distinct from the scan `root`, which may be a scoped SUBDIR (e.g.
42
+ * `lint packages/foo`). A shared-dir ref resolves against THIS, not the subdir,
43
+ * so a scoped scan doesn't false-flag the top-level shared tree. Defaults to
44
+ * `root` (unchanged when scanning the repo root itself).
45
+ */
46
+ readonly sharedDirsRoot?: string;
47
+ /**
48
+ * REQUIRED existence check for bundled-resource resolution. The disk scan
49
+ * (`scanPlugin`) injects `node:fs` `existsSync`; the browser file-map engine
50
+ * (`scanFiles`) a map-backed impl — so the same `scanSkills` runs on disk OR
51
+ * in the browser with no `node:` import in this node-free module.
52
+ */
53
+ readonly existsSync: (p: string) => boolean;
54
+ }
55
+ export declare function scanSkills(files: Record<string, string>, cls: SurfaceClassifier, ctx: SkillScanContext): ScanSkill[];
56
+ export declare function descriptionOverlapsFor(files: Record<string, string>, cls: SurfaceClassifier): DescriptionOverlap[];
57
+ /**
58
+ * Model-invocable skills whose description is so long the trigger signal is
59
+ * buried (heuristic proxy; degrades recall + precision). Same surfaces as the
60
+ * overlap check. See skill-description-budget.ts.
61
+ */
62
+ export declare function descriptionBudgetFor(files: Record<string, string>, cls: SurfaceClassifier): DescriptionBudgetIssue[];
63
+ export declare function scanAgents(files: Record<string, string>, dialect: HarnessDialect, declaredServers: readonly string[], cls: SurfaceClassifier): ScanAgent[];
64
+ /**
65
+ * A compiled `vigiles/hook` artifact runs through the `hook-runtime run-program`
66
+ * runtime entrypoint; any other hook command is hand-written (a shell script or
67
+ * an inline one-liner) the author maintains directly. The basis for the
68
+ * `prefer-compiled-hooks` nudge.
69
+ */
70
+ export declare function isManagedHookCommand(command: string): boolean;
71
+ /** The `prefer-compiled-hooks` recommendation message (shared by `lint` + `scan`). */
72
+ export declare function preferCompiledHooksMessage(count: number): string;
73
+ export declare function scanHooks(regs: readonly HookRegistration[], root: string, pluginRootToken: string, exists: (p: string) => boolean): {
74
+ hooks: ScanHook[];
75
+ inline: number;
76
+ manual: number;
77
+ };
78
+ /**
79
+ * Frontmatter-schema check — **subagents only**. Per the Claude Code docs, a
80
+ * subagent (`agents/*.md`) REQUIRES `name` + `description` (no fallback) or it
81
+ * won't register. A SKILL.md requires NOTHING: `name` falls back to the directory
82
+ * name and `description` to the first body paragraph, so a frontmatter-less skill
83
+ * still loads — flagging it would be a false positive (skill description QUALITY
84
+ * is a separate, behavioral concern). See https://code.claude.com/docs/en/skills
85
+ * and …/sub-agents.
86
+ */
87
+ export declare function frontmatterIssuesFor(files: Record<string, string>, cls: SurfaceClassifier): FrontmatterIssue[];
88
+ /**
89
+ * Agent frontmatter VALUE validity — a `model:` or `color:` that's a close typo
90
+ * of a real one. A bad `model:` silently falls back; a bad `color:` is ignored.
91
+ * High-precision (close-typo only); a full/dated model id is left alone. Folded
92
+ * into the `subagent-frontmatter` rule. Agents only (skills have no model/color).
93
+ */
94
+ export declare function frontmatterValueIssuesFor(files: Record<string, string>, cls: SurfaceClassifier): FrontmatterValueIssue[];
95
+ /**
96
+ * Frontmatter that EXISTS but isn't valid YAML — the `frontmatter-valid` signal.
97
+ * Reported for skills + agents via the shared reader's `malformed` flag. Honest
98
+ * caveat (see docs/rules/frontmatter-valid.md): js-yaml is stricter than some
99
+ * loaders, so a one-line `description:` containing a `: ` colon or an `<example>`
100
+ * block is flagged even though it may still load — which is why scan surfaces it
101
+ * as an informational note (NOT a structural defect) and the lint rule is a
102
+ * warn, not an error. The file's other fields are still salvaged.
103
+ */
104
+ export declare function malformedFrontmatterFor(files: Record<string, string>, cls: SurfaceClassifier): FrontmatterParseIssue[];
105
+ /**
106
+ * Skill-metadata RECOMMENDATION (not a correctness check): a `SKILL.md` loads
107
+ * fine without frontmatter (`name` ← dir, `description` ← first body paragraph),
108
+ * but relying on those fallbacks is fragile — the dir name may be unclear and the
109
+ * first paragraph is often a heading or boilerplate, making a weak trigger
110
+ * surface. Best practice is an EXPLICIT `name` + `description`. Flags skills
111
+ * missing either; surfaced as a soft note in scan (NOT a structural defect, NOT
112
+ * scored) and gated by the `skill-frontmatter` lint rule (warn by default).
113
+ */
114
+ export declare function skillMetaIssuesFor(files: Record<string, string>, cls: SurfaceClassifier): FrontmatterIssue[];
115
+ /**
116
+ * Flatten the per-surface lethal-trifecta + skill-resource findings into the
117
+ * path-tagged report lists the `audit` report AND the `lethal-trifecta` /
118
+ * `skill-resource-resolves` lint rules both consume (one detector, no drift).
119
+ */
120
+ export declare function collectSurfaceFindings(agents: readonly ScanAgent[], skills: readonly ScanSkill[]): {
121
+ trifectaFindings: ScanTrifectaFinding[];
122
+ skillResourceFindings: ScanSkillResourceFinding[];
123
+ skillFenceFindings: ScanSkillFenceFinding[];
124
+ };
125
+ /**
126
+ * Build the subagent delegation graph and flag a lethal trifecta that EMERGES
127
+ * across an edge (own ∪ delegated-to capability) though no single unit trips it.
128
+ *
129
+ * Edge source (deterministic, audit-available): a subagent that lists the `Task`
130
+ * tool can dispatch any sibling subagent, so it `delegatesTo` every OTHER agent.
131
+ * An inherits-all agent (`tools === null`) carries the wildcard, which the
132
+ * detector's FP-safe guard skips (that maximal-blast case is the per-unit
133
+ * advisory's job). One detector, no drift. (Richer edge sources — a typed
134
+ * railway's `delegate()` chain, a Flue subagent inheritance tree — plug in here.)
135
+ */
136
+ export declare function collectDelegationTrifecta(agents: readonly ScanAgent[], dialect: HarnessDialect): ScanDelegationFinding[];
137
+ /**
138
+ * Build `hookBlockIssues` entries by walking the canonical object-keyed-by-event
139
+ * settings shape PER REGISTRATION — so a script registered under several events
140
+ * is inspected under EACH (no de-dup by script path), inline one-liners are
141
+ * included (script token → null, inspect the command), and a script token is
142
+ * resolved to its ABSOLUTE on-disk path against the plugin root (not the caller's
143
+ * cwd). Addresses the gaps a de-duplicated `ScanHook[]` would miss.
144
+ */
145
+ export declare function collectHookBlockEntries(regs: readonly HookRegistration[], root: string, pluginRootToken: string, exists: (p: string) => boolean): HookScriptEntry[];
146
+ /** Extract (event, matcher) pairs from the normalized hook registrations. */
147
+ export declare function collectHookMatchers(regs: readonly HookRegistration[]): HookMatcherEntry[];
148
+ /** Tally how many scanned agents fall into each purity rung (effectSurface). */
149
+ export declare function summarizePurity(agents: readonly ScanAgent[]): {
150
+ pure: number;
151
+ bounded: number;
152
+ unrestricted: number;
153
+ };
154
+ //# sourceMappingURL=scan-core.d.ts.map