dsh-plugin-guide 0.3.31 → 0.3.33

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.
@@ -0,0 +1,162 @@
1
+ // Assert that the numbers this project states about itself match reality.
2
+ //
3
+ // Why this exists: the audits found the same class of defect repeatedly, always
4
+ // a number stated in prose that no longer matched the thing it described.
5
+ // * the family roster was written four different ways in four files;
6
+ // * the pitfall list was "30+" in English and "20+" in three translations;
7
+ // * the mirror was described as "215 pages" long after it reached 429;
8
+ // * `unfixed-issues.md` carried four separate arithmetic slips ("4 items ...
9
+ // 5 items").
10
+ // None of those are content errors — they are drift between a claim and its
11
+ // source. `data/facts.json` is the single home for the numbers; this script
12
+ // checks the claims a machine can verify.
13
+ //
14
+ // Usage: node scripts/check-facts.mjs [--json]
15
+
16
+ import { readFileSync, readdirSync, statSync } from 'node:fs'
17
+ import { join, relative } from 'node:path'
18
+
19
+ const root = process.cwd()
20
+ const asJson = process.argv.includes('--json')
21
+ const facts = JSON.parse(readFileSync(join(root, 'data', 'facts.json'), 'utf8'))
22
+
23
+ const failures = []
24
+ const checked = []
25
+ const ok = (name, detail) => checked.push(`${name}: ${detail}`)
26
+ const bad = (name, detail) => failures.push(`${name}: ${detail}`)
27
+
28
+ const read = (p) => readFileSync(join(root, p), 'utf8')
29
+
30
+ /** Count `## ` sections in a document. */
31
+ const sections = (text) => text.split(/\r?\n/).filter((l) => /^##\s/.test(l)).length
32
+
33
+ /** Count top-level bullets inside the section starting at `heading`. */
34
+ function bulletsIn(text, heading) {
35
+ const lines = text.split(/\r?\n/)
36
+ const start = lines.findIndex((l) => l.trim() === `## ${heading}`)
37
+ if (start < 0) return -1
38
+ let end = lines.length
39
+ for (let i = start + 1; i < lines.length; i++) {
40
+ if (/^##\s/.test(lines[i])) { end = i; break }
41
+ }
42
+ return lines.slice(start + 1, end).filter((l) => /^- \S/.test(l)).length
43
+ }
44
+
45
+ // ---- 1. README structure claims -------------------------------------------------
46
+ {
47
+ const text = read('README.md')
48
+ const got = sections(text)
49
+ if (got !== facts.readme.sections) bad('README sections', `${got} in README.md vs ${facts.readme.sections} in facts.json`)
50
+ else ok('README sections', String(got))
51
+
52
+ const wyg = bulletsIn(text, 'What you get')
53
+ if (wyg !== facts.readme.whatYouGetEntries) bad('What you get entries', `${wyg} vs ${facts.readme.whatYouGetEntries}`)
54
+ else ok('What you get entries', String(wyg))
55
+
56
+ // Every translation must match too; the parity gate covers structure, but this
57
+ // catches the case where a translation lost entries AND facts.json was not updated.
58
+ for (const f of ['README-zh.md', 'README-es.md', 'README-pt.md', 'README-hi.md']) {
59
+ let t
60
+ try { t = read(f) } catch { continue }
61
+ const n = bulletsIn(t, 'What you get')
62
+ if (n !== facts.readme.whatYouGetEntries) bad(`${f} "What you get"`, `${n} vs ${facts.readme.whatYouGetEntries}`)
63
+ }
64
+ }
65
+
66
+ // ---- 2. Knowledge-base inventory -------------------------------------------------
67
+ {
68
+ const guide = readdirSync(join(root, 'guide')).filter((f) => f.endsWith('.md')).length
69
+ if (guide !== facts.knowledgeBase.guideDocuments) bad('guide/ documents', `${guide} vs ${facts.knowledgeBase.guideDocuments}`)
70
+ else ok('guide/ documents', String(guide))
71
+
72
+ const refs = readdirSync(join(root, 'references'))
73
+ .filter((f) => f.endsWith('.md') && statSync(join(root, 'references', f)).isFile()).length
74
+ if (refs !== facts.knowledgeBase.referenceDocuments) bad('references/ documents', `${refs} vs ${facts.knowledgeBase.referenceDocuments}`)
75
+ else ok('references/ documents', String(refs))
76
+ }
77
+
78
+ // ---- 3. Mirror claims ------------------------------------------------------------
79
+ {
80
+ const snap = read('references/official-docs/SNAPSHOT.md')
81
+ const commit = /^\|\s*Source commit\s*\|\s*`([0-9a-f]{40})`\s*\|/m.exec(snap)?.[1]
82
+ if (commit !== facts.mirror.pinnedCommit) bad('mirror pinned commit', `${commit} in SNAPSHOT.md vs ${facts.mirror.pinnedCommit} in facts.json`)
83
+ else ok('mirror pinned commit', commit.slice(0, 7))
84
+
85
+ // Count the mirrored markdown pages and the zh pairs independently.
86
+ let md = 0
87
+ let zh = 0
88
+ const walk = (dir) => {
89
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
90
+ const p = join(dir, e.name)
91
+ if (e.isDirectory()) walk(p)
92
+ else if (e.name.endsWith('.md')) {
93
+ md++
94
+ if (e.name.endsWith('.zh.md')) zh++
95
+ }
96
+ }
97
+ }
98
+ walk(join(root, 'references', 'official-docs', 'docs'))
99
+ if (md !== facts.mirror.mdPages) bad('mirror md pages', `${md} counted vs ${facts.mirror.mdPages} in facts.json`)
100
+ else ok('mirror md pages', String(md))
101
+ if (zh !== facts.mirror.zhPairs) bad('mirror zh pairs', `${zh} counted vs ${facts.mirror.zhPairs} in facts.json`)
102
+ else ok('mirror zh pairs', String(zh))
103
+
104
+ // Any document quoting a page count must quote the current one.
105
+ for (const f of ['guide/quick-reference.md', 'guide/quick-reference.zh-CN.md', 'references/sources.md']) {
106
+ let t
107
+ try { t = read(f) } catch { continue }
108
+ const stale = /\b(215|369|182)\s*(?:md|pages|篇)/i.exec(t)
109
+ if (stale) bad(`${f} stale page count`, `states "${stale[0]}" but the mirror has ${facts.mirror.mdPages} pages`)
110
+ }
111
+ }
112
+
113
+ // ---- 4. CLI check counts ---------------------------------------------------------
114
+ {
115
+ const src = read('src/cli/skill-sections.ts')
116
+ const n = [...src.matchAll(/^\s*'[a-z0-9-]+':\s*\{/gm)].length
117
+ if (n !== facts.cli.staticChecks) bad('CLI static checks', `${n} citations vs ${facts.cli.staticChecks} in facts.json`)
118
+ else ok('CLI static checks', String(n))
119
+ }
120
+
121
+ // ---- 5. Seam index consistency ---------------------------------------------------
122
+ {
123
+ const idx = JSON.parse(read('data/seams/index.json'))
124
+ const corridor = idx.seams.filter((s) => /^L\d+$/.test(s.id)).length
125
+ if (corridor !== facts.seams.corridorL) bad('corridor L seams', `${corridor} vs ${facts.seams.corridorL}`)
126
+ else ok('corridor L seams', String(corridor))
127
+
128
+ const areas = idx.capabilities.filter((c) => /^A\d+$/.test(c.id)).length
129
+ if (areas !== facts.seams.capabilityAreas) bad('capability areas', `${areas} vs ${facts.seams.capabilityAreas}`)
130
+ else ok('capability areas', String(areas))
131
+
132
+ const corridorSeams = idx.seams.filter((s) => /^L\d+$/.test(s.id))
133
+ const detectable = corridorSeams.filter((s) => s.detectable).length
134
+ const advisory = corridorSeams.length - detectable
135
+ if (detectable !== facts.seams.staticallyDetectable) bad('statically detectable seams', `${detectable} vs ${facts.seams.staticallyDetectable}`)
136
+ else ok('statically detectable seams', String(detectable))
137
+ if (advisory !== facts.seams.advisoryOnly) bad('advisory-only seams', `${advisory} vs ${facts.seams.advisoryOnly}`)
138
+ else ok('advisory-only seams', String(advisory))
139
+ }
140
+
141
+ // ---- 6. Release consistency ------------------------------------------------------
142
+ {
143
+ const pkg = JSON.parse(read('package.json'))
144
+ const changelog = read('CHANGELOG.md')
145
+ if (!changelog.includes(`## [${pkg.version}]`)) {
146
+ bad('CHANGELOG section', `no "## [${pkg.version}]" section for the current package.json version`)
147
+ } else ok('CHANGELOG section', pkg.version)
148
+ }
149
+
150
+ if (asJson) {
151
+ console.log(JSON.stringify({ facts: facts.schema, checked, failures }, null, 2))
152
+ } else {
153
+ for (const c of checked) console.log(` ok ${c}`)
154
+ if (failures.length) {
155
+ console.error('\ncheck-facts: FAIL')
156
+ for (const f of failures) console.error(` - ${f}`)
157
+ console.error('\nEvery number above has one home: data/facts.json. Update it, then update the prose.')
158
+ } else {
159
+ console.log(`\ncheck-facts OK (${checked.length} assertions against data/facts.json)`)
160
+ }
161
+ }
162
+ process.exit(failures.length ? 1 : 0)
@@ -0,0 +1,117 @@
1
+ // Measure how much of the offline mirror is actually referenced.
2
+ //
3
+ // `references/mirror-scope-decision.md` records a rejected proposal: drop the
4
+ // mirror content that no authored document references, to save ~80% of the
5
+ // published package. This script is the evidence. Run it before reviving that
6
+ // proposal — the answer depends on whether directory globs count as references,
7
+ // and this repository declares several broad ones.
8
+ //
9
+ // Usage: node scripts/measure-mirror-references.mjs [--json]
10
+
11
+ import { readdirSync, readFileSync, statSync } from 'node:fs'
12
+ import { join } from 'node:path'
13
+
14
+ const root = process.cwd()
15
+ const mirrorRoot = join(root, 'references', 'official-docs')
16
+ const json = process.argv.includes('--json')
17
+
18
+ /** Documents that count as "the knowledge base" when looking for references. */
19
+ function authoredFiles() {
20
+ const out = []
21
+ const add = (p) => {
22
+ try { if (statSync(p).isFile()) out.push(p) } catch { /* optional */ }
23
+ }
24
+ add(join(root, 'SKILL.md'))
25
+ add(join(root, 'llms.txt'))
26
+ for (const dir of ['guide', 'references']) {
27
+ try {
28
+ for (const f of readdirSync(join(root, dir))) add(join(root, dir, f))
29
+ } catch { /* optional */ }
30
+ }
31
+ for (const f of readdirSync(root)) if (/^README.*\.md$/.test(f)) add(join(root, f))
32
+ return out.filter((p) => !p.includes('official-docs') && !p.includes('mirror-scope'))
33
+ }
34
+
35
+ const authored = authoredFiles()
36
+ const corpus = authored.map((p) => readFileSync(p, 'utf8'))
37
+
38
+ // Everything the corpus names: full paths, bare basenames, and directory globs.
39
+ const paths = new Set()
40
+ const names = new Set()
41
+ const globDirs = new Set()
42
+ for (const text of corpus) {
43
+ for (const m of text.matchAll(/references\/official-docs\/([A-Za-z0-9._/-]+)/g)) paths.add(m[1])
44
+ for (const m of text.matchAll(/\bdocs\/[A-Za-z0-9._/-]+/g)) paths.add(m[0])
45
+ for (const m of text.matchAll(/([A-Za-z0-9._-]+)\.(md|json|ya?ml|png)\b/g)) names.add(`${m[1]}.${m[2]}`)
46
+ for (const m of text.matchAll(/([A-Za-z0-9._/-]+)\/\*\*/g)) globDirs.add(m[1])
47
+ }
48
+
49
+ function walk(dir, base = '') {
50
+ const out = []
51
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
52
+ const rel = base ? `${base}/${e.name}` : e.name
53
+ if (e.isDirectory()) out.push(...walk(join(dir, e.name), rel))
54
+ else out.push(rel)
55
+ }
56
+ return out
57
+ }
58
+
59
+ /**
60
+ * How the corpus reaches this mirror path.
61
+ *
62
+ * A glob only covers a path when the glob directory is a PREFIX of it:
63
+ * `docs/persistence-changes/**` covers `docs/persistence-changes/x.json`, but a
64
+ * bare `docs/**` does not imply everything under `docs/`. Matching a segment name
65
+ * anywhere inflates the referenced count and produced a wrong "80% unreferenced"
66
+ * figure in the original audit.
67
+ */
68
+ function referencedBy(rel) {
69
+ if (paths.has(rel) || paths.has(`docs/${rel}`)) return 'exact'
70
+ const parts = rel.split('/')
71
+ for (let i = 1; i <= parts.length; i++) {
72
+ if (globDirs.has(parts.slice(0, i).join('/'))) return 'dir-glob'
73
+ }
74
+ if (names.has(parts[parts.length - 1])) return 'basename'
75
+ return null
76
+ }
77
+
78
+ const SKIP = new Set(['README.md', 'SNAPSHOT.md', 'LICENSE', 'CLAUDE.md'])
79
+ const buckets = { exact: [], 'dir-glob': [], basename: [], unreferenced: [] }
80
+ for (const rel of walk(mirrorRoot).sort()) {
81
+ if (SKIP.has(rel)) continue
82
+ const size = statSync(join(mirrorRoot, rel)).size
83
+ buckets[referencedBy(rel) ?? 'unreferenced'].push({ rel, size })
84
+ }
85
+
86
+ const MB = (n) => (n / 1048576).toFixed(2) + ' MB'
87
+ const sum = (a) => a.reduce((s, x) => s + x.size, 0)
88
+
89
+ if (json) {
90
+ console.log(JSON.stringify({
91
+ authoredDocuments: authored.length,
92
+ buckets: Object.fromEntries(Object.entries(buckets).map(([k, v]) => [k, { files: v.length, bytes: sum(v) }])),
93
+ }, null, 2))
94
+ } else {
95
+ console.log(`authored documents scanned: ${authored.length}`)
96
+ console.log('\nmirror files by reference strength:')
97
+ for (const k of ['exact', 'dir-glob', 'basename', 'unreferenced']) {
98
+ console.log(` ${k.padEnd(14)} ${String(buckets[k].length).padStart(4)} files ${MB(sum(buckets[k])).padStart(10)}`)
99
+ }
100
+ const unreferenced = buckets.unreferenced
101
+ console.log(`\ngenuinely unreferenced: ${unreferenced.length} files, ${MB(sum(unreferenced))}`)
102
+ for (const x of unreferenced) console.log(` ${MB(x.size).padStart(10)} ${x.rel}`)
103
+
104
+ const byExt = new Map()
105
+ for (const x of buckets['dir-glob']) {
106
+ const ext = x.rel.slice(x.rel.lastIndexOf('.'))
107
+ const g = byExt.get(ext) ?? { n: 0, b: 0 }
108
+ g.n++
109
+ g.b += x.size
110
+ byExt.set(ext, g)
111
+ }
112
+ console.log('\nreachable only through a declared directory glob, by extension:')
113
+ for (const [ext, g] of [...byExt].sort((a, b) => b[1].b - a[1].b)) {
114
+ console.log(` ${ext.padEnd(12)} ${String(g.n).padStart(4)} files ${MB(g.b).padStart(10)}`)
115
+ }
116
+ console.log('\nSee references/mirror-scope-decision.md before acting on these numbers.')
117
+ }
@@ -32,6 +32,43 @@ check(existsSync(join(root, 'bin', 'dsh-plugin-dev.js')), 'bin/dsh-plugin-dev.js
32
32
  check(existsSync(join(root, 'templates', 'ts')), 'templates/ts exists')
33
33
  check(existsSync(join(root, 'templates', 'js')), 'templates/js exists')
34
34
 
35
+ // Files a subcommand reads at RUNTIME but that nothing else asserts are shipped.
36
+ // `data/seams/index.json` was missing from package.json `files[]` while the
37
+ // `seams` command already depended on it: the command worked from the source
38
+ // tree and would have failed for every installed user with "seam index not
39
+ // found". Reading the file in-repo cannot catch that, so assert the manifest
40
+ // declares it.
41
+ {
42
+ const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
43
+ const declared = (p) => pkg.files.some((f) => f === p || f.startsWith(`${p}/`) || f === `${p}`)
44
+ const runtimeInputs = [
45
+ ['data', 'data/seams/index.json read by `seams`'],
46
+ ['data', 'data/facts.json read by check-facts'],
47
+ ['SKILL.md', 'SKILL.md registered as the agent skill'],
48
+ ]
49
+ for (const [entry, why] of runtimeInputs) {
50
+ check(declared(entry), `package.json files[] ships ${why}`)
51
+ }
52
+ const seamIndex = JSON.parse(readFileSync(join(root, 'data', 'seams', 'index.json'), 'utf8'))
53
+ check(Array.isArray(seamIndex.seams) && seamIndex.seams.length > 0, 'seam index parses and is non-empty')
54
+ }
55
+
56
+ // The `seams` command must resolve its data through the BUILT bundle, since that
57
+ // is what ships; a source-tree-only path resolution works in tests and fails in
58
+ // the published artifact.
59
+ {
60
+ const seams = runNode(['dist/dsh-plugin-dev.js', 'seams', 'L3', '--json'], { stdio: ['ignore', 'pipe', 'pipe'] })
61
+ let parsed = null
62
+ try { parsed = JSON.parse(seams.stdout) } catch { parsed = null }
63
+ check(seams.status === 0 && parsed?.state === 'found', `bundled \`seams L3\` resolves its data (exit ${seams.status})`)
64
+
65
+ // The third state must survive bundling: an unmatched query reports unknown.
66
+ const unknown = runNode(['dist/dsh-plugin-dev.js', 'seams', 'zzz-nothing', '--json'], { stdio: ['ignore', 'pipe', 'pipe'] })
67
+ let u = null
68
+ try { u = JSON.parse(unknown.stdout) } catch { u = null }
69
+ check(unknown.status === 3 && u?.state === 'unknown', `bundled \`seams\` reports unknown with exit 3 (exit ${unknown.status})`)
70
+ }
71
+
35
72
  // 2. Version + dogfood self-check.
36
73
  const version = runNode(['dist/dsh-plugin-dev.js', '--version'])
37
74
  check(version.status === 0 && /\d+\.\d+\.\d+/.test(version.stdout.trim()), `--version prints a semver (${version.stdout.trim()})`)