@skill-graph/cli 0.5.7 → 0.5.8
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/CHANGELOG.md +27 -3
- package/README.md +40 -14
- package/SKILL_GRAPH.md +2 -2
- package/bin/skill-graph.js +118 -2
- package/docs/ADOPTION.md +1 -1
- package/docs/PRIMER.md +6 -5
- package/docs/QUICKSTART-30MIN.md +2 -2
- package/docs/SKILL_AUDIT_CHECKLIST.md +1 -1
- package/docs/SKILL_METADATA_PROTOCOL.md +2 -2
- package/docs/_archived/marketplace-publication-priority-2026-05-18.md +1 -1
- package/docs/_drafts/0.5.8-release-prep.md +164 -0
- package/docs/field-reference.generated.md +1 -1
- package/docs/field-reference.md +2 -2
- package/docs/manifest-field-mapping.md +3 -3
- package/docs/marketplace-publication-queue.generated.md +2 -2
- package/docs/plans/scripts-roadmap.md +2 -2
- package/docs/positioning.md +88 -0
- package/docs/research/skill-comprehension-eval-research.md +5 -5
- package/docs/research/skill-demand-gap-roadmap-2026-05-16.md +215 -0
- package/docs/status.generated.md +48 -0
- package/examples/audits/context-graph/findings.md +59 -0
- package/examples/audits/context-graph/scorecard.md +22 -0
- package/examples/audits/context-graph/verdict.md +33 -0
- package/examples/evals/a11y.json +45 -13
- package/examples/evals/api-design.json +18 -5
- package/examples/evals/code-review.json +18 -5
- package/examples/evals/data-modeling.json +18 -5
- package/examples/evals/database-migration.json +18 -5
- package/examples/evals/debugging.json +37 -11
- package/examples/evals/dependency-architecture.json +18 -5
- package/examples/evals/design-system-architecture.json +18 -5
- package/examples/evals/error-tracking.json +18 -5
- package/examples/evals/event-contract-design.json +18 -5
- package/examples/evals/form-ux-architecture.json +18 -5
- package/examples/evals/framework-fit-analysis.json +18 -5
- package/examples/evals/graph-audit.json +55 -13
- package/examples/evals/information-architecture.json +18 -5
- package/examples/evals/interaction-feedback.json +18 -5
- package/examples/evals/interaction-patterns.json +18 -5
- package/examples/evals/layout-composition.json +18 -5
- package/examples/evals/lint-overlay.json +38 -11
- package/examples/evals/microcopy.json +18 -5
- package/examples/evals/observability-modeling.json +18 -5
- package/examples/evals/pattern-recognition.json +32 -9
- package/examples/evals/performance-engineering.json +18 -5
- package/examples/evals/refactor.json +41 -12
- package/examples/evals/semiotics.json +18 -5
- package/examples/evals/skill-infrastructure.json +32 -9
- package/examples/evals/skill-router.json +42 -13
- package/examples/evals/system-interface-contracts.json +18 -5
- package/examples/evals/task-analysis.json +18 -5
- package/examples/evals/testing-strategy.json +36 -11
- package/examples/evals/type-safety.json +251 -66
- package/examples/evals/visual-design-foundations.json +18 -5
- package/examples/evals/webhook-integration.json +18 -5
- package/examples/fixture-skills/README.md +47 -0
- package/examples/fixture-skills/comprehension-full/SKILL.md +79 -0
- package/examples/fixture-skills/minimal-capability/SKILL.md +51 -0
- package/examples/fixture-skills/with-grounding/SKILL.md +78 -0
- package/examples/fixture-skills/with-relations/SKILL.md +87 -0
- package/examples/skills.manifest.sample.json +1722 -446
- package/marketplace/README.md +1 -1
- package/marketplace/skills/a11y/SKILL.md +1 -1
- package/marketplace/skills/best-practice/SKILL.md +211 -0
- package/marketplace/skills/context-graph/SKILL.md +1 -1
- package/marketplace/skills/debugging/SKILL.md +1 -1
- package/marketplace/skills/graph-audit/SKILL.md +3 -1
- package/marketplace/skills/postgres-rls/SKILL.md +284 -0
- package/marketplace/skills/refactor/SKILL.md +1 -1
- package/marketplace/skills/skill-infrastructure/SKILL.md +2 -0
- package/marketplace/skills/skill-router/SKILL.md +3 -1
- package/marketplace/skills/testing-strategy/SKILL.md +1 -1
- package/package.json +3 -1
- package/scripts/__tests__/test-marketplace-export.js +6 -2
- package/scripts/__tests__/test-v3-1-alias-contract.js +3 -3
- package/scripts/build-status-doc.js +177 -0
- package/scripts/check-doc-drift.js +224 -0
- package/scripts/check-markdown-links.js +34 -4
- package/scripts/check-mirror-freeze.js +270 -0
- package/scripts/export-marketplace-skills.js +35 -6
- package/scripts/lib/audit-prompt-builder.js +3 -3
- package/scripts/lib/parse-frontmatter.js +2 -2
- package/scripts/skill-audit.js +7 -9
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate `docs/status.generated.md` — a single-source-of-truth status
|
|
4
|
+
* snapshot that pulls live values from package.json, the schema, the
|
|
5
|
+
* generated manifest, and the deterministic check scripts.
|
|
6
|
+
*
|
|
7
|
+
* The intent is to make the project's trust surface auditable from one URL:
|
|
8
|
+
* a reader can see, without running any code, what the current package
|
|
9
|
+
* version, schema version, skill count, and check states are.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node scripts/build-status-doc.js # write docs/status.generated.md
|
|
13
|
+
* node scripts/build-status-doc.js --check # print summary, exit non-zero on drift
|
|
14
|
+
* node scripts/build-status-doc.js --stdout # print to stdout, do not write file
|
|
15
|
+
* node scripts/build-status-doc.js --no-checks # skip check execution (just version + count)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { spawnSync } = require('child_process');
|
|
23
|
+
|
|
24
|
+
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
25
|
+
const OUTPUT_PATH = path.join(REPO_ROOT, 'docs', 'status.generated.md');
|
|
26
|
+
|
|
27
|
+
function readJson(relPath) {
|
|
28
|
+
return JSON.parse(fs.readFileSync(path.join(REPO_ROOT, relPath), 'utf8'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function runCheck(scriptRelPath, label) {
|
|
32
|
+
const t0 = Date.now();
|
|
33
|
+
const r = spawnSync('node', [scriptRelPath], {
|
|
34
|
+
cwd: REPO_ROOT,
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
timeout: 60_000,
|
|
37
|
+
});
|
|
38
|
+
const duration_ms = Date.now() - t0;
|
|
39
|
+
if (r.error) {
|
|
40
|
+
return { label, status: 'ERROR', duration_ms, detail: r.error.message };
|
|
41
|
+
}
|
|
42
|
+
const tail = (r.stdout + r.stderr).trim().split('\n').slice(-1)[0] || '';
|
|
43
|
+
return {
|
|
44
|
+
label,
|
|
45
|
+
status: r.status === 0 ? 'PASS' : 'FAIL',
|
|
46
|
+
exit_code: r.status,
|
|
47
|
+
duration_ms,
|
|
48
|
+
detail: tail.slice(0, 200),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readSchemaVersion() {
|
|
53
|
+
const schema = readJson('schemas/skill.schema.json');
|
|
54
|
+
const sv = schema?.properties?.schema_version;
|
|
55
|
+
if (typeof sv?.const === 'number') return sv.const;
|
|
56
|
+
if (Array.isArray(sv?.oneOf)) {
|
|
57
|
+
for (const b of sv.oneOf) if (typeof b.const === 'number') return b.const;
|
|
58
|
+
}
|
|
59
|
+
return 'unknown';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readSkillCount() {
|
|
63
|
+
const manifestPath = path.join(REPO_ROOT, 'skills.manifest.json');
|
|
64
|
+
if (!fs.existsSync(manifestPath)) return null;
|
|
65
|
+
const m = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
66
|
+
return Array.isArray(m.skills) ? m.skills.length : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readMirrorStatus() {
|
|
70
|
+
const adrPath = path.join(REPO_ROOT, 'docs', 'adr', '0009-sibling-repo-deprecation.md');
|
|
71
|
+
if (!fs.existsSync(adrPath)) return 'unknown';
|
|
72
|
+
return 'docs-only mirrors per ADR 0009 (2026-05-18)';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function renderMarkdown(state) {
|
|
76
|
+
const { pkg, schema_version, skill_count, checks, generated_at, mirror_status } = state;
|
|
77
|
+
const checkRow = c => {
|
|
78
|
+
const badge = c.status === 'PASS' ? '✅ PASS' : c.status === 'SKIP' ? '⏭️ SKIP' : '❌ ' + c.status;
|
|
79
|
+
const detail = c.detail ? c.detail.replace(/\|/g, '\\|') : '';
|
|
80
|
+
return `| ${c.label} | ${badge} | ${c.duration_ms ?? '—'} ms | ${detail} |`;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return `# Skill Graph — Generated Status
|
|
84
|
+
|
|
85
|
+
> **Generated:** ${generated_at}
|
|
86
|
+
> **Generator:** \`node scripts/build-status-doc.js\` (regenerate; never hand-edit)
|
|
87
|
+
>
|
|
88
|
+
> This file is the single-source-of-truth status snapshot for the project's
|
|
89
|
+
> trust surface. Each value below is pulled from a deterministic origin:
|
|
90
|
+
> \`package.json\`, \`schemas/skill.schema.json\`, the generated manifest, ADR
|
|
91
|
+
> 0009, and the live exit code of each check script.
|
|
92
|
+
|
|
93
|
+
## Identity
|
|
94
|
+
|
|
95
|
+
| Field | Value | Source |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| Package name | \`${pkg.name}\` | \`package.json\` |
|
|
98
|
+
| Package version | \`${pkg.version}\` | \`package.json\` |
|
|
99
|
+
| Node engine | \`${pkg.engines?.node ?? '—'}\` | \`package.json\` |
|
|
100
|
+
| Active schema version | \`${schema_version}\` | \`schemas/skill.schema.json\` |
|
|
101
|
+
| Skill count (manifest) | \`${skill_count ?? '—'}\` | \`skills.manifest.json\` |
|
|
102
|
+
| Mirror status | ${mirror_status} | \`docs/adr/0009-sibling-repo-deprecation.md\` |
|
|
103
|
+
|
|
104
|
+
## Checks
|
|
105
|
+
|
|
106
|
+
| Check | Status | Duration | Last line |
|
|
107
|
+
|---|---|---|---|
|
|
108
|
+
${checks.map(checkRow).join('\n')}
|
|
109
|
+
|
|
110
|
+
## How to refresh
|
|
111
|
+
|
|
112
|
+
\`\`\`bash
|
|
113
|
+
node scripts/build-status-doc.js
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
\`docs/status.generated.md\` is regenerated and overwritten each run. CI
|
|
117
|
+
should commit the regenerated file alongside any code that affects the
|
|
118
|
+
underlying values (package version bump, schema bump, new lint check,
|
|
119
|
+
etc.).
|
|
120
|
+
|
|
121
|
+
## What this replaces
|
|
122
|
+
|
|
123
|
+
- Hand-maintained "Latest release" lines in README hero sections (drifted three minor versions in Phase 1).
|
|
124
|
+
- Ad-hoc "skill count" claims scattered across docs (drifted from 137 → 141 → 145 in Phase 1 alone).
|
|
125
|
+
- Manual "we run these checks" lists in CONTRIBUTING.
|
|
126
|
+
|
|
127
|
+
The reader is now one URL away from the truth.
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function main() {
|
|
132
|
+
const argv = process.argv.slice(2);
|
|
133
|
+
const opts = {
|
|
134
|
+
check: argv.includes('--check'),
|
|
135
|
+
stdout: argv.includes('--stdout'),
|
|
136
|
+
skipChecks: argv.includes('--no-checks'),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const pkg = readJson('package.json');
|
|
140
|
+
const schema_version = readSchemaVersion();
|
|
141
|
+
const skill_count = readSkillCount();
|
|
142
|
+
const mirror_status = readMirrorStatus();
|
|
143
|
+
const generated_at = new Date().toISOString();
|
|
144
|
+
|
|
145
|
+
const checks = opts.skipChecks ? [] : [
|
|
146
|
+
runCheck('scripts/check-markdown-links.js', 'check-markdown-links'),
|
|
147
|
+
runCheck('scripts/check-protocol-consistency.js', 'check-protocol-consistency'),
|
|
148
|
+
runCheck('scripts/check-doc-drift.js', 'check-doc-drift'),
|
|
149
|
+
runCheck('scripts/check-mirror-freeze.js', 'check-mirror-freeze'),
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const state = { pkg, schema_version, skill_count, mirror_status, generated_at, checks };
|
|
153
|
+
const markdown = renderMarkdown(state);
|
|
154
|
+
|
|
155
|
+
if (opts.stdout) {
|
|
156
|
+
process.stdout.write(markdown);
|
|
157
|
+
process.exit(0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
fs.writeFileSync(OUTPUT_PATH, markdown);
|
|
161
|
+
|
|
162
|
+
const failed = checks.filter(c => c.status !== 'PASS' && c.status !== 'SKIP');
|
|
163
|
+
if (opts.check && failed.length > 0) {
|
|
164
|
+
process.stderr.write(
|
|
165
|
+
`FAIL build-status-doc: ${failed.length} check(s) not passing — see docs/status.generated.md\n`
|
|
166
|
+
);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
process.stdout.write(
|
|
171
|
+
`OK wrote ${path.relative(REPO_ROOT, OUTPUT_PATH)} (${pkg.name}@${pkg.version}, schema v${schema_version}, ${skill_count ?? '?'} skills, ${checks.length} checks)\n`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { readSchemaVersion, readSkillCount, renderMarkdown, runCheck };
|
|
176
|
+
|
|
177
|
+
if (require.main === module) main();
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Schema-version drift sentinel for documentation.
|
|
4
|
+
*
|
|
5
|
+
* Reads the active schema_version from schemas/skill.schema.json and scans
|
|
6
|
+
* active .md docs for stale references like `schema_version: 4`, `v4 today`,
|
|
7
|
+
* `equals \`5\``, and similar patterns that would teach old contracts.
|
|
8
|
+
*
|
|
9
|
+
* Allowlist:
|
|
10
|
+
* - Files under any `_archived/` segment (historical snapshots)
|
|
11
|
+
* - Files under `docs/migrations/` (intentionally cite multiple versions)
|
|
12
|
+
* - Files matching `CHANGELOG.md` at any depth (release notes cite versions)
|
|
13
|
+
* - Files under `examples/` (test fixtures and sample skills intentionally
|
|
14
|
+
* test multiple schema versions and historical exports)
|
|
15
|
+
* - Filenames containing `migration` or `compatibility` (cross-version docs)
|
|
16
|
+
*
|
|
17
|
+
* Each finding includes file:line and the offending fragment.
|
|
18
|
+
*
|
|
19
|
+
* Exit codes:
|
|
20
|
+
* 0 — no drift in active docs
|
|
21
|
+
* 1 — at least one drift hit in an active doc
|
|
22
|
+
*
|
|
23
|
+
* Flags:
|
|
24
|
+
* --json Emit findings as JSON
|
|
25
|
+
* --quiet Only print failure summary line
|
|
26
|
+
* --include-warn Report warning-class hits (e.g. raw `v4`, `v5` mentions)
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const { workspaceRoot } = require('./lib/roots');
|
|
34
|
+
|
|
35
|
+
const REPO_ROOT = workspaceRoot();
|
|
36
|
+
const SKILL_SCHEMA_PATH = path.join(REPO_ROOT, 'schemas', 'skill.schema.json');
|
|
37
|
+
const IGNORED_DIRS = new Set(['.git', 'node_modules', '.artifacts', '.roundtable', 'marketplace']);
|
|
38
|
+
|
|
39
|
+
function readActiveSchemaVersion() {
|
|
40
|
+
const schema = JSON.parse(fs.readFileSync(SKILL_SCHEMA_PATH, 'utf8'));
|
|
41
|
+
const sv = schema?.properties?.schema_version;
|
|
42
|
+
if (!sv) throw new Error(`Cannot resolve schema_version constraint in ${SKILL_SCHEMA_PATH}`);
|
|
43
|
+
// schema may be `{ const: N }` or `{ oneOf: [{ const: N }, { const: "N" }] }`
|
|
44
|
+
if (typeof sv.const === 'number') return sv.const;
|
|
45
|
+
if (Array.isArray(sv.oneOf)) {
|
|
46
|
+
for (const branch of sv.oneOf) {
|
|
47
|
+
if (typeof branch.const === 'number') return branch.const;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`Unsupported schema_version shape in ${SKILL_SCHEMA_PATH}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isAllowlisted(absPath) {
|
|
54
|
+
const rel = path.relative(REPO_ROOT, absPath).split(path.sep);
|
|
55
|
+
if (rel.some(seg => seg === '_archived')) return true;
|
|
56
|
+
if (rel[0] === 'docs' && rel[1] === 'migrations') return true;
|
|
57
|
+
if (rel[0] === 'examples') return true;
|
|
58
|
+
if (path.basename(absPath) === 'CHANGELOG.md') return true;
|
|
59
|
+
const base = path.basename(absPath).toLowerCase();
|
|
60
|
+
if (base.includes('migration') || base.includes('compatibility')) return true;
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function collectMarkdownFiles(dir, out = []) {
|
|
65
|
+
if (!fs.existsSync(dir)) return out;
|
|
66
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
67
|
+
if (entry.name.startsWith('.') && IGNORED_DIRS.has(entry.name)) continue;
|
|
68
|
+
const abs = path.join(dir, entry.name);
|
|
69
|
+
if (entry.isDirectory()) {
|
|
70
|
+
if (!IGNORED_DIRS.has(entry.name)) collectMarkdownFiles(abs, out);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) out.push(abs);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildPatterns(activeVersion) {
|
|
79
|
+
// Patterns that name an explicit non-current version.
|
|
80
|
+
const errorPatterns = [];
|
|
81
|
+
for (let v = 1; v < activeVersion; v++) {
|
|
82
|
+
errorPatterns.push({
|
|
83
|
+
kind: 'schema_version',
|
|
84
|
+
regex: new RegExp(String.raw`\bschema_version:\s*"?${v}"?\b`),
|
|
85
|
+
version: v,
|
|
86
|
+
});
|
|
87
|
+
errorPatterns.push({
|
|
88
|
+
kind: 'schema_version-equals',
|
|
89
|
+
regex: new RegExp(String.raw`equals\s*\`${v}\``),
|
|
90
|
+
version: v,
|
|
91
|
+
});
|
|
92
|
+
errorPatterns.push({
|
|
93
|
+
kind: 'schema_version-tracks-latest',
|
|
94
|
+
regex: new RegExp(String.raw`tracks\s+latest\s*\(v${v}\s+today\)`, 'i'),
|
|
95
|
+
version: v,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// Loose patterns reported only with --include-warn.
|
|
99
|
+
const warnPatterns = [];
|
|
100
|
+
for (let v = 1; v < activeVersion; v++) {
|
|
101
|
+
warnPatterns.push({
|
|
102
|
+
kind: 'bare-version-token',
|
|
103
|
+
regex: new RegExp(String.raw`\bv${v}\b(?!\s*-?to-?)`),
|
|
104
|
+
version: v,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return { errorPatterns, warnPatterns };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Heading-level allowlist: when inside a migration section (e.g. `## v4 -> v5`
|
|
111
|
+
// or `### v5 → v6 (historical)`), lines are treated as migration context and
|
|
112
|
+
// not reported. The section ends at the next heading at the same level or
|
|
113
|
+
// shallower.
|
|
114
|
+
function buildMigrationSectionMask(lines) {
|
|
115
|
+
const mask = new Array(lines.length).fill(false);
|
|
116
|
+
let inMigration = false;
|
|
117
|
+
let migrationDepth = 0;
|
|
118
|
+
const migrationHeadingRe = /^(#{2,6})\s+.*\bv\d+\s*(?:->|→|to)\s*v\d+\b/i;
|
|
119
|
+
const headingRe = /^(#{1,6})\s+/;
|
|
120
|
+
|
|
121
|
+
lines.forEach((line, idx) => {
|
|
122
|
+
const mh = line.match(migrationHeadingRe);
|
|
123
|
+
if (mh) {
|
|
124
|
+
inMigration = true;
|
|
125
|
+
migrationDepth = mh[1].length;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (inMigration) {
|
|
129
|
+
const hh = line.match(headingRe);
|
|
130
|
+
if (hh && hh[1].length <= migrationDepth) {
|
|
131
|
+
inMigration = false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
mask[idx] = inMigration;
|
|
135
|
+
});
|
|
136
|
+
return mask;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function scanFile(absPath, patterns) {
|
|
140
|
+
const text = fs.readFileSync(absPath, 'utf8');
|
|
141
|
+
const lines = text.split(/\r?\n/);
|
|
142
|
+
const migrationMask = buildMigrationSectionMask(lines);
|
|
143
|
+
const hits = [];
|
|
144
|
+
lines.forEach((line, idx) => {
|
|
145
|
+
if (migrationMask[idx]) return;
|
|
146
|
+
for (const p of patterns) {
|
|
147
|
+
if (p.regex.test(line)) {
|
|
148
|
+
hits.push({
|
|
149
|
+
file: path.relative(REPO_ROOT, absPath),
|
|
150
|
+
line: idx + 1,
|
|
151
|
+
kind: p.kind,
|
|
152
|
+
version: p.version,
|
|
153
|
+
snippet: line.trim().slice(0, 240),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
return hits;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function main() {
|
|
162
|
+
const argv = process.argv.slice(2);
|
|
163
|
+
const opts = {
|
|
164
|
+
json: argv.includes('--json'),
|
|
165
|
+
quiet: argv.includes('--quiet'),
|
|
166
|
+
includeWarn: argv.includes('--include-warn'),
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const activeVersion = readActiveSchemaVersion();
|
|
170
|
+
const { errorPatterns, warnPatterns } = buildPatterns(activeVersion);
|
|
171
|
+
|
|
172
|
+
const files = collectMarkdownFiles(REPO_ROOT)
|
|
173
|
+
.filter(f => !isAllowlisted(f))
|
|
174
|
+
.sort((a, b) => a.localeCompare(b));
|
|
175
|
+
|
|
176
|
+
const errorHits = [];
|
|
177
|
+
const warnHits = [];
|
|
178
|
+
for (const file of files) {
|
|
179
|
+
errorHits.push(...scanFile(file, errorPatterns));
|
|
180
|
+
if (opts.includeWarn) warnHits.push(...scanFile(file, warnPatterns));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (opts.json) {
|
|
184
|
+
process.stdout.write(JSON.stringify({
|
|
185
|
+
active_schema_version: activeVersion,
|
|
186
|
+
files_scanned: files.length,
|
|
187
|
+
errors: errorHits,
|
|
188
|
+
warnings: warnHits,
|
|
189
|
+
}, null, 2) + '\n');
|
|
190
|
+
process.exit(errorHits.length > 0 ? 1 : 0);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!opts.quiet) {
|
|
194
|
+
if (warnHits.length > 0) {
|
|
195
|
+
for (const h of warnHits) {
|
|
196
|
+
process.stderr.write(`WARN ${h.file}:${h.line} [${h.kind}] ${h.snippet}\n`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (const h of errorHits) {
|
|
200
|
+
process.stderr.write(`${h.file}:${h.line} [${h.kind} v${h.version}] ${h.snippet}\n`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (errorHits.length > 0) {
|
|
205
|
+
process.stderr.write(`FAIL doc drift: ${errorHits.length} stale schema-version reference(s) in active docs (active v${activeVersion}). Allowlisted: _archived/, docs/migrations/, CHANGELOG.md.\n`);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const warnNote = opts.includeWarn && warnHits.length > 0
|
|
210
|
+
? ` (${warnHits.length} warning(s) reported)`
|
|
211
|
+
: '';
|
|
212
|
+
process.stdout.write(`OK doc drift sentinel: ${files.length} active doc(s) scanned against schema v${activeVersion}${warnNote}\n`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = {
|
|
216
|
+
buildMigrationSectionMask,
|
|
217
|
+
buildPatterns,
|
|
218
|
+
collectMarkdownFiles,
|
|
219
|
+
isAllowlisted,
|
|
220
|
+
readActiveSchemaVersion,
|
|
221
|
+
scanFile,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if (require.main === module) main();
|
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
* that GitHub will resolve from Markdown files. Paths are checked with
|
|
7
7
|
* case-sensitive segment matching even on Windows so links do not pass locally
|
|
8
8
|
* and 404 after push.
|
|
9
|
+
*
|
|
10
|
+
* Severity model:
|
|
11
|
+
* - Active docs (default): broken links are errors and fail the build.
|
|
12
|
+
* - Archived docs (under any `_archived/` segment): broken links are warnings
|
|
13
|
+
* and DO NOT fail the build. Archived docs are historical snapshots; the
|
|
14
|
+
* project's link guarantees apply to the active surface only.
|
|
15
|
+
*
|
|
16
|
+
* Override with --strict-archived to treat archived broken links as errors
|
|
17
|
+
* (useful when migrating an archive batch or auditing snapshot integrity).
|
|
9
18
|
*/
|
|
10
19
|
|
|
11
20
|
'use strict';
|
|
@@ -17,6 +26,14 @@ const { workspaceRoot } = require('./lib/roots');
|
|
|
17
26
|
const REPO_ROOT = workspaceRoot();
|
|
18
27
|
const IGNORED_DIRS = new Set(['.git', 'node_modules', '.artifacts', '.roundtable']);
|
|
19
28
|
|
|
29
|
+
// Lenient policy: any markdown file beneath a `_archived/` segment is treated
|
|
30
|
+
// as a historical snapshot. Broken links inside such files become warnings
|
|
31
|
+
// rather than build-failing errors.
|
|
32
|
+
function isArchivedPath(absPath) {
|
|
33
|
+
const rel = path.relative(REPO_ROOT, absPath).split(path.sep);
|
|
34
|
+
return rel.some(segment => segment === '_archived');
|
|
35
|
+
}
|
|
36
|
+
|
|
20
37
|
function repoRelative(filePath) {
|
|
21
38
|
return path.relative(REPO_ROOT, filePath).split(path.sep).join('/');
|
|
22
39
|
}
|
|
@@ -174,18 +191,27 @@ function checkFile(filePath) {
|
|
|
174
191
|
}
|
|
175
192
|
|
|
176
193
|
function main() {
|
|
194
|
+
const flagArgs = process.argv.slice(2).filter(a => a.startsWith('--'));
|
|
195
|
+
const strictArchived = flagArgs.includes('--strict-archived');
|
|
177
196
|
const args = process.argv.slice(2).filter(a => !a.startsWith('--'));
|
|
178
197
|
const files = args.length > 0
|
|
179
198
|
? args.map(a => path.resolve(a)).filter(p => fs.existsSync(p) && p.toLowerCase().endsWith('.md'))
|
|
180
199
|
: collectMarkdownFiles(REPO_ROOT).sort((a, b) => repoRelative(a).localeCompare(repoRelative(b)));
|
|
181
200
|
|
|
182
201
|
const failures = [];
|
|
202
|
+
const warnings = [];
|
|
183
203
|
for (const file of files) {
|
|
184
|
-
|
|
185
|
-
|
|
204
|
+
const archived = !strictArchived && isArchivedPath(file);
|
|
205
|
+
for (const issue of checkFile(file)) {
|
|
206
|
+
(archived ? warnings : failures).push({ file, ...issue });
|
|
186
207
|
}
|
|
187
208
|
}
|
|
188
209
|
|
|
210
|
+
for (const warning of warnings) {
|
|
211
|
+
process.stderr.write(
|
|
212
|
+
`WARN ${repoRelative(warning.file)}:${warning.line}: ${warning.message} (${warning.target}) [archived snapshot]\n`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
189
215
|
for (const failure of failures) {
|
|
190
216
|
process.stderr.write(
|
|
191
217
|
`${repoRelative(failure.file)}:${failure.line}: ${failure.message} (${failure.target})\n`
|
|
@@ -193,11 +219,14 @@ function main() {
|
|
|
193
219
|
}
|
|
194
220
|
|
|
195
221
|
if (failures.length > 0) {
|
|
196
|
-
process.stderr.write(`FAIL markdown links: ${failures.length} broken local link(s)\n`);
|
|
222
|
+
process.stderr.write(`FAIL markdown links: ${failures.length} broken local link(s) in active docs (${warnings.length} in _archived/ ignored — use --strict-archived to elevate)\n`);
|
|
197
223
|
process.exit(1);
|
|
198
224
|
}
|
|
199
225
|
|
|
200
|
-
|
|
226
|
+
const suffix = warnings.length > 0
|
|
227
|
+
? ` — ${warnings.length} archived-doc warning(s) ignored (use --strict-archived to elevate)`
|
|
228
|
+
: '';
|
|
229
|
+
process.stdout.write(`OK markdown links (${files.length} file(s))${suffix}\n`);
|
|
201
230
|
}
|
|
202
231
|
|
|
203
232
|
module.exports = {
|
|
@@ -205,6 +234,7 @@ module.exports = {
|
|
|
205
234
|
collectMarkdownFiles,
|
|
206
235
|
existsCaseSensitive,
|
|
207
236
|
extractInlineLinks,
|
|
237
|
+
isArchivedPath,
|
|
208
238
|
slugifyHeading,
|
|
209
239
|
};
|
|
210
240
|
|