vouchington-tooling 0.1.8 → 0.2.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 (64) hide show
  1. package/README.md +22 -5
  2. package/dist/cli/commands/link-skill.d.mts +5 -0
  3. package/dist/cli/commands/link-skill.mjs +6 -0
  4. package/dist/cli/index.mjs +3 -0
  5. package/dist/cli/parse.d.mts +5 -0
  6. package/dist/cli/parse.mjs +25 -0
  7. package/dist/cli/usage.d.mts +1 -1
  8. package/dist/cli/usage.mjs +2 -0
  9. package/dist/gha-post-review/github.d.mts +2 -0
  10. package/dist/gha-post-review/github.mjs +47 -5
  11. package/dist/gha-post-review/post.mjs +6 -1
  12. package/dist/index.d.mts +2 -0
  13. package/dist/index.mjs +1 -0
  14. package/dist/pnpm-install/index.d.mts +2 -1
  15. package/dist/pnpm-install/index.mjs +2 -1
  16. package/dist/pnpm-install/install-operations.d.mts +4 -0
  17. package/dist/pnpm-install/install-operations.mjs +38 -0
  18. package/dist/pnpm-install/metadata-legacy.d.mts +4 -0
  19. package/dist/pnpm-install/metadata-legacy.mjs +81 -0
  20. package/dist/pnpm-install/metadata.d.mts +16 -3
  21. package/dist/pnpm-install/metadata.mjs +103 -36
  22. package/dist/pnpm-install/pending-builds.d.mts +17 -0
  23. package/dist/pnpm-install/pending-builds.mjs +87 -0
  24. package/dist/pnpm-install/pnpm-install-fake-pnpm.test-helpers.d.mts +1 -0
  25. package/dist/pnpm-install/pnpm-install-fake-pnpm.test-helpers.mjs +52 -0
  26. package/dist/pnpm-install/pnpm-install-fixture.test-helpers.mjs +6 -40
  27. package/dist/pnpm-install/runner.mjs +72 -56
  28. package/dist/pnpm-install/transition.d.mts +23 -0
  29. package/dist/pnpm-install/transition.mjs +36 -0
  30. package/dist/skill-discovery/index.d.mts +12 -0
  31. package/dist/skill-discovery/index.mjs +64 -0
  32. package/dist/skill-discovery/manifest.d.mts +15 -0
  33. package/dist/skill-discovery/manifest.mjs +85 -0
  34. package/dist/skill-discovery/target-directory.d.mts +10 -0
  35. package/dist/skill-discovery/target-directory.mjs +126 -0
  36. package/package.json +6 -1
  37. package/scripts/build.mjs +16 -4
  38. package/skills/agent-workflow/SKILL.md +5 -0
  39. package/skills/agent-workflow/references/evidence-sweep.md +8 -0
  40. package/skills/agent-workflow/references/implementation-and-review.md +9 -0
  41. package/skills/agent-workflow/references/implementation.md +8 -0
  42. package/skills/agent-workflow/references/review.md +7 -0
  43. package/skills/backend-vitest-test-authoring/SKILL.md +15 -0
  44. package/skills/backend-vitest-test-authoring/references/integration-boundaries.md +10 -0
  45. package/skills/dotnet-test-authoring/SKILL.md +16 -0
  46. package/skills/manifest.json +158 -0
  47. package/skills/nextjs-vitest-test-authoring/SKILL.md +15 -0
  48. package/skills/nextjs-vitest-test-authoring/references/framework-boundaries.md +9 -0
  49. package/skills/planning/SKILL.md +3 -0
  50. package/skills/planning/references/impact-discovery.md +9 -0
  51. package/skills/playwright-authoring/SKILL.md +15 -0
  52. package/skills/playwright-authoring/references/browser-reliability.md +9 -0
  53. package/skills/postgres-node-performance-tuning/SKILL.md +15 -0
  54. package/skills/postgres-node-performance-tuning/references/performance-patterns.md +14 -0
  55. package/skills/postgres-partitioning-uuid-v7/SKILL.md +16 -0
  56. package/skills/postgres-partitioning-uuid-v7/references/partition-lifecycle.md +14 -0
  57. package/skills/storybook-authoring/SKILL.md +15 -0
  58. package/skills/storybook-authoring/references/component-coverage.md +9 -0
  59. package/skills/swift-test-authoring/SKILL.md +15 -0
  60. package/skills/swift-test-authoring/references/network-test-doubles.md +9 -0
  61. package/skills/test-authoring/SKILL.md +17 -0
  62. package/skills/test-authoring/references/core-practice.md +10 -0
  63. package/skills/vitest-test-authoring/SKILL.md +15 -0
  64. package/skills/vitest-test-authoring/references/mock-boundaries.md +9 -0
@@ -0,0 +1,64 @@
1
+ import { lstat, realpath } from 'node:fs/promises';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { linkDirectoryEntry, resolveTargetDirectory, } from './target-directory.mjs';
4
+ import { assertContained, isContained, isSafeSkillName, readSkillManifest, } from './manifest.mjs';
5
+ export { readSkillManifest } from './manifest.mjs';
6
+ export async function linkSkill(options) {
7
+ if (!isSafeSkillName(options.name))
8
+ throw new Error(`Invalid skill name: ${options.name}`);
9
+ const sourceRoot = resolve(options.sourceRoot);
10
+ const canonicalSourceRoot = await realpath(sourceRoot);
11
+ const manifest = await readSkillManifest(sourceRoot);
12
+ const entry = manifest.skills.find((candidate) => candidate.name === options.name);
13
+ if (entry === undefined)
14
+ throw new Error(`Unknown skill: ${options.name}`);
15
+ const targetRoot = await resolveTargetDirectory(options.targetRoot);
16
+ const linked = new Set();
17
+ const linking = new Set();
18
+ return linkManifestSkill(sourceRoot, canonicalSourceRoot, targetRoot, manifest, entry, linked, linking);
19
+ }
20
+ async function linkManifestSkill(sourceRoot, canonicalSourceRoot, targetRoot, manifest, entry, linked, linking) {
21
+ if (linked.has(entry.name))
22
+ return linkResult(targetRoot, entry.name, sourceRoot, canonicalSourceRoot, entry.path);
23
+ if (linking.has(entry.name))
24
+ throw new Error(`Circular skill prerequisite: ${entry.name}`);
25
+ linking.add(entry.name);
26
+ try {
27
+ for (const prerequisite of prerequisitesFor(manifest, entry)) {
28
+ await linkManifestSkill(sourceRoot, canonicalSourceRoot, targetRoot, manifest, prerequisite, linked, linking);
29
+ }
30
+ const result = await linkResult(targetRoot, entry.name, sourceRoot, canonicalSourceRoot, entry.path);
31
+ linked.add(entry.name);
32
+ return result;
33
+ }
34
+ finally {
35
+ linking.delete(entry.name);
36
+ }
37
+ }
38
+ async function linkResult(targetRoot, name, sourceRoot, canonicalSourceRoot, skillPath) {
39
+ const source = await resolveSkillSource(sourceRoot, canonicalSourceRoot, skillPath);
40
+ const path = assertContained(targetRoot.path, name);
41
+ return { created: await linkDirectoryEntry(source, targetRoot, name), path, source };
42
+ }
43
+ function prerequisitesFor(manifest, entry) {
44
+ const entriesByName = new Map(manifest.skills.map((candidate) => [candidate.name, candidate]));
45
+ return (entry.prerequisites ?? []).map((name) => entriesByName.get(name));
46
+ }
47
+ async function resolveSkillSource(sourceRoot, canonicalSourceRoot, skillPath) {
48
+ const candidate = assertContained(sourceRoot, skillPath);
49
+ let skill;
50
+ try {
51
+ skill = await realpath(candidate);
52
+ }
53
+ catch {
54
+ /* v8 ignore next 2 -- only an attacker replacing a validated source can reach this. */
55
+ throw new Error(`Invalid skill source: ${skillPath}`);
56
+ }
57
+ /* v8 ignore next 2 -- only an attacker replacing a validated source can reach this. */
58
+ if (!isContained(canonicalSourceRoot, skill) || !(await lstat(skill)).isFile())
59
+ throw new Error(`Skill source escapes root: ${skillPath}`);
60
+ // Validate through the canonical path, but retain the caller's logical root in
61
+ // the link target. Package managers replace their physical store paths during
62
+ // upgrades while the logical installed path remains stable.
63
+ return dirname(candidate);
64
+ }
@@ -0,0 +1,15 @@
1
+ export type SkillManifestEntry = {
2
+ name: string;
3
+ plugin: string;
4
+ pluginVersion: string;
5
+ path: string;
6
+ prerequisites?: string[];
7
+ };
8
+ export type SkillManifest = {
9
+ version: 1;
10
+ skills: SkillManifestEntry[];
11
+ };
12
+ export declare function readSkillManifest(sourceRoot: string): Promise<SkillManifest>;
13
+ export declare function assertContained(root: string, child: string): string;
14
+ export declare function isSafeSkillName(name: string): boolean;
15
+ export declare function isContained(root: string, path: string): boolean;
@@ -0,0 +1,85 @@
1
+ import { lstat, readFile, realpath } from 'node:fs/promises';
2
+ import { basename, isAbsolute, relative, resolve, join } from 'node:path';
3
+ export async function readSkillManifest(sourceRoot) {
4
+ const root = await realpath(resolve(sourceRoot));
5
+ const parsed = JSON.parse(await readFile(join(root, 'manifest.json'), 'utf8'));
6
+ if (!isManifest(parsed))
7
+ throw new Error('Invalid skills manifest');
8
+ await validateManifestEntries(root, parsed.skills);
9
+ return parsed;
10
+ }
11
+ export function assertContained(root, child) {
12
+ if (isAbsolute(child))
13
+ throw new Error(`Skill path escapes root: ${child}`);
14
+ const path = resolve(root, child);
15
+ if (!isContained(root, path))
16
+ throw new Error(`Skill path escapes root: ${child}`);
17
+ return path;
18
+ }
19
+ export function isSafeSkillName(name) {
20
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
21
+ }
22
+ export function isContained(root, path) {
23
+ const pathRelative = relative(root, path);
24
+ return !pathRelative.startsWith('..') && !isAbsolute(pathRelative);
25
+ }
26
+ async function validateManifestEntries(root, entries) {
27
+ const names = new Set();
28
+ const lexicalPaths = new Set();
29
+ const canonicalPaths = new Set();
30
+ for (const entry of entries) {
31
+ if (!isSafeSkillName(entry.name))
32
+ throw new Error(`Invalid skill name: ${entry.name}`);
33
+ if (names.has(entry.name))
34
+ throw new Error(`Duplicate skill name: ${entry.name}`);
35
+ names.add(entry.name);
36
+ const prerequisites = entry.prerequisites ?? [];
37
+ if (new Set(prerequisites).size !== prerequisites.length)
38
+ throw new Error(`Duplicate skill prerequisite: ${entry.name}`);
39
+ for (const prerequisite of prerequisites) {
40
+ if (!isSafeSkillName(prerequisite))
41
+ throw new Error(`Invalid skill prerequisite: ${prerequisite}`);
42
+ }
43
+ if (basename(entry.path) !== 'SKILL.md')
44
+ throw new Error(`Invalid skill source: ${entry.path}`);
45
+ const path = assertContained(root, entry.path);
46
+ if (lexicalPaths.has(path))
47
+ throw new Error(`Duplicate skill path: ${entry.path}`);
48
+ lexicalPaths.add(path);
49
+ let canonicalPath;
50
+ try {
51
+ canonicalPath = await realpath(path);
52
+ }
53
+ catch {
54
+ throw new Error(`Invalid skill source: ${entry.path}`);
55
+ }
56
+ if (!isContained(root, canonicalPath))
57
+ throw new Error(`Skill source escapes root: ${entry.path}`);
58
+ if (!(await lstat(canonicalPath)).isFile())
59
+ throw new Error(`Invalid skill source: ${entry.path}`);
60
+ if (canonicalPaths.has(canonicalPath))
61
+ throw new Error(`Duplicate skill path: ${entry.path}`);
62
+ canonicalPaths.add(canonicalPath);
63
+ }
64
+ for (const entry of entries) {
65
+ for (const prerequisite of entry.prerequisites ?? []) {
66
+ if (!names.has(prerequisite))
67
+ throw new Error(`Missing prerequisite skill: ${prerequisite}`);
68
+ }
69
+ }
70
+ }
71
+ function isManifest(value) {
72
+ if (value === null || typeof value !== 'object')
73
+ return false;
74
+ const manifest = value;
75
+ return manifest.version === 1 && Array.isArray(manifest.skills) && manifest.skills.every(isEntry);
76
+ }
77
+ function isEntry(value) {
78
+ if (value === null || typeof value !== 'object')
79
+ return false;
80
+ const entry = value;
81
+ return ([entry.name, entry.plugin, entry.pluginVersion, entry.path].every((field) => typeof field === 'string' && field.length > 0) &&
82
+ (entry.prerequisites === undefined ||
83
+ (Array.isArray(entry.prerequisites) &&
84
+ entry.prerequisites.every((name) => typeof name === 'string'))));
85
+ }
@@ -0,0 +1,10 @@
1
+ export type TargetDirectory = {
2
+ path: string;
3
+ dev: bigint;
4
+ ino: bigint;
5
+ };
6
+ export declare function resolveTargetDirectory(targetRoot: string, beforeRevalidate?: () => Promise<void>): Promise<TargetDirectory>;
7
+ export declare function linkDirectoryEntry(source: string, target: TargetDirectory, name: string, beforeWorker?: () => Promise<void>, worker?: DirectoryLinkWorker, afterWorker?: () => Promise<void>): Promise<boolean>;
8
+ type DirectoryLinkWorker = (source: string, target: TargetDirectory, name: string) => Promise<string>;
9
+ export declare function snapshotTargetDirectory(path: string): Promise<TargetDirectory>;
10
+ export {};
@@ -0,0 +1,126 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { lstat } from 'node:fs/promises';
3
+ import { parse, relative, resolve, sep, join } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ const execFileAsync = promisify(execFile);
6
+ export async function resolveTargetDirectory(targetRoot, beforeRevalidate) {
7
+ const path = resolve(targetRoot);
8
+ const ancestors = await snapshotTargetAncestors(path);
9
+ await beforeRevalidate?.();
10
+ await assertTargetAncestorsUnchanged(ancestors);
11
+ return ancestors.at(-1);
12
+ }
13
+ export async function linkDirectoryEntry(source, target, name, beforeWorker, worker = runDirectoryLinkWorker, afterWorker) {
14
+ await beforeWorker?.();
15
+ const stdout = await worker(source, target, name);
16
+ await afterWorker?.();
17
+ await assertTargetUnchanged(target);
18
+ if (stdout === 'created')
19
+ return true;
20
+ if (stdout === 'existing')
21
+ return false;
22
+ throw new Error('Skill link worker returned an invalid result');
23
+ }
24
+ async function runDirectoryLinkWorker(source, target, name) {
25
+ try {
26
+ const { stdout } = await execFileAsync(process.execPath, [
27
+ '--input-type=module',
28
+ '--eval',
29
+ LINK_WORKER,
30
+ source,
31
+ name,
32
+ String(target.dev),
33
+ String(target.ino),
34
+ ], { cwd: target.path, windowsHide: true });
35
+ return stdout;
36
+ }
37
+ catch (error) {
38
+ if (error.stderr?.includes('Target root changed during skill linking'))
39
+ throw new Error('Target root changed during skill linking');
40
+ throw error;
41
+ }
42
+ }
43
+ export async function snapshotTargetDirectory(path) {
44
+ const stat = await lstat(path, { bigint: true });
45
+ if (stat.isSymbolicLink())
46
+ throw new Error(`Target root contains symlink: ${path}`);
47
+ if (!stat.isDirectory())
48
+ throw new Error(`Invalid target root: ${path}`);
49
+ return { path, dev: stat.dev, ino: stat.ino };
50
+ }
51
+ async function assertTargetUnchanged(target) {
52
+ try {
53
+ const current = await snapshotTargetDirectory(target.path);
54
+ if (current.dev === target.dev && current.ino === target.ino)
55
+ return;
56
+ }
57
+ catch { }
58
+ throw new Error('Target root changed during skill linking');
59
+ }
60
+ async function snapshotTargetAncestors(path) {
61
+ const parsed = parse(path);
62
+ let ancestor = parsed.root;
63
+ const ancestors = [];
64
+ for (const component of relative(parsed.root, path).split(sep)) {
65
+ ancestor = join(ancestor, component);
66
+ try {
67
+ ancestors.push(await snapshotTargetDirectory(ancestor));
68
+ }
69
+ catch (error) {
70
+ if (error.code === 'ENOENT')
71
+ throw new Error(`Target root must exist: ${path}`);
72
+ throw error;
73
+ }
74
+ }
75
+ return ancestors;
76
+ }
77
+ async function assertTargetAncestorsUnchanged(ancestors) {
78
+ try {
79
+ for (const target of ancestors) {
80
+ const current = await snapshotTargetDirectory(target.path);
81
+ if (current.dev !== target.dev || current.ino !== target.ino)
82
+ throw new Error('Target root changed while resolving');
83
+ }
84
+ }
85
+ catch {
86
+ throw new Error('Target root changed while resolving');
87
+ }
88
+ }
89
+ const LINK_WORKER = String.raw `
90
+ import { lstat, readlink, symlink } from 'node:fs/promises'
91
+
92
+ const [source, name, dev, ino] = process.argv.slice(1)
93
+ const directory = await lstat('.', { bigint: true })
94
+ if (!directory.isDirectory() || directory.isSymbolicLink() || directory.dev !== BigInt(dev) || directory.ino !== BigInt(ino))
95
+ throw new Error('Target root changed during skill linking')
96
+ async function assertExistingMatchesSource() {
97
+ const destination = await lstat(name)
98
+ if (!destination.isSymbolicLink() || (await readlink(name)) !== source)
99
+ throw new Error('Destination already exists: ' + name)
100
+ }
101
+
102
+ let created = false
103
+ try {
104
+ await assertExistingMatchesSource()
105
+ } catch (error) {
106
+ if (error?.code !== 'ENOENT') throw error
107
+ try {
108
+ await symlink(source, name, 'dir')
109
+ created = true
110
+ } catch (error) {
111
+ if (error?.code === 'EEXIST') {
112
+ await assertExistingMatchesSource()
113
+ } else {
114
+ if (process.platform !== 'win32' || !['EACCES', 'EPERM'].includes(error?.code)) throw error
115
+ try {
116
+ await symlink(source, name, 'junction')
117
+ created = true
118
+ } catch (error) {
119
+ if (error?.code !== 'EEXIST') throw error
120
+ await assertExistingMatchesSource()
121
+ }
122
+ }
123
+ }
124
+ }
125
+ process.stdout.write(created ? 'created' : 'existing')
126
+ `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "Vouchington CLI and extractable tooling libraries.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
6
6
  "bugs": {
@@ -31,6 +31,11 @@
31
31
  "import": "./dist/index.mjs",
32
32
  "default": "./dist/index.mjs"
33
33
  },
34
+ "./skill-discovery": {
35
+ "types": "./dist/skill-discovery/index.d.mts",
36
+ "import": "./dist/skill-discovery/index.mjs",
37
+ "default": "./dist/skill-discovery/index.mjs"
38
+ },
34
39
  "./runner-port-policy": {
35
40
  "types": "./dist/runner-port-policy/index.d.mts",
36
41
  "import": "./dist/runner-port-policy/index.mjs",
package/scripts/build.mjs CHANGED
@@ -1,7 +1,10 @@
1
1
  import { execFileSync } from 'node:child_process'
2
2
  import { chmod, copyFile, cp, mkdir, rm } from 'node:fs/promises'
3
+ import { join } from 'node:path'
3
4
  import { fileURLToPath } from 'node:url'
4
5
 
6
+ import skillManifest from '../skill-manifest.json' with { type: 'json' }
7
+
5
8
  const packageRoot = new URL('..', import.meta.url)
6
9
  const dist = new URL('../dist', import.meta.url)
7
10
 
@@ -16,9 +19,18 @@ await copyFile(
16
19
  new URL('../src/runner-port-policy/runner-port-policy.json', import.meta.url),
17
20
  new URL('../dist/runner-port-policy/runner-port-policy.json', import.meta.url),
18
21
  )
19
- await cp(
20
- new URL('../../../plugins/vouchington-workflow/skills', import.meta.url),
21
- new URL('../skills', import.meta.url),
22
- { recursive: true },
22
+ const pluginRoot = fileURLToPath(new URL('../../../plugins', import.meta.url))
23
+ const skillsRoot = fileURLToPath(new URL('../skills/', import.meta.url))
24
+ const seenSkills = new Set()
25
+ for (const skill of skillManifest.skills) {
26
+ if (seenSkills.has(skill.name)) throw new Error(`Duplicate skill: ${skill.name}`)
27
+ seenSkills.add(skill.name)
28
+ await cp(join(pluginRoot, skill.plugin, 'skills', skill.name), join(skillsRoot, skill.name), {
29
+ recursive: true,
30
+ })
31
+ }
32
+ await copyFile(
33
+ new URL('../skill-manifest.json', import.meta.url),
34
+ new URL('../skills/manifest.json', import.meta.url),
23
35
  )
24
36
  await chmod(new URL('../dist/cli/index.mjs', import.meta.url), 0o755)
@@ -23,5 +23,10 @@ branching, review, and release policy.
23
23
  6. Review the diff for accidental files, secrets, generated output, broken documentation links,
24
24
  and assumptions that belong in local instructions instead.
25
25
 
26
+ For portable implementation and review checks, read [implementation](references/implementation.md),
27
+ [review](references/review.md), and [evidence sweep](references/evidence-sweep.md). The older
28
+ [implementation and review](references/implementation-and-review.md) remains a compact overview.
29
+ Local instructions remain authoritative for commands, commits, review systems, and release policy.
30
+
26
31
  Do not invent a default branch, runner class, documentation root, review system, merge policy, or
27
32
  command catalog. A consumer wrapper or local instruction file owns those choices.
@@ -0,0 +1,8 @@
1
+ # Evidence sweep
2
+
3
+ Map every acceptance criterion to a focused test, inspection, or manual verification with a concrete
4
+ result. Run the required broader checks after focused validation. Treat skipped validation as a
5
+ finding: state the blocker, affected surface, and remaining risk.
6
+
7
+ Before handoff, compare the final diff to the accepted decision ledger and verify documentation,
8
+ generated output, and public contracts agree with the implementation.
@@ -0,0 +1,9 @@
1
+ # Implementation and review
2
+
3
+ Before editing, map callers, tests, configuration, and documentation that own the behavior. Keep the
4
+ change within the accepted boundary; record a new dependency or product decision before widening it.
5
+
6
+ After focused validation, inspect the complete diff for accidental generated files, secret exposure,
7
+ dead paths, and documentation drift. Validate the public interface and failure paths, not only the
8
+ happy path. Before a reviewable commit, run the repository-required checks and state any skipped
9
+ checks with their concrete blocker.
@@ -0,0 +1,8 @@
1
+ # Implementation
2
+
3
+ Keep an accepted-decision ledger for interfaces, data boundaries, validation, and ownership choices
4
+ that could otherwise be lost during coding. Start from a failing behavioral test when practical and
5
+ complete the real path rather than adding placeholders or compatibility shims without a requirement.
6
+
7
+ When behavior changes, update the public contract, documentation, fixtures, and generated artifacts
8
+ that describe it. Keep local commands, branch policy, and release mechanics in the consumer wrapper.
@@ -0,0 +1,7 @@
1
+ # Review
2
+
3
+ Review the complete diff, not only changed implementation lines. Check premise and scope, public
4
+ contract compatibility, failure paths, authorization, untrusted input, secrets, and security
5
+ boundaries. Remove dead paths and stale documentation rather than labeling them as legacy.
6
+
7
+ Record unresolved risk as an explicit accepted decision or follow-up, never as an unnoticed gap.
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: backend-vitest-test-authoring
3
+ description: Apply backend Vitest patterns for service, provider, persistence, and queue boundaries.
4
+ ---
5
+
6
+ # Backend Vitest test authoring
7
+
8
+ Apply [vitest-test-authoring](../vitest-test-authoring/SKILL.md) first. Exercise real integration
9
+ boundaries only when their lifecycle is controlled by the test environment; otherwise mock the
10
+ network or provider edge. Randomize fixture identities where shared stores can collide, assert
11
+ authorization and retry behavior at boundaries, and clean up resources deterministically.
12
+
13
+ Consumer wrappers own database setup, queue providers, test projects, and rate-limit policy.
14
+
15
+ Read [integration boundaries](references/integration-boundaries.md) for fixture and collision safety.
@@ -0,0 +1,10 @@
1
+ # Integration boundaries
2
+
3
+ Use real persistence, queue, and internal-service boundaries when the test environment controls
4
+ their lifecycle. Mock only external provider edges. Create collision-safe fixture identities and
5
+ register event listeners before the mutation that produces them. Cleanup must be ownership-scoped;
6
+ never broadly reset shared state used by concurrent tests.
7
+
8
+ Assert persisted state, emitted work, or externally visible results at the boundary. A mock-only
9
+ test is insufficient when import wiring, serialization, transaction behavior, or retries are part
10
+ of the contract.
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: dotnet-test-authoring
3
+ description: Add maintainable .NET tests with appropriate unit, app, and integration boundaries.
4
+ ---
5
+
6
+ # .NET test authoring
7
+
8
+ Place tests at the narrowest layer that proves the behavior. Batch native or external selection
9
+ where the project requires it, isolate process and filesystem state, and assert serialized contracts
10
+ instead of private implementation details. Keep test helpers typed and reusable only when they remove
11
+ repeated setup without hiding important expectations.
12
+
13
+ Consumer wrappers own solution layout, test frameworks, native dependencies, and coverage policy.
14
+
15
+ Keep portable library tests separate from rendered application tests; batch compatible native targets
16
+ in one selection pass so a shared build validates the same source set.
@@ -0,0 +1,158 @@
1
+ {
2
+ "version": 1,
3
+ "skills": [
4
+ {
5
+ "name": "agent-workflow",
6
+ "plugin": "vouchington-workflow",
7
+ "pluginVersion": "0.3.0",
8
+ "path": "agent-workflow/SKILL.md"
9
+ },
10
+ {
11
+ "name": "backend-vitest-test-authoring",
12
+ "plugin": "vouchington-testing",
13
+ "pluginVersion": "0.1.0",
14
+ "path": "backend-vitest-test-authoring/SKILL.md",
15
+ "prerequisites": ["vitest-test-authoring"]
16
+ },
17
+ {
18
+ "name": "blackboard",
19
+ "plugin": "vouchington-workflow",
20
+ "pluginVersion": "0.3.0",
21
+ "path": "blackboard/SKILL.md"
22
+ },
23
+ {
24
+ "name": "dotnet-test-authoring",
25
+ "plugin": "vouchington-testing",
26
+ "pluginVersion": "0.1.0",
27
+ "path": "dotnet-test-authoring/SKILL.md"
28
+ },
29
+ {
30
+ "name": "git-commit-checklist",
31
+ "plugin": "vouchington-workflow",
32
+ "pluginVersion": "0.3.0",
33
+ "path": "git-commit-checklist/SKILL.md"
34
+ },
35
+ {
36
+ "name": "github-actions-checklist",
37
+ "plugin": "vouchington-workflow",
38
+ "pluginVersion": "0.3.0",
39
+ "path": "github-actions-checklist/SKILL.md"
40
+ },
41
+ {
42
+ "name": "github-issue",
43
+ "plugin": "vouchington-workflow",
44
+ "pluginVersion": "0.3.0",
45
+ "path": "github-issue/SKILL.md"
46
+ },
47
+ {
48
+ "name": "nextjs-vitest-test-authoring",
49
+ "plugin": "vouchington-testing",
50
+ "pluginVersion": "0.1.0",
51
+ "path": "nextjs-vitest-test-authoring/SKILL.md",
52
+ "prerequisites": ["vitest-test-authoring"]
53
+ },
54
+ {
55
+ "name": "organize-github-issues",
56
+ "plugin": "vouchington-workflow",
57
+ "pluginVersion": "0.3.0",
58
+ "path": "organize-github-issues/SKILL.md"
59
+ },
60
+ {
61
+ "name": "package-json-checklist",
62
+ "plugin": "vouchington-workflow",
63
+ "pluginVersion": "0.3.0",
64
+ "path": "package-json-checklist/SKILL.md"
65
+ },
66
+ {
67
+ "name": "planning",
68
+ "plugin": "vouchington-workflow",
69
+ "pluginVersion": "0.3.0",
70
+ "path": "planning/SKILL.md"
71
+ },
72
+ {
73
+ "name": "playwright-authoring",
74
+ "plugin": "vouchington-testing",
75
+ "pluginVersion": "0.1.0",
76
+ "path": "playwright-authoring/SKILL.md"
77
+ },
78
+ {
79
+ "name": "postgres-node-performance-tuning",
80
+ "plugin": "vouchington-database",
81
+ "pluginVersion": "0.1.0",
82
+ "path": "postgres-node-performance-tuning/SKILL.md"
83
+ },
84
+ {
85
+ "name": "postgres-partitioning-uuid-v7",
86
+ "plugin": "vouchington-database",
87
+ "pluginVersion": "0.1.0",
88
+ "path": "postgres-partitioning-uuid-v7/SKILL.md"
89
+ },
90
+ {
91
+ "name": "pr-description",
92
+ "plugin": "vouchington-workflow",
93
+ "pluginVersion": "0.3.0",
94
+ "path": "pr-description/SKILL.md"
95
+ },
96
+ {
97
+ "name": "retrospective",
98
+ "plugin": "vouchington-workflow",
99
+ "pluginVersion": "0.3.0",
100
+ "path": "retrospective/SKILL.md"
101
+ },
102
+ {
103
+ "name": "retrospective-distill",
104
+ "plugin": "vouchington-workflow",
105
+ "pluginVersion": "0.3.0",
106
+ "path": "retrospective-distill/SKILL.md"
107
+ },
108
+ {
109
+ "name": "review-ci-logs",
110
+ "plugin": "vouchington-workflow",
111
+ "pluginVersion": "0.3.0",
112
+ "path": "review-ci-logs/SKILL.md"
113
+ },
114
+ {
115
+ "name": "review-github-issue-taxonomy",
116
+ "plugin": "vouchington-workflow",
117
+ "pluginVersion": "0.3.0",
118
+ "path": "review-github-issue-taxonomy/SKILL.md"
119
+ },
120
+ {
121
+ "name": "revisit-followups",
122
+ "plugin": "vouchington-workflow",
123
+ "pluginVersion": "0.3.0",
124
+ "path": "revisit-followups/SKILL.md"
125
+ },
126
+ {
127
+ "name": "static-analysis-checklist",
128
+ "plugin": "vouchington-workflow",
129
+ "pluginVersion": "0.3.0",
130
+ "path": "static-analysis-checklist/SKILL.md"
131
+ },
132
+ {
133
+ "name": "storybook-authoring",
134
+ "plugin": "vouchington-testing",
135
+ "pluginVersion": "0.1.0",
136
+ "path": "storybook-authoring/SKILL.md"
137
+ },
138
+ {
139
+ "name": "swift-test-authoring",
140
+ "plugin": "vouchington-testing",
141
+ "pluginVersion": "0.1.0",
142
+ "path": "swift-test-authoring/SKILL.md"
143
+ },
144
+ {
145
+ "name": "test-authoring",
146
+ "plugin": "vouchington-testing",
147
+ "pluginVersion": "0.1.0",
148
+ "path": "test-authoring/SKILL.md"
149
+ },
150
+ {
151
+ "name": "vitest-test-authoring",
152
+ "plugin": "vouchington-testing",
153
+ "pluginVersion": "0.1.0",
154
+ "path": "vitest-test-authoring/SKILL.md",
155
+ "prerequisites": ["test-authoring"]
156
+ }
157
+ ]
158
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: nextjs-vitest-test-authoring
3
+ description: Apply Vitest patterns for Next.js components, routes, and server-side modules.
4
+ ---
5
+
6
+ # Next.js Vitest test authoring
7
+
8
+ Apply [vitest-test-authoring](../vitest-test-authoring/SKILL.md) first. Mock framework navigation,
9
+ headers, and server-only boundaries at the framework edge, then test component behavior through
10
+ rendered output and user-visible state. Keep API-response fixtures representative and avoid testing
11
+ framework internals. Use browser tests for behavior that cannot be represented in the test runtime.
12
+
13
+ Consumer wrappers own framework mock helpers, render libraries, and route fixture conventions.
14
+
15
+ Read [framework boundaries](references/framework-boundaries.md) for module shape and server-only cases.
@@ -0,0 +1,9 @@
1
+ # Framework boundaries
2
+
3
+ Mock framework navigation, headers, cookies, and server-only APIs at the framework edge, not inside
4
+ the component or route under test. Preserve typed module shape and use the consumer's shared mocks
5
+ when they exist. Build API responses with typed factories rather than inline partial objects.
6
+
7
+ Use direct rendering for component behavior. If request scope, hydration, browser APIs, or server
8
+ module resolution cannot be represented faithfully, move the assertion to an integration or browser
9
+ test instead of widening a unit-test mock.
@@ -19,5 +19,8 @@ Use before implementation work that needs a durable plan. Repository-local `AGEN
19
19
  5. Validate and save the plan using the repository's required issue or document workflow before
20
20
  implementation when local policy requires one.
21
21
 
22
+ For cross-cutting changes, read [impact discovery](references/impact-discovery.md) before selecting
23
+ tests or concluding that a surface has no dependents.
24
+
22
25
  Do not invent a plan template, default repository, issue taxonomy, dependency graph tool, or
23
26
  approval workflow. A consumer wrapper owns those choices.
@@ -0,0 +1,9 @@
1
+ # Impact discovery
2
+
3
+ Start from the changed surface and trace imports, callers, configuration, generated artifacts, and
4
+ tests. Use the repository's graph or search tools where available; otherwise record the evidence
5
+ used and the uncertainty that remains. Select focused tests from real dependents, then include
6
+ broader validation when a shared contract, package boundary, or generated artifact changes.
7
+
8
+ Do not treat a tool's incomplete graph as proof that no dependent exists. Escalate uncertain
9
+ high-risk boundaries for independent review or a broader test selection.