@csark0812/skeleton 1.1.0 → 1.1.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/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Skeleton
2
2
 
3
+ **Source of truth for** Package overview.
4
+
5
+ <!-- doc-meta: owner=eng | last-reviewed=2026-07-13 -->
6
+
3
7
  Single source of truth (SSOT) linter for agent-enabled repos.
4
8
 
5
9
  Agent repos accumulate skills, rules, registries, and cross-linked docs faster than anyone can keep them straight by hand. Code repos solved this decades ago with ESLint — deterministic checks, CI gates, fix what you can before merge. Skeleton does the same job for documentation architecture: what's canonical, what links where, what must not exist, and whether your skill overrides are wired correctly.
@@ -83,12 +87,12 @@ skeleton customize resolve <slug>
83
87
 
84
88
  **Validate changed** routes git diffs to the right audit:
85
89
 
86
- | Path | Action |
87
- | -------------------------------------------- | --------------------------- |
88
- | Docs/skills in scan perimeter | path-scoped audit |
89
- | `.sh`, `.bash`, `.zsh` | shellcheck or `bash -n` |
90
- | Other `.json` | JSONC-tolerant syntax check |
91
- | `.ts`, `.py`, `package.json`, `project.json` | skip |
90
+ | Path | Action |
91
+ | ----------------------------------------------------------------- | --------------------------- |
92
+ | Docs/skills in scan perimeter | path-scoped audit |
93
+ | `.sh`, `.bash`, `.zsh` | shellcheck or `bash -n` |
94
+ | Other `.json` | JSONC-tolerant syntax check |
95
+ | `.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, `.cjs`, `.py`, `package.json`, `project.json` | skip (exits 1 if all skip) |
92
96
 
93
97
  Pre-commit: `skeleton validate changed --staged` (path-scoped, fast).
94
98
  CI: `skeleton validate changed --base origin/main` (global rules first, then changed files).
@@ -128,11 +132,18 @@ One command for humans: `npx skeleton init --skills`.
128
132
 
129
133
  ## Development
130
134
 
131
- Requires Node ≥ 22. Uses Bun for dev and tests.
135
+ Requires Node ≥ 22. Uses Bun for dev and tests. Agent cold-start: [AGENTS.md](AGENTS.md).
132
136
 
133
137
  ```bash
134
138
  bun install
135
139
  bun test
140
+ bun run typecheck
136
141
  bun run build
137
142
  bun run audit:self
138
143
  ```
144
+
145
+ `validate:changed` is docs/skills only — it skips code/config extensions (see table). All-skip inputs exit non-zero. Use `bun test` and `bun run typecheck` for code.
146
+
147
+ ### Publishing 1.1.2 (human)
148
+
149
+ Keep version `1.1.2` until `npm publish`. Preflight: `bun test && bun run typecheck && bun run build && bun run audit:self`. Then publish and tag when ready (npm is still on `1.1.1` until then).
package/dist/cli.js CHANGED
@@ -16536,7 +16536,8 @@ var $visitAsync = visit.visitAsync;
16536
16536
  // src/audit/config/load.ts
16537
16537
  var SCHEMA_CANDIDATES = [
16538
16538
  join(dirname2(fileURLToPath2(import.meta.url)), "../../../schemas/config.schema.json"),
16539
- join(dirname2(fileURLToPath2(import.meta.url)), "../schemas/config.schema.json")
16539
+ join(dirname2(fileURLToPath2(import.meta.url)), "../schemas/config.schema.json"),
16540
+ join(dirname2(fileURLToPath2(import.meta.url)), "../../schemas/config.schema.json")
16540
16541
  ];
16541
16542
  function resolveSchemaPath() {
16542
16543
  for (const candidate of SCHEMA_CANDIDATES) {
@@ -16594,6 +16595,9 @@ function mergedExcludes(config) {
16594
16595
  function retiredSkills(config) {
16595
16596
  return config.scan.retiredSkills ?? [];
16596
16597
  }
16598
+ function nonPublicSkills(config) {
16599
+ return config.scan.nonPublicSkills ?? [];
16600
+ }
16597
16601
 
16598
16602
  // src/audit/core/skill-roots.ts
16599
16603
  import { existsSync as existsSync2, readdirSync as readdirSync2, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
@@ -28604,7 +28608,6 @@ function isGeneratedReference(content3) {
28604
28608
  }
28605
28609
 
28606
28610
  // src/audit/rules/skill-index.ts
28607
- var NON_PUBLIC_SLUGS = new Set(["align-commands"]);
28608
28611
  function walkSkillMarkdown(dir) {
28609
28612
  const files = [];
28610
28613
  if (!existsSync8(dir))
@@ -28658,6 +28661,7 @@ function scanFileForSkillLinks(ctx, filePath, index2) {
28658
28661
  }
28659
28662
  function validateReadmeTaxonomy(ctx, index2, diskSlugs) {
28660
28663
  const issues = [];
28664
+ const nonPublic = new Set(nonPublicSkills(ctx.config));
28661
28665
  for (const skillRoot of index2.roots) {
28662
28666
  if (skillRoot.kind !== "nested")
28663
28667
  continue;
@@ -28669,7 +28673,7 @@ function validateReadmeTaxonomy(ctx, index2, diskSlugs) {
28669
28673
  continue;
28670
28674
  const taxonomySlugs = parseReadmeTaxonomySlugs(readme);
28671
28675
  const nestedSlugs = diskSlugs.filter((slug2) => existsSync8(join8(ctx.root, skillRoot.relPath, slug2, "SKILL.md")));
28672
- const publicSlugs = nestedSlugs.filter((slug2) => !NON_PUBLIC_SLUGS.has(slug2));
28676
+ const publicSlugs = nestedSlugs.filter((slug2) => !nonPublic.has(slug2));
28673
28677
  const relReadme = `${skillRoot.relPath}/README.md`;
28674
28678
  for (const slug2 of publicSlugs) {
28675
28679
  if (!taxonomySlugs.includes(slug2)) {
@@ -28763,10 +28767,16 @@ function findLocalCanonicalLinks(root2, content3, sourceFile) {
28763
28767
  }
28764
28768
  const inReferencesDir = /\/references\//.test(sourceFile);
28765
28769
  if (inReferencesDir) {
28770
+ const refsIdx = sourceFile.lastIndexOf("/references/");
28771
+ const withinRefs = sourceFile.slice(refsIdx + "/references/".length);
28772
+ const withinDir = withinRefs.includes("/") ? withinRefs.slice(0, withinRefs.lastIndexOf("/")) : "";
28766
28773
  const siblingRe = /\((?!https?:|#|\.\.\/)([a-z0-9./_-]+\.md)\)/gi;
28767
28774
  for (const match of content3.matchAll(siblingRe)) {
28768
- const refPath = normalizeRelPath(match[1] ?? "");
28769
- if (!refPath || !canonicalExists(root2, refPath))
28775
+ const raw = normalizeRelPath(match[1] ?? "");
28776
+ if (!raw)
28777
+ continue;
28778
+ const refPath = withinDir ? normalizeRelPath(join9(withinDir, raw)) : raw;
28779
+ if (!canonicalExists(root2, refPath))
28770
28780
  continue;
28771
28781
  links.push({ refPath, sourceFile });
28772
28782
  }
@@ -28795,6 +28805,21 @@ function discoverSkillReferencePlans(root2) {
28795
28805
  links.push(link2);
28796
28806
  }
28797
28807
  }
28808
+ const queue = [...refPaths];
28809
+ while (queue.length > 0) {
28810
+ const refPath = queue.pop();
28811
+ if (!refPath || !canonicalExists(root2, refPath))
28812
+ continue;
28813
+ const canonicalContent = readFileSync8(join9(root2, CANONICAL_REFS_DIR, refPath), "utf8");
28814
+ const syntheticSource = generatedRefPath(slug2, refPath);
28815
+ for (const link2 of findLocalCanonicalLinks(root2, canonicalContent, syntheticSource)) {
28816
+ if (refPaths.has(link2.refPath))
28817
+ continue;
28818
+ refPaths.add(link2.refPath);
28819
+ links.push(link2);
28820
+ queue.push(link2.refPath);
28821
+ }
28822
+ }
28798
28823
  if (refPaths.size > 0) {
28799
28824
  plans.push({ skill: slug2, refPaths, links });
28800
28825
  }
@@ -29022,10 +29047,13 @@ function runAudit(options) {
29022
29047
 
29023
29048
  // src/customize/resolve.ts
29024
29049
  import { existsSync as existsSync11, readFileSync as readFileSync10 } from "node:fs";
29025
- import { join as join11, relative as relative8 } from "node:path";
29050
+ import { basename as basename2, join as join11, relative as relative8 } from "node:path";
29026
29051
  var CUSTOMIZE_PREFIX = "Customize: ";
29052
+ function customizeDir(root2) {
29053
+ return join11(root2, REGISTRY_DIR_REL, "customize");
29054
+ }
29027
29055
  function customizePathForSlug(root2, slug2) {
29028
- return join11(root2, REGISTRY_DIR_REL, "customize", `${slug2}.md`);
29056
+ return join11(customizeDir(root2), `${slug2}.md`);
29029
29057
  }
29030
29058
  function findCustomizeViaRegistry(root2, slug2) {
29031
29059
  for (const rel of parseRegistryPaths(root2)) {
@@ -29036,11 +29064,10 @@ function findCustomizeViaRegistry(root2, slug2) {
29036
29064
  }
29037
29065
  return null;
29038
29066
  }
29039
- function resolveCustomize(root2, slug2) {
29067
+ function resolveSlugFile(root2, slug2) {
29040
29068
  const direct = customizePathForSlug(root2, slug2);
29041
29069
  if (existsSync11(direct)) {
29042
29070
  return {
29043
- slug: slug2,
29044
29071
  content: readFileSync10(direct, "utf8"),
29045
29072
  path: normalizeRelPath(relative8(root2, direct))
29046
29073
  };
@@ -29049,12 +29076,64 @@ function resolveCustomize(root2, slug2) {
29049
29076
  if (registryPath) {
29050
29077
  const abs = join11(root2, registryPath);
29051
29078
  return {
29052
- slug: slug2,
29053
29079
  content: readFileSync10(abs, "utf8"),
29054
29080
  path: registryPath
29055
29081
  };
29056
29082
  }
29057
- return { slug: slug2, content: null, path: null };
29083
+ return { content: null, path: null };
29084
+ }
29085
+ function alwaysIncludeBasenames(root2) {
29086
+ try {
29087
+ const config = loadConfig(root2);
29088
+ return config.customize?.alwaysInclude ?? [];
29089
+ } catch {
29090
+ return [];
29091
+ }
29092
+ }
29093
+ function readAlwaysInclude(root2, basenames, skipBasename) {
29094
+ const parts = [];
29095
+ const paths = [];
29096
+ const dir = customizeDir(root2);
29097
+ for (const name of basenames) {
29098
+ const file = basename2(name);
29099
+ if (skipBasename && file === skipBasename)
29100
+ continue;
29101
+ const abs = join11(dir, file);
29102
+ if (!existsSync11(abs))
29103
+ continue;
29104
+ parts.push(readFileSync10(abs, "utf8").trimEnd());
29105
+ paths.push(normalizeRelPath(relative8(root2, abs)));
29106
+ }
29107
+ return { parts, paths };
29108
+ }
29109
+ function resolveCustomize(root2, slug2) {
29110
+ const slugFile = resolveSlugFile(root2, slug2);
29111
+ const alwaysNames = alwaysIncludeBasenames(root2);
29112
+ const skip = slugFile.path != null ? basename2(slugFile.path) : null;
29113
+ const always = readAlwaysInclude(root2, alwaysNames, skip);
29114
+ const parts = [];
29115
+ const included = [];
29116
+ if (slugFile.content != null && slugFile.content.trim().length > 0) {
29117
+ parts.push(slugFile.content.trimEnd());
29118
+ if (slugFile.path)
29119
+ included.push(slugFile.path);
29120
+ }
29121
+ parts.push(...always.parts);
29122
+ included.push(...always.paths);
29123
+ if (parts.length === 0) {
29124
+ return { slug: slug2, content: null, path: slugFile.path, included: [] };
29125
+ }
29126
+ return {
29127
+ slug: slug2,
29128
+ content: parts.join(`
29129
+
29130
+ ---
29131
+
29132
+ `) + `
29133
+ `,
29134
+ path: slugFile.path ?? always.paths[0] ?? null,
29135
+ included
29136
+ };
29058
29137
  }
29059
29138
  function resolveCustomizeFromRoot(slug2, startDir) {
29060
29139
  const root2 = findRepoRoot(startDir);
@@ -29610,7 +29689,7 @@ function registerPath(options) {
29610
29689
 
29611
29690
  // src/validate/changed.ts
29612
29691
  import { existsSync as existsSync17, readFileSync as readFileSync14 } from "node:fs";
29613
- import { basename as basename2, extname, join as join17 } from "node:path";
29692
+ import { basename as basename3, extname, join as join17 } from "node:path";
29614
29693
  import { spawnSync as spawnSync4 } from "node:child_process";
29615
29694
 
29616
29695
  // src/validate/git-diff.ts
@@ -29641,7 +29720,7 @@ var COMMAND_CONFIG_NAMES = new Set(["package.json", "project.json"]);
29641
29720
  function bucketFor(relPath2, root2) {
29642
29721
  const normalized = normalizeRelPath(relPath2);
29643
29722
  const ext = extname(normalized).toLowerCase();
29644
- const name = basename2(normalized);
29723
+ const name = basename3(normalized);
29645
29724
  if (SKIP_EXTENSIONS.has(ext))
29646
29725
  return "skip";
29647
29726
  if (COMMAND_CONFIG_NAMES.has(name))
@@ -29732,6 +29811,7 @@ function runValidateChanged(options = {}) {
29732
29811
  }
29733
29812
  buckets[bucket].push(relPath2);
29734
29813
  }
29814
+ const audited = buckets.docs.length + buckets.skills.length + buckets.shell.length + buckets.json.length;
29735
29815
  let exitCode = 0;
29736
29816
  if (options.base) {
29737
29817
  const globalExit = runAudit({
@@ -29746,6 +29826,12 @@ function runValidateChanged(options = {}) {
29746
29826
  if (globalExit !== 0)
29747
29827
  exitCode = 1;
29748
29828
  }
29829
+ if (skipped > 0 && audited === 0) {
29830
+ console.error(`validate changed: all paths were skipped (code/config). This does not verify TypeScript or app code.
29831
+ ` + ` In this repo: bun test && bun run typecheck
29832
+ ` + " Consumers: run your local code validation gates.");
29833
+ return 1;
29834
+ }
29749
29835
  if (buckets.docs.length > 0) {
29750
29836
  const docExit = runAudit({
29751
29837
  suite: "docs",
@@ -30007,7 +30093,12 @@ function main() {
30007
30093
  console.error("register: path required");
30008
30094
  process.exit(1);
30009
30095
  }
30010
- registerPath(opts);
30096
+ registerPath({
30097
+ path: opts.path,
30098
+ topic: opts.topic,
30099
+ dryRun: opts.dryRun,
30100
+ json: opts.json
30101
+ });
30011
30102
  process.exit(0);
30012
30103
  }
30013
30104
  if (command === "customize" && argv[1] === "resolve") {