@vegastack/skills 0.4.0 → 0.6.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.
- package/README.md +4 -2
- package/dist/index.js +38 -45
- package/package.json +1 -1
- package/skill/architect/SKILL.md +68 -0
- package/skill/architect/agents/openai.yaml +4 -0
- package/skill/architect/assets/adr-template.md +21 -0
- package/skill/architect/assets/arch-template.md +20 -0
- package/skill/architect/references/advisory.md +102 -0
- package/skill/architect/references/ai-agents.md +95 -0
- package/skill/architect/references/data.md +90 -0
- package/skill/architect/references/infra.md +128 -0
- package/skill/architect/references/mobile.md +78 -0
- package/skill/architect/references/pinned-facts.md +108 -0
- package/skill/architect/references/principles.md +91 -0
- package/skill/architect/references/project-profile.md +37 -0
- package/skill/architect/references/security.md +97 -0
- package/skill/architect/references/stack.md +38 -0
- package/skill/architect/references/web.md +152 -0
- package/skill/architect/refresh/REFRESH.md +29 -0
- package/skill/architect/refresh/sources.json +244 -0
- package/skill/skill-maintainer/references/release-ops.md +11 -15
- package/skill/skill-maintainer/refresh/REFRESH.md +3 -3
- package/skill-integrity.json +20 -44
- package/skill/arch-guardian/SKILL.md +0 -84
- package/skill/arch-guardian/agents/openai.yaml +0 -4
- package/skill/arch-guardian/assets/adr-template.md +0 -25
- package/skill/arch-guardian/assets/answers-example.json +0 -10
- package/skill/arch-guardian/assets/architecture-profile.json +0 -13
- package/skill/arch-guardian/assets/architecture-profile.schema.json +0 -31
- package/skill/arch-guardian/assets/deployment-review-template.md +0 -24
- package/skill/arch-guardian/assets/service-design-template.md +0 -33
- package/skill/arch-guardian/assets/threat-model-template.md +0 -34
- package/skill/arch-guardian/references/advisory-report.md +0 -65
- package/skill/arch-guardian/references/architecture/agent-product.md +0 -22
- package/skill/arch-guardian/references/architecture/ai-cost.md +0 -24
- package/skill/arch-guardian/references/architecture/ai-data-boundaries.md +0 -21
- package/skill/arch-guardian/references/architecture/ai-evals.md +0 -28
- package/skill/arch-guardian/references/architecture/connectors-sandbox.md +0 -39
- package/skill/arch-guardian/references/architecture/data-memory.md +0 -25
- package/skill/arch-guardian/references/architecture/delivery-operations.md +0 -34
- package/skill/arch-guardian/references/architecture/durable-execution.md +0 -45
- package/skill/arch-guardian/references/architecture/flutter.md +0 -26
- package/skill/arch-guardian/references/architecture/foundation.md +0 -31
- package/skill/arch-guardian/references/architecture/hosting-reliability.md +0 -37
- package/skill/arch-guardian/references/architecture/identity-tenancy.md +0 -37
- package/skill/arch-guardian/references/architecture/model-lifecycle.md +0 -20
- package/skill/arch-guardian/references/architecture/models-observability.md +0 -23
- package/skill/arch-guardian/references/architecture/realtime-channels.md +0 -16
- package/skill/arch-guardian/references/architecture/security-privacy.md +0 -27
- package/skill/arch-guardian/references/architecture/topology-monorepo.md +0 -47
- package/skill/arch-guardian/references/architecture/web.md +0 -29
- package/skill/arch-guardian/references/foundation-compatibility.json +0 -44
- package/skill/arch-guardian/references/golden-patterns.md +0 -43
- package/skill/arch-guardian/references/profile-governance.md +0 -40
- package/skill/arch-guardian/references/rule-model.json +0 -36
- package/skill/arch-guardian/references/workflows.md +0 -48
- package/skill/arch-guardian/refresh/REFRESH.md +0 -47
- package/skill/arch-guardian/refresh/sources.json +0 -1171
- package/skill/arch-guardian/scripts/lib.mjs +0 -48
- package/skill/arch-guardian/scripts/profile-tool.mjs +0 -217
- package/skill/arch-guardian/scripts/refresh-evidence.mjs +0 -366
- package/skill/arch-guardian/scripts/schema-validate.mjs +0 -63
- package/skill/arch-guardian/scripts/validate-profile.mjs +0 -65
- package/skill/arch-guardian/scripts/verify-corpus.mjs +0 -136
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
function pointer(root, reference) {
|
|
2
|
-
if (!reference.startsWith('#/')) throw new Error(`Only local JSON Schema references are supported: ${reference}`)
|
|
3
|
-
return reference.slice(2).split('/').reduce((value, key) => value?.[key.replaceAll('~1', '/').replaceAll('~0', '~')], root)
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
const same = (left, right) => JSON.stringify(left) === JSON.stringify(right)
|
|
7
|
-
const typeOf = value => Array.isArray(value) ? 'array' : value === null ? 'null' : typeof value
|
|
8
|
-
const joinPath = (base, key) => `${base}/${String(key).replaceAll('~', '~0').replaceAll('/', '~1')}`
|
|
9
|
-
const validDate = value => {
|
|
10
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false
|
|
11
|
-
const [year, month, day] = value.split('-').map(Number)
|
|
12
|
-
const parsed = new Date(Date.UTC(year, month - 1, day))
|
|
13
|
-
return parsed.getUTCFullYear() === year && parsed.getUTCMonth() === month - 1 && parsed.getUTCDate() === day
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function validateJsonSchema(schema, value) {
|
|
17
|
-
const errors = []
|
|
18
|
-
function validate(node, data, path = '$', quiet = false) {
|
|
19
|
-
const local = []
|
|
20
|
-
const add = message => local.push(`${path}: ${message}`)
|
|
21
|
-
if (node.$ref) return validate(pointer(schema, node.$ref), data, path, quiet)
|
|
22
|
-
if (node.const !== undefined && !same(data, node.const)) add(`must equal ${JSON.stringify(node.const)}`)
|
|
23
|
-
if (node.enum && !node.enum.some(item => same(item, data))) add(`must be one of ${node.enum.map(item => JSON.stringify(item)).join(', ')}`)
|
|
24
|
-
if (node.type) {
|
|
25
|
-
const allowed = Array.isArray(node.type) ? node.type : [node.type]
|
|
26
|
-
if (!allowed.includes(typeOf(data)) || (typeOf(data) === 'number' && !Number.isFinite(data))) add(`must be ${allowed.join(' or ')}`)
|
|
27
|
-
}
|
|
28
|
-
if (typeof data === 'string') {
|
|
29
|
-
if (node.minLength !== undefined && data.length < node.minLength) add(`must have length >= ${node.minLength}`)
|
|
30
|
-
if (node.pattern && !new RegExp(node.pattern).test(data)) add(`must match ${node.pattern}`)
|
|
31
|
-
if (node.format === 'date' && !validDate(data)) add('must be a real ISO calendar date (YYYY-MM-DD)')
|
|
32
|
-
}
|
|
33
|
-
if (typeof data === 'number' && node.minimum !== undefined && data < node.minimum) add(`must be >= ${node.minimum}`)
|
|
34
|
-
if (Array.isArray(data)) {
|
|
35
|
-
if (node.minItems !== undefined && data.length < node.minItems) add(`must contain at least ${node.minItems} item(s)`)
|
|
36
|
-
if (node.uniqueItems && new Set(data.map(item => JSON.stringify(item))).size !== data.length) add('must contain unique items')
|
|
37
|
-
if (node.items) data.forEach((item, index) => local.push(...validate(node.items, item, joinPath(path, index), true)))
|
|
38
|
-
}
|
|
39
|
-
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
40
|
-
const keys = Object.keys(data)
|
|
41
|
-
if (node.minProperties !== undefined && keys.length < node.minProperties) add(`must contain at least ${node.minProperties} properties`)
|
|
42
|
-
for (const required of node.required ?? []) if (!(required in data)) local.push(`${joinPath(path, required)}: is required`)
|
|
43
|
-
for (const [key, child] of Object.entries(node.properties ?? {})) if (key in data) local.push(...validate(child, data[key], joinPath(path, key), true))
|
|
44
|
-
if (node.propertyNames) for (const key of keys) local.push(...validate(node.propertyNames, key, `${path} property ${JSON.stringify(key)}`, true))
|
|
45
|
-
const known = new Set(Object.keys(node.properties ?? {}))
|
|
46
|
-
for (const key of keys.filter(key => !known.has(key))) {
|
|
47
|
-
if (node.additionalProperties === false) local.push(`${joinPath(path, key)}: additional property is not allowed`)
|
|
48
|
-
else if (node.additionalProperties && typeof node.additionalProperties === 'object') local.push(...validate(node.additionalProperties, data[key], joinPath(path, key), true))
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
for (const child of node.allOf ?? []) local.push(...validate(child, data, path, true))
|
|
52
|
-
if (node.oneOf) {
|
|
53
|
-
const matches = node.oneOf.map(child => validate(child, data, path, true)).filter(result => result.length === 0).length
|
|
54
|
-
if (matches !== 1) add(`must match exactly one schema branch (matched ${matches})`)
|
|
55
|
-
}
|
|
56
|
-
if (node.not && validate(node.not, data, path, true).length === 0) add('must not match forbidden schema')
|
|
57
|
-
if (node.if && validate(node.if, data, path, true).length === 0 && node.then) local.push(...validate(node.then, data, path, true))
|
|
58
|
-
if (!quiet) errors.push(...local)
|
|
59
|
-
return local
|
|
60
|
-
}
|
|
61
|
-
validate(schema, value)
|
|
62
|
-
return errors
|
|
63
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { readFile } from 'node:fs/promises'
|
|
3
|
-
import { dirname, join, resolve } from 'node:path'
|
|
4
|
-
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
5
|
-
import { listFiles, readJsonYaml, resolveProfile } from './lib.mjs'
|
|
6
|
-
import { validateJsonSchema } from './schema-validate.mjs'
|
|
7
|
-
|
|
8
|
-
const skillRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
9
|
-
const architectureRoot = join(skillRoot, 'references', 'architecture')
|
|
10
|
-
const schemaPath = join(skillRoot, 'assets', 'architecture-profile.schema.json')
|
|
11
|
-
|
|
12
|
-
export async function canonicalRuleIds() {
|
|
13
|
-
const ids = new Set()
|
|
14
|
-
const duplicates = new Set()
|
|
15
|
-
for (const path of await listFiles(architectureRoot, file => file.endsWith('.md'))) {
|
|
16
|
-
const body = await readFile(path, 'utf8')
|
|
17
|
-
for (const match of body.matchAll(/\*\*([A-Z]+-[0-9]{3}) —/g)) {
|
|
18
|
-
if (ids.has(match[1])) duplicates.add(match[1])
|
|
19
|
-
ids.add(match[1])
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
if (duplicates.size) throw new Error(`Duplicate canonical rule IDs: ${[...duplicates].join(', ')}`)
|
|
23
|
-
return ids
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Collect every REQUIRED-* placeholder still present so a draft cannot be mistaken for
|
|
27
|
-
// confirmed facts.
|
|
28
|
-
function unconfirmedValues(value, path = '') {
|
|
29
|
-
if (typeof value === 'string') return value.startsWith('REQUIRED-') ? [path || '(root)'] : []
|
|
30
|
-
if (Array.isArray(value)) return value.flatMap((item, index) => unconfirmedValues(item, `${path}[${index}]`))
|
|
31
|
-
if (value && typeof value === 'object') return Object.entries(value).flatMap(([key, item]) => unconfirmedValues(item, path ? `${path}.${key}` : key))
|
|
32
|
-
return []
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function validateProfile(path) {
|
|
36
|
-
let profile
|
|
37
|
-
try { profile = await readJsonYaml(path) } catch (error) {
|
|
38
|
-
return { profile: null, errors: [error.message] }
|
|
39
|
-
}
|
|
40
|
-
if (profile.schemaVersion === 2 || profile.schemaVersion === 3) {
|
|
41
|
-
return { profile, errors: [`schemaVersion ${profile.schemaVersion} is obsolete; run profile-tool.mjs migrate <profile> for a deterministic read-only v4 draft, then confirm project facts (v2 needs a fresh v4 profile from answers)`] }
|
|
42
|
-
}
|
|
43
|
-
const errors = []
|
|
44
|
-
const schema = JSON.parse(await readFile(schemaPath, 'utf8'))
|
|
45
|
-
for (const message of validateJsonSchema(schema, profile)) errors.push(message)
|
|
46
|
-
for (const location of unconfirmedValues(profile)) errors.push(`${location}: replace the REQUIRED placeholder with a confirmed fact`)
|
|
47
|
-
return { profile, errors }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
51
|
-
const argument = process.argv.slice(2).find(value => !value.startsWith('-'))
|
|
52
|
-
let path = argument ? resolve(argument) : null
|
|
53
|
-
if (!path) {
|
|
54
|
-
const discovered = await resolveProfile(process.cwd())
|
|
55
|
-
if (!discovered) { console.error('error: no .vegastack/architecture.json (or legacy .yaml) profile found; pass a path explicitly'); process.exit(2) }
|
|
56
|
-
if (discovered.legacy) console.error(`deprecation: ${discovered.relative} uses the legacy .yaml name for a JSON document; rename to .vegastack/architecture.json`)
|
|
57
|
-
path = discovered.path
|
|
58
|
-
}
|
|
59
|
-
const json = process.argv.includes('--json')
|
|
60
|
-
const result = await validateProfile(path)
|
|
61
|
-
if (json) console.log(JSON.stringify({ path, valid: result.errors.length === 0, errors: result.errors }, null, 2))
|
|
62
|
-
else if (result.errors.length) for (const error of result.errors) console.error(`invalid profile: ${error}`)
|
|
63
|
-
else console.log(`validate-profile: valid ${path}`)
|
|
64
|
-
if (result.errors.length) process.exitCode = 1
|
|
65
|
-
}
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { readFile } from 'node:fs/promises'
|
|
3
|
-
import { basename, dirname, join, relative, resolve } from 'node:path'
|
|
4
|
-
import { fileURLToPath } from 'node:url'
|
|
5
|
-
import { listFiles, pathExists, readJsonYaml } from './lib.mjs'
|
|
6
|
-
import { canonicalRuleIds } from './validate-profile.mjs'
|
|
7
|
-
|
|
8
|
-
let mermaid = null
|
|
9
|
-
try {
|
|
10
|
-
const { JSDOM } = await import('jsdom')
|
|
11
|
-
const dom = new JSDOM('<!doctype html><html><body></body></html>')
|
|
12
|
-
globalThis.window = dom.window
|
|
13
|
-
globalThis.document = dom.window.document
|
|
14
|
-
mermaid = (await import('mermaid')).default
|
|
15
|
-
mermaid.initialize({ startOnLoad: false, securityLevel: 'strict' })
|
|
16
|
-
} catch {
|
|
17
|
-
// Installed copies have no development dependencies and use structural checks.
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const skillRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
21
|
-
const references = join(skillRoot, 'references')
|
|
22
|
-
const architectureRoot = join(references, 'architecture')
|
|
23
|
-
const skillPath = join(skillRoot, 'SKILL.md')
|
|
24
|
-
const skillBody = await readFile(skillPath, 'utf8')
|
|
25
|
-
const chapters = await listFiles(architectureRoot, path => path.endsWith('.md'))
|
|
26
|
-
const registry = await readJsonYaml(join(skillRoot, 'refresh', 'sources.json'))
|
|
27
|
-
const sourceIds = new Set(registry.sources.map(source => source.id))
|
|
28
|
-
const ruleIds = await canonicalRuleIds()
|
|
29
|
-
const ruleModel = await readJsonYaml(join(references, 'rule-model.json'))
|
|
30
|
-
const errors = []
|
|
31
|
-
let diagrams = 0
|
|
32
|
-
let normativeLines = 0
|
|
33
|
-
|
|
34
|
-
const expectedReferences = new Set(['agent-product.md', 'ai-cost.md', 'ai-data-boundaries.md', 'ai-evals.md', 'connectors-sandbox.md', 'data-memory.md', 'delivery-operations.md', 'durable-execution.md', 'flutter.md', 'foundation.md', 'hosting-reliability.md', 'identity-tenancy.md', 'model-lifecycle.md', 'models-observability.md', 'realtime-channels.md', 'security-privacy.md', 'topology-monorepo.md', 'web.md'])
|
|
35
|
-
if (chapters.length !== expectedReferences.size) errors.push(`expected ${expectedReferences.size} normative references, found ${chapters.length}`)
|
|
36
|
-
for (const path of chapters) if (!expectedReferences.delete(basename(path))) errors.push(`unexpected architecture reference ${basename(path)}`)
|
|
37
|
-
for (const missing of expectedReferences) errors.push(`missing architecture reference ${missing}`)
|
|
38
|
-
|
|
39
|
-
const skillLines = skillBody.split('\n').length
|
|
40
|
-
if (skillLines > 120) errors.push(`SKILL.md exceeds 120 lines (${skillLines})`)
|
|
41
|
-
const routed = new Set([...skillBody.matchAll(/\(references\/architecture\/([^)]+\.md)\)/g)].map(match => match[1]))
|
|
42
|
-
|
|
43
|
-
const citedSources = new Set()
|
|
44
|
-
for (const path of chapters) {
|
|
45
|
-
const body = await readFile(path, 'utf8')
|
|
46
|
-
normativeLines += body.split('\n').length
|
|
47
|
-
if (!routed.has(basename(path))) errors.push(`${path}: not routed directly from SKILL.md`)
|
|
48
|
-
for (const match of body.matchAll(/\[([A-Z0-9]+(?:-[A-Z0-9]+)*)\]|<!-- source: ([A-Z0-9]+(?:-[A-Z0-9]+)*) -->/g)) {
|
|
49
|
-
const id = match[1] ?? match[2]
|
|
50
|
-
citedSources.add(id)
|
|
51
|
-
if (!sourceIds.has(id)) errors.push(`${path}: unknown source ${id}`)
|
|
52
|
-
}
|
|
53
|
-
for (const [index, line] of body.split('\n').entries()) {
|
|
54
|
-
const definitions = [...line.matchAll(/\*\*([A-Z]+-[0-9]{3}) —/g)]
|
|
55
|
-
if (definitions.length && !/\bMUST(?:\s+NOT)?\b/.test(line)) errors.push(`${path}: rule ${definitions[0][1]} must define a MUST or MUST NOT invariant in its canonical line`)
|
|
56
|
-
if (/\bMUST(?:\s+NOT)?\b/.test(line) && definitions.length !== 1) errors.push(`${path}:${index + 1}: every MUST/MUST NOT must occur in exactly one canonical rule definition`)
|
|
57
|
-
}
|
|
58
|
-
for (const block of body.matchAll(/```mermaid\n([\s\S]*?)```/g)) {
|
|
59
|
-
diagrams += 1
|
|
60
|
-
const text = block[1].trim()
|
|
61
|
-
if (!/^(flowchart|sequenceDiagram|stateDiagram-v2)\b/.test(text)) errors.push(`${path}: unsupported Mermaid declaration`)
|
|
62
|
-
if ((text.match(/\[/g) ?? []).length !== (text.match(/\]/g) ?? []).length) errors.push(`${path}: unbalanced Mermaid brackets`)
|
|
63
|
-
if (mermaid) try { await mermaid.parse(text) } catch (error) { errors.push(`${path}: Mermaid parser: ${error.message}`) }
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (normativeLines > 800) errors.push(`normative references exceed 800 lines (${normativeLines})`)
|
|
67
|
-
if (diagrams < 4) errors.push(`expected at least four decision-useful Mermaid diagrams, found ${diagrams}`)
|
|
68
|
-
|
|
69
|
-
// criticalSources in foundation-compatibility must mirror every critical:true registry entry —
|
|
70
|
-
// asserted here so the mirror can never drift silently.
|
|
71
|
-
const compatibility = await readJsonYaml(join(references, 'foundation-compatibility.json'))
|
|
72
|
-
const declaredCritical = new Set(compatibility.sourceDrift?.criticalSources ?? [])
|
|
73
|
-
const actualCritical = new Set(registry.sources.filter(source => source.critical).map(source => source.id))
|
|
74
|
-
for (const id of actualCritical) if (!declaredCritical.has(id)) errors.push(`criticalSources mirror missing critical registry source ${id}`)
|
|
75
|
-
for (const id of declaredCritical) if (!actualCritical.has(id)) errors.push(`criticalSources mirror lists non-critical or unknown source ${id}`)
|
|
76
|
-
|
|
77
|
-
for (const source of registry.sources) {
|
|
78
|
-
if (!citedSources.has(source.id)) errors.push(`orphan source registry entry ${source.id}`)
|
|
79
|
-
if (!source.urls?.primary) errors.push(`source ${source.id} has no primary URL`)
|
|
80
|
-
for (const target of source.affected ?? []) {
|
|
81
|
-
const [kind, value] = String(target).split(':', 2)
|
|
82
|
-
if (!['rule', 'ref', 'profile'].includes(kind)) errors.push(`source ${source.id} has invalid affected mapping ${target}`)
|
|
83
|
-
if (kind === 'rule' && !ruleIds.has(value)) errors.push(`source ${source.id} maps unknown rule ${value}`)
|
|
84
|
-
if (kind === 'ref' && !await pathExists(join(architectureRoot, `${value}.md`))) errors.push(`source ${source.id} maps unknown reference ${value}`)
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const modeledRules = new Set()
|
|
89
|
-
for (const group of ruleModel.groups ?? []) {
|
|
90
|
-
for (const required of ['activation', 'verification', 'rationale']) if (!group[required]) errors.push(`rule model group missing ${required}`)
|
|
91
|
-
for (const rule of group.rules ?? []) {
|
|
92
|
-
if (modeledRules.has(rule)) errors.push(`rule model duplicates ${rule}`)
|
|
93
|
-
modeledRules.add(rule)
|
|
94
|
-
if (!ruleIds.has(rule)) errors.push(`rule model names unknown rule ${rule}`)
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
for (const rule of ruleIds) if (!modeledRules.has(rule)) errors.push(`canonical rule ${rule} has no activation/verification/rationale model`)
|
|
98
|
-
for (const override of ruleModel.overrides ?? []) if (!modeledRules.has(override.rule)) errors.push(`rule model override names unknown rule ${override.rule}`)
|
|
99
|
-
|
|
100
|
-
const bannedNames = ['golden-architecture.md', 'evidence-manifest.json', 'coverage-matrix.md', 'verification-ledger.md', '23-evidence-comparisons.md', '24-roadmap.md', 'compile-guide.mjs', 'architecture-check.mjs', 'control-catalog.json']
|
|
101
|
-
const allFiles = await listFiles(skillRoot)
|
|
102
|
-
for (const path of allFiles) if (bannedNames.includes(basename(path))) errors.push(`forbidden runtime artifact remains: ${relative(skillRoot, path)}`)
|
|
103
|
-
const runtimeText = (await Promise.all((await listFiles(skillRoot, path => /\.(?:md|json|yaml|yml|mjs)$/.test(path) && !path.includes('/tests/'))).map(path => readFile(path, 'utf8')))).join('\n')
|
|
104
|
-
const historicalNames = ['C' + 'RM', 'S' + 'IM', 'Fl' + 'ue']
|
|
105
|
-
if (new RegExp(`\\b(?:${historicalNames.join('|')})\\b`).test(runtimeText)) errors.push('historical comparison names remain in runtime skill')
|
|
106
|
-
|
|
107
|
-
const githubSlug = heading => heading.toLowerCase().trim().replace(/<[^>]+>/g, '').replace(/[^\p{L}\p{N}\s-]/gu, '').replace(/\s+/g, '-')
|
|
108
|
-
const headingsFor = body => new Set([...body.matchAll(/^#{1,6}\s+(.+)$/gm)].map(match => githubSlug(match[1])))
|
|
109
|
-
for (const path of await listFiles(skillRoot, path => path.endsWith('.md'))) {
|
|
110
|
-
const body = await readFile(path, 'utf8')
|
|
111
|
-
for (const match of body.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)) {
|
|
112
|
-
const target = match[1]
|
|
113
|
-
if (/^(https?:|mailto:)/.test(target)) continue
|
|
114
|
-
const [filePart, anchor] = target.split('#')
|
|
115
|
-
const targetPath = filePart ? resolve(dirname(path), decodeURIComponent(filePart)) : path
|
|
116
|
-
if (!await pathExists(targetPath)) { errors.push(`${path}: broken link ${target}`); continue }
|
|
117
|
-
if (anchor && !headingsFor(await readFile(targetPath, 'utf8')).has(anchor)) errors.push(`${path}: broken anchor ${target}`)
|
|
118
|
-
}
|
|
119
|
-
const lines = body.split('\n')
|
|
120
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
121
|
-
if (!lines[index].startsWith('|')) continue
|
|
122
|
-
const expected = (lines[index].match(/\|/g) ?? []).length
|
|
123
|
-
let cursor = index + 1
|
|
124
|
-
while (cursor < lines.length && lines[cursor].startsWith('|')) {
|
|
125
|
-
const actual = (lines[cursor].match(/\|/g) ?? []).length
|
|
126
|
-
if (actual !== expected) errors.push(`${path}:${cursor + 1}: malformed Markdown table`)
|
|
127
|
-
cursor += 1
|
|
128
|
-
}
|
|
129
|
-
index = cursor - 1
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (errors.length) {
|
|
134
|
-
for (const error of errors) console.error(error)
|
|
135
|
-
process.exitCode = 1
|
|
136
|
-
} else console.log(`verify-corpus: ${chapters.length} normative references, ${normativeLines} lines, ${ruleIds.size} rules, ${diagrams} Mermaid diagrams, ${sourceIds.size} cited sources, Mermaid mode=${mermaid ? 'formal' : 'structural-fallback'}`)
|