@vegastack/skills 0.2.0 → 0.3.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 (40) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +9 -9
  3. package/package.json +1 -1
  4. package/skill/arch-guardian/SKILL.md +27 -39
  5. package/skill/arch-guardian/agents/openai.yaml +2 -2
  6. package/skill/arch-guardian/assets/adr-template.md +10 -25
  7. package/skill/arch-guardian/assets/answers-example.json +8 -18
  8. package/skill/arch-guardian/assets/architecture-profile.json +10 -22
  9. package/skill/arch-guardian/assets/architecture-profile.schema.json +13 -195
  10. package/skill/arch-guardian/references/advisory-report.md +65 -0
  11. package/skill/arch-guardian/references/architecture/ai-cost.md +2 -0
  12. package/skill/arch-guardian/references/architecture/ai-data-boundaries.md +2 -0
  13. package/skill/arch-guardian/references/architecture/ai-evals.md +2 -0
  14. package/skill/arch-guardian/references/architecture/connectors-sandbox.md +2 -2
  15. package/skill/arch-guardian/references/architecture/delivery-operations.md +5 -5
  16. package/skill/arch-guardian/references/architecture/durable-execution.md +3 -1
  17. package/skill/arch-guardian/references/architecture/flutter.md +1 -1
  18. package/skill/arch-guardian/references/architecture/foundation.md +17 -19
  19. package/skill/arch-guardian/references/architecture/hosting-reliability.md +1 -1
  20. package/skill/arch-guardian/references/architecture/identity-tenancy.md +7 -7
  21. package/skill/arch-guardian/references/architecture/model-lifecycle.md +5 -3
  22. package/skill/arch-guardian/references/architecture/models-observability.md +1 -1
  23. package/skill/arch-guardian/references/architecture/security-privacy.md +9 -5
  24. package/skill/arch-guardian/references/architecture/topology-monorepo.md +1 -1
  25. package/skill/arch-guardian/references/architecture/web.md +1 -1
  26. package/skill/arch-guardian/references/foundation-compatibility.json +1 -1
  27. package/skill/arch-guardian/references/profile-governance.md +25 -39
  28. package/skill/arch-guardian/references/rule-model.json +2 -2
  29. package/skill/arch-guardian/references/workflows.md +15 -12
  30. package/skill/arch-guardian/refresh/REFRESH.md +9 -2
  31. package/skill/arch-guardian/refresh/sources.json +26 -14
  32. package/skill/arch-guardian/scripts/lib.mjs +1 -10
  33. package/skill/arch-guardian/scripts/profile-tool.mjs +43 -49
  34. package/skill/arch-guardian/scripts/refresh-evidence.mjs +43 -5
  35. package/skill/arch-guardian/scripts/validate-profile.mjs +20 -196
  36. package/skill/arch-guardian/scripts/verify-corpus.mjs +1 -13
  37. package/skill/skill-maintainer/refresh/REFRESH.md +1 -1
  38. package/skill-integrity.json +34 -35
  39. package/skill/arch-guardian/references/control-catalog.json +0 -55
  40. package/skill/arch-guardian/scripts/architecture-check.mjs +0 -323
@@ -1,323 +0,0 @@
1
- #!/usr/bin/env node
2
- import { readFile, readdir } from 'node:fs/promises'
3
- import { basename, dirname, extname, relative as relativePath, resolve } from 'node:path'
4
- import { fileURLToPath, pathToFileURL } from 'node:url'
5
- import { issue, pathExists, resolveProfile } from './lib.mjs'
6
- import { canonicalRuleIds, validateProfile } from './validate-profile.mjs'
7
-
8
- const scriptDirectory = dirname(fileURLToPath(import.meta.url))
9
- const scanExtensions = new Set(['.ts', '.tsx', '.js', '.mjs', '.cjs', '.json', '.yaml', '.yml', '.toml', '.sql', '.md', '.py', '.go', '.java', '.kt', '.dart', '.rs', '.php', '.rb'])
10
- const scanNames = new Set(['.env', '.env.local', '.env.production', 'Dockerfile'])
11
- const ignored = new Set(['node_modules', '.git', '.turbo', 'dist', 'build', '.next', '.agents', '.claude', '.vegastack', 'coverage', 'vendor', 'out', '.output', '.vercel', '.wrangler'])
12
- // Files this large or with lines this long are treated as generated bundles, not reviewable evidence.
13
- const bundleBodyBytes = 500_000
14
- const bundleLineLength = 5_000
15
- const findingsCapPerControl = 25
16
- const capabilityPatterns = {
17
- webControlPlane: /(?:from\s+['"]next(?:\/|['"])|next\.config|@opennextjs\/cloudflare)/i,
18
- flutter: /(?:pubspec\.yaml|package:flutter|flutter_secure_storage)/i,
19
- agents: /(?:from\s+['"]eve['"]|@workflow\/world|AgentRun)/i,
20
- jobs: /(?:from\s+['"]pg-boss['"]|\bPgBoss\b)/i,
21
- sandbox: /(?:@cloudflare\/sandbox|enableInternet|block_network|SandboxProvider)/i,
22
- connectors: /(?:\bMCP\b|webhook|connector|refresh_token)/i,
23
- knowledge: /(?:pgvector|vector\(|\bknowledge\b|embedding)/i,
24
- modelRouting: /(?:generateText|streamText|AI Gateway|BYOK|model provider)/i,
25
- enterpriseIdentity: /(?:\bSCIM\b|\bSSO\b|saml)/i,
26
- realtime: /(?:EventSource|text\/event-stream|WebSocket)/i,
27
- notifications: /(?:firebase_messaging|\bAPNs\b|notification intent)/i,
28
- secrets: /(?:OPENBAO|VAULT_ADDR|CLIENT_SECRET|API_KEY|DATABASE_URL)/i
29
- }
30
-
31
- async function loadGuardianIgnore(root) {
32
- const path = resolve(root, '.guardianignore')
33
- if (!await pathExists(path)) return []
34
- return (await readFile(path, 'utf8'))
35
- .split('\n')
36
- .map(line => line.trim())
37
- .filter(line => line && !line.startsWith('#'))
38
- .map(line => line.replace(/\/+$/, ''))
39
- }
40
-
41
- // Traversal skips (rather than aborts on) symlinks, agent-skill trees, and .guardianignore
42
- // prefixes; the caller reports skips as a single NOT_VERIFIED finding.
43
- async function sourceFiles(root, ignorePrefixes, skipped) {
44
- const files = []
45
- async function walk(directory) {
46
- const entries = await readdir(directory, { withFileTypes: true })
47
- if (directory !== root && entries.some(entry => entry.name === 'SKILL.md')) {
48
- skipped.skillTrees.push(relativePath(root, directory).split('\\').join('/'))
49
- return
50
- }
51
- for (const entry of entries) {
52
- if (ignored.has(entry.name)) continue
53
- const path = resolve(directory, entry.name)
54
- const relative = relativePath(root, path).split('\\').join('/')
55
- if (ignorePrefixes.some(prefix => relative === prefix || relative.startsWith(`${prefix}/`))) continue
56
- if (entry.isSymbolicLink()) { skipped.symlinks.push(relative); continue }
57
- if (entry.isDirectory()) await walk(path)
58
- else if (entry.isFile() && (scanExtensions.has(extname(path)) || scanNames.has(basename(path)))) files.push(path)
59
- }
60
- }
61
- await walk(root)
62
- return files.sort()
63
- }
64
-
65
- function looksGenerated(body) {
66
- if (body.length > bundleBodyBytes) return true
67
- let lineStart = 0
68
- for (let index = 0; index <= body.length; index += 1) {
69
- if (index === body.length || body[index] === '\n') {
70
- if (index - lineStart > bundleLineLength) return true
71
- lineStart = index + 1
72
- }
73
- }
74
- return false
75
- }
76
-
77
- const lineOf = (body, index) => body.slice(0, Math.max(0, index)).split('\n').length
78
- const evidence = (path, body, index = 0, type = 'static-sentinel') => ({ path, line: lineOf(body, index), type })
79
- const enabled = (profile, name) => profile?.capabilities?.[name]?.status === 'enabled'
80
- const owned = (profile, name) => enabled(profile, name) && profile.capabilities[name].ownership === 'owned'
81
- const roots = (profile, name) => profile?.capabilities?.[name]?.sourceRoots ?? []
82
- const inRoots = (path, selected = []) => selected.some(root => path === root || path.startsWith(`${root.replace(/\/$/, '')}/`))
83
- const moduleSpecifiers = body => [...body.matchAll(/(?:from\s+|require\(\s*|import\(\s*)['"]([^'"]+)['"]/g)].map(match => match[1])
84
- const nonProductionPath = path => path.split('/').some(part => ['test', 'tests', '__tests__', 'fixture', 'fixtures', 'example', 'examples', 'docs'].includes(part.toLowerCase()))
85
- const normalizedSqlName = name => name.replaceAll('"', '').toLowerCase()
86
-
87
- function tenantTables(body) {
88
- return [...body.matchAll(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([\w."-]+)\s*\(([\s\S]*?)\);/gi)]
89
- .filter(match => /\bworkspace_id\b/i.test(match[2]))
90
- .map(match => ({ name: match[1].replaceAll('"', ''), definition: match[2], index: match.index ?? 0 }))
91
- }
92
-
93
- function tablePolicies(sqlFiles) {
94
- const policies = new Map()
95
- for (const file of sqlFiles) for (const match of file.body.matchAll(/CREATE\s+POLICY\b[\s\S]*?;/gi)) {
96
- const statement = match[0]
97
- const table = statement.match(/\bON\s+(?:ONLY\s+)?([\w."-]+)/i)?.[1]
98
- if (!table) continue
99
- const key = normalizedSqlName(table)
100
- if (!policies.has(key)) policies.set(key, [])
101
- policies.get(key).push({ statement, path: file.path, body: file.body, index: match.index ?? 0 })
102
- }
103
- return policies
104
- }
105
-
106
- const tenantClause = (statement, clause) => new RegExp(`\\b${clause}\\s*\\([\\s\\S]*?(?:workspace_id|current_setting)`, 'i').test(statement) && !new RegExp(`\\b${clause}\\s*\\(\\s*true\\s*\\)`, 'i').test(statement)
107
-
108
- function rawFinding(findings, rule, control, message, path, body = '', index = 0, extra = {}) {
109
- findings.push(issue('FAIL', rule, control, message, evidence(path, body, index), extra))
110
- }
111
-
112
- function applyExceptions(findings, exceptionStates, profilePath) {
113
- const matched = new Set()
114
- const output = findings.map(finding => {
115
- const exceptionType = finding.status === 'FAIL' ? 'static-sentinel' : finding.status === 'NOT_VERIFIED' ? 'manual-qualification' : null
116
- if (!exceptionType || finding.exceptionEligible === false) return finding
117
- // An exception without a controls list covers every control under its single rule; paths stay exact.
118
- const state = exceptionStates.find(item => item.valid && item.exception.verificationType === exceptionType && item.exception.ruleId === finding.rule && (!item.exception.controls || item.exception.controls.includes(finding.control)) && item.exception.paths.includes(finding.path))
119
- if (!state) return finding
120
- matched.add(state.exception.id)
121
- return { ...finding, status: 'EXCEPTED', severity: 'accepted-risk', exceptionId: state.exception.id, acceptedBy: state.exception.projectOwner, message: `${finding.message} — accepted project risk; foundation recommendation remains unmet` }
122
- })
123
- for (const state of exceptionStates.filter(item => item.valid && !matched.has(item.exception.id))) output.push(issue('FAIL', 'FOUND-002', 'exception.scope-match', `${state.exception.id} did not match an eligible emitted finding for its rule/path/verification scope (controls, when listed, must also match exactly)`, { path: profilePath, line: 1, type: 'declared-control' }, { verificationType: 'semantic', exceptionEligible: false }))
124
- return output
125
- }
126
-
127
- function capFindings(findings) {
128
- const groups = new Map()
129
- const output = []
130
- for (const finding of findings) {
131
- if (finding.status !== 'FAIL') { output.push(finding); continue }
132
- const key = `${finding.rule}/${finding.control}`
133
- const group = groups.get(key) ?? { kept: 0, suppressed: 0, last: null }
134
- groups.set(key, group)
135
- if (group.kept < findingsCapPerControl) {
136
- group.kept += 1
137
- group.last = finding
138
- output.push(finding)
139
- } else {
140
- group.suppressed += 1
141
- }
142
- }
143
- for (const group of groups.values()) if (group.suppressed > 0 && group.last) group.last.suppressedCount = group.suppressed
144
- return output
145
- }
146
-
147
- export async function checkArchitecture(projectRoot, options = {}) {
148
- const root = resolve(projectRoot)
149
- const findings = []
150
- const discovered = options.profile ? null : await resolveProfile(root)
151
- const profileRelative = options.profile ?? discovered?.relative ?? '.vegastack/architecture.json'
152
- const profilePath = resolve(root, profileRelative)
153
- const legacyProfileName = options.profile ? profileRelative.endsWith('.yaml') : Boolean(discovered?.legacy)
154
- let profile = null
155
- let exceptionStates = []
156
- if (!await pathExists(profilePath)) rawFinding(findings, 'FOUND-001', 'profile.present', 'Commit a confirmed v3 architecture profile for CI; read-only review continued without mutation', profileRelative)
157
- else {
158
- const validation = await validateProfile(profilePath, { projectRoot: root, now: options.now })
159
- profile = validation.profile
160
- exceptionStates = validation.exceptions
161
- for (const diagnostic of validation.diagnostics) rawFinding(findings, diagnostic.rule, diagnostic.control, diagnostic.message, diagnostic.path, '', 0, { exceptionEligible: diagnostic.exceptionEligible })
162
- }
163
-
164
- const skipped = { symlinks: [], skillTrees: [], generated: [] }
165
- let files = []
166
- try { files = await sourceFiles(root, await loadGuardianIgnore(root), skipped) } catch (error) {
167
- rawFinding(findings, 'DEL-001', 'inspection.traversal', `Cannot complete repository traversal: ${error.message}`, '.')
168
- }
169
- const content = []
170
- const observed = Object.fromEntries(Object.keys(capabilityPatterns).map(name => [name, []]))
171
- for (const path of files) {
172
- if (resolve(path) === profilePath) continue
173
- const relative = relativePath(root, path).split('\\').join('/')
174
- let body
175
- try { body = await readFile(path, 'utf8') } catch (error) { rawFinding(findings, 'DEL-001', 'inspection.read', `Cannot inspect file: ${error.message}`, relative); continue }
176
- if (looksGenerated(body)) { skipped.generated.push(relative); continue }
177
- content.push({ path: relative, body, extension: extname(path) })
178
- const searchable = `${relative}\n${body}`
179
- const machineSource = extname(path) !== '.md'
180
- const productionEvidence = machineSource && !nonProductionPath(relative)
181
- if (productionEvidence) for (const [name, pattern] of Object.entries(capabilityPatterns)) if (pattern.test(searchable)) observed[name].push(relative)
182
-
183
- const localSecretAllowance = relative === '.env.local' && profile?.environments?.localDevelopment?.allowances?.includes('local-secrets')
184
- if (productionEvidence && !localSecretAllowance) {
185
- for (const match of body.matchAll(/^\s*(?:export\s+)?(?:const\s+)?([A-Z][A-Z0-9_]*(?:SECRET(?:_KEY)?|PASSWORD|PRIVATE_KEY|TOKEN|API_KEY)|API_KEY|CLIENT_SECRET|REFRESH_TOKEN|OPENBAO_TOKEN|DATABASE_URL|BYOK)\s*[:=]\s*(?!process\.env|env\.|os\.environ|System\.getenv|\$\{|['"]?REDACTED)([^\s,;]{8,}|['"][^'"$]{8,}['"])/gim)) rawFinding(findings, 'SEC-002', 'secret.plaintext', 'Probable plaintext credential', relative, body, match.index)
186
- for (const match of body.matchAll(/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g)) rawFinding(findings, 'SEC-002', 'secret.plaintext', 'Probable plaintext private key', relative, body, match.index)
187
- for (const match of body.matchAll(/authorization\s*:\s*['"]Bearer\s+[^$'"]{8,}/gi)) rawFinding(findings, 'SEC-002', 'secret.bearer', 'Hard-coded bearer credential', relative, body, match.index)
188
- for (const match of body.matchAll(/(console\.(?:log|info|warn|error)|logger\.(?:debug|info|warn|error))\s*\([^\n]*(?:TOKEN|SECRET|PASSWORD|DATABASE_URL|API_KEY|AUTHORIZATION|COOKIE|PROMPT|RESTRICTED)/gi)) rawFinding(findings, 'OBS-002', 'telemetry.sensitive-data', 'Probable credential or restricted content emitted to telemetry', relative, body, match.index)
189
- }
190
-
191
- const sandboxApplicable = productionEvidence && (enabled(profile, 'sandbox') || observed.sandbox.length > 0 || capabilityPatterns.sandbox.test(searchable))
192
- if (sandboxApplicable) for (const match of body.matchAll(/enableInternet\s*[:=]\s*true|block_network\s*[:=]\s*false|allowedHosts\s*[:=]\s*\[\s*['"]\*['"]/g)) rawFinding(findings, 'SBX-003', 'sandbox.egress', 'Production sandbox egress must default deny without wildcards', relative, body, match.index)
193
-
194
- const tenancyApplicable = productionEvidence && (profile?.project?.tenancy === 'multi-tenant-shared-schema' || /app\.workspace_id|workspace_id/.test(body))
195
- if (tenancyApplicable) {
196
- for (const match of body.matchAll(/\bSET\s+(?!LOCAL\b)(?:SESSION\s+)?app\.workspace_id\b/gi)) rawFinding(findings, 'TEN-003', 'tenancy.context-local', 'Tenant context must use SET LOCAL in the protected transaction', relative, body, match.index)
197
- for (const match of body.matchAll(/\bBYPASSRLS\b/gi)) rawFinding(findings, 'TEN-003', 'tenancy.role-bypassrls', 'Application/request roles must not receive BYPASSRLS', relative, body, match.index)
198
- }
199
- if (productionEvidence && (enabled(profile, 'agents') || observed.agents.length)) for (const match of body.matchAll(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[\w."-]*(?:workflow_steps|workflow_runs|agent_approvals|continuations)\b/gi)) rawFinding(findings, 'DUR-001', 'agents.dual-workflow-state', 'Do not create a second workflow, tool-loop, HITL, or continuation store', relative, body, match.index)
200
-
201
- const specifiers = moduleSpecifiers(body)
202
- if (productionEvidence && owned(profile, 'webControlPlane') && inRoots(relative, roots(profile, 'webControlPlane'))) for (const specifier of specifiers) if (specifier === 'eve' || specifier === 'pg-boss' || specifier.startsWith('@workflow/')) rawFinding(findings, specifier === 'pg-boss' ? 'RUN-002' : 'RUN-001', 'runtime.web-bundle-boundary', `Web source root must not depend on long-running runtime package ${specifier}`, relative, body, body.indexOf(specifier))
203
- if (productionEvidence && owned(profile, 'agents') && inRoots(relative, roots(profile, 'agents')) && specifiers.includes('pg-boss')) rawFinding(findings, 'DUR-003', 'agents.pg-boss-ownership', 'EVE runtime must not own pg-boss orchestration', relative, body, body.indexOf('pg-boss'))
204
- if (productionEvidence && owned(profile, 'jobs') && inRoots(relative, roots(profile, 'jobs')) && specifiers.some(specifier => specifier === 'eve' || specifier.startsWith('@workflow/'))) rawFinding(findings, 'DUR-001', 'jobs.workflow-ownership', 'Jobs runtime must not own EVE/Workflow state', relative, body, 0)
205
-
206
- const authApplicable = productionEvidence && ((enabled(profile, 'webControlPlane') && profile.project?.access !== 'public') || /better-auth/.test(body))
207
- if (authApplicable) {
208
- for (const match of body.matchAll(/disableCSRFCheck\s*:\s*true|disableOriginCheck\s*:\s*true/g)) rawFinding(findings, 'AUTH-003', 'auth.csrf-origin', 'CSRF and origin checks must remain enabled', relative, body, match.index)
209
- for (const match of body.matchAll(/trustedOrigins\s*[:=][\s\S]{0,120}(?:['"]\*|\*:\/\/|http:\/\/)/gi)) rawFinding(findings, 'AUTH-003', 'auth.trusted-origins', 'Production trusted origins must be exact HTTPS origins', relative, body, match.index)
210
- for (const match of body.matchAll(/require_pkce\s*:\s*false/g)) rawFinding(findings, 'AUTH-004', 'auth.pkce', 'Delegated OAuth clients must not opt out of PKCE', relative, body, match.index)
211
- for (const match of body.matchAll(/defaultSCIM\s*:\s*true|storeSCIMToken\s*:\s*['"]plain['"]/g)) rawFinding(findings, 'AUTH-006', 'identity.scim-storage', 'Production SCIM must be organization-scoped with protected token storage', relative, body, match.index)
212
- for (const match of body.matchAll(/enableSessionForAPIKeys\s*:\s*true|referenceId\s*:\s*['"]user|storeKey\s*:\s*true/g)) rawFinding(findings, 'AUTH-005', 'auth.api-key-scope', 'Workspace API keys must be organization-scoped, hashed, expiring, and must not create browser sessions', relative, body, match.index)
213
- }
214
- if (enabled(profile, 'realtime')) for (const match of body.matchAll(/websocket/gi)) if (/default|primary/i.test(body) && /stream|run/i.test(body)) findings.push(issue('NOT_VERIFIED', 'RT-002', 'realtime.websocket-trigger', 'Verify that bidirectional presence/collaboration justifies WebSockets; default to resumable SSE', evidence(relative, body, match.index), { verificationType: 'manual-qualification', reason: 'intent cannot be proven statically', risk: 'durable state or streaming coupled to WebSocket lifetime', owner: 'project owner', nextAction: 'document and reproduce the bidirectional requirement' }))
215
- }
216
-
217
- if (skipped.symlinks.length || skipped.skillTrees.length || skipped.generated.length) {
218
- const parts = []
219
- if (skipped.symlinks.length) parts.push(`${skipped.symlinks.length} symlinked path(s)`)
220
- if (skipped.skillTrees.length) parts.push(`${skipped.skillTrees.length} agent-skill tree(s)`)
221
- if (skipped.generated.length) parts.push(`${skipped.generated.length} generated/bundled file(s)`)
222
- findings.push(issue('NOT_VERIFIED', 'DEL-001', 'inspection.traversal', `Skipped without inspection: ${parts.join(', ')} (first: ${[...skipped.symlinks, ...skipped.skillTrees, ...skipped.generated][0]})`, { path: '.', line: 1, type: 'static-sentinel' }, { verificationType: 'manual-qualification', reason: 'paths were excluded from static inspection', risk: 'excluded paths may contain violations', owner: 'project owner', nextAction: 'review excluded paths or extend .guardianignore deliberately', skipped }))
223
- }
224
-
225
- if (profile) for (const [name, paths] of Object.entries(observed)) {
226
- if (paths.length && !enabled(profile, name)) rawFinding(findings, 'FOUND-004', `capability.${name}.undeclared`, `Repository evidence suggests disabled capability ${name}: ${paths.slice(0, 3).join(', ')}`, paths[0])
227
- if (owned(profile, name) && !content.some(item => inRoots(item.path, roots(profile, name)))) rawFinding(findings, 'FOUND-004', `capability.${name}.missing-code`, `Enabled owned capability ${name} has no inspectable code in declared source roots`, roots(profile, name)[0] ?? profileRelative)
228
- }
229
-
230
- if (profile?.project?.tenancy === 'multi-tenant-shared-schema') {
231
- const sqlFiles = content.filter(item => item.extension === '.sql' && !nonProductionPath(item.path))
232
- const allSql = sqlFiles.map(item => item.body).join('\n')
233
- const policiesByTable = tablePolicies(sqlFiles)
234
- const tables = sqlFiles.flatMap(file => tenantTables(file.body).map(table => ({ ...table, path: file.path, body: file.body })))
235
- for (const table of tables) {
236
- const escaped = table.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
237
- const primary = table.definition.match(/PRIMARY\s+KEY\s*\(([^)]*)\)/i)?.[1] ?? ''
238
- if (!/\bworkspace_id\b/i.test(primary)) rawFinding(findings, 'TEN-001', 'tenancy.composite-primary-key', `Tenant table ${table.name} primary key must include workspace_id`, table.path, table.body, table.index)
239
- for (const match of table.definition.matchAll(/(?:UNIQUE|REFERENCES\s+[\w."-]+)\s*\(([^)]*)\)/gi)) if (!/\bworkspace_id\b/i.test(match[1])) rawFinding(findings, 'TEN-001', 'tenancy.composite-relationship', `Tenant table ${table.name} unique/foreign relationship must include workspace_id`, table.path, table.body, table.index + (match.index ?? 0))
240
- if (!new RegExp(`ALTER\\s+TABLE\\s+"?${escaped}"?\\s+ENABLE\\s+ROW\\s+LEVEL\\s+SECURITY`, 'i').test(allSql)) rawFinding(findings, 'TEN-002', 'tenancy.rls-enable', `Tenant table ${table.name} must enable RLS`, table.path, table.body, table.index)
241
- if (!new RegExp(`ALTER\\s+TABLE\\s+"?${escaped}"?\\s+FORCE\\s+ROW\\s+LEVEL\\s+SECURITY`, 'i').test(allSql)) rawFinding(findings, 'TEN-002', 'tenancy.rls-force', `Tenant table ${table.name} must force RLS`, table.path, table.body, table.index)
242
- const policies = policiesByTable.get(normalizedSqlName(table.name)) ?? []
243
- if (!policies.some(policy => tenantClause(policy.statement, 'USING')) || !policies.some(policy => tenantClause(policy.statement, 'WITH\\s+CHECK'))) rawFinding(findings, 'TEN-002', 'tenancy.rls-policy', `Tenant table ${table.name} requires table-bound fail-closed tenant USING and WITH CHECK predicates; command-specific policies may split them`, table.path, table.body, table.index)
244
- }
245
- if (!tables.length) rawFinding(findings, 'TEN-002', 'tenancy.rls-evidence', 'Shared-schema tenancy requires reviewed tenant table/RLS migration evidence', 'database migrations')
246
- const all = content.filter(item => item.extension !== '.md' && !nonProductionPath(item.path)).map(item => item.body).join('\n')
247
- if (!/(SET\s+LOCAL\s+app\.workspace_id|set_config\s*\(\s*['"]app\.workspace_id['"][^)]*,\s*true\s*\))/i.test(all)) rawFinding(findings, 'TEN-003', 'tenancy.context-evidence', 'Set tenant context locally inside each protected transaction', 'data access')
248
- for (const file of sqlFiles) for (const match of file.body.matchAll(/SECURITY\s+DEFINER/gi)) {
249
- const window = file.body.slice(Math.max(0, (match.index ?? 0) - 700), (match.index ?? 0) + 700)
250
- if (!/SET\s+(?:LOCAL\s+)?search_path\s*(?:=|TO)\s*(?:pg_catalog|['"]?[^,;'"\s]+['"]?\s*,\s*pg_temp)/i.test(window) || !/(workspace_id|current_setting\s*\(\s*['"]app\.workspace_id|authorize)/i.test(window)) rawFinding(findings, 'TEN-004', 'tenancy.security-definer', 'SECURITY DEFINER tenant paths require a fixed search_path and explicit tenant/authorization guard', file.path, file.body, match.index)
251
- }
252
- }
253
-
254
- if (profile && enabled(profile, 'webControlPlane') && profile.project.access !== 'public' && owned(profile, 'webControlPlane')) {
255
- const nextCode = content.filter(item => item.extension !== '.md' && !nonProductionPath(item.path) && inRoots(item.path, roots(profile, 'webControlPlane'))).map(item => item.body).join('\n')
256
- if (!/from\s+['"]better-auth['"]/.test(nextCode)) rawFinding(findings, 'AUTH-003', 'auth.library', 'Configure the reviewed Better Auth server library', 'identity configuration')
257
- if (profile.project.tenancy.startsWith('multi-tenant') && !/organization\s*\(/.test(nextCode)) rawFinding(findings, 'AUTH-001', 'auth.organization-boundary', 'Configure Better Auth Organization as the workspace boundary', 'identity configuration')
258
- if ((enabled(profile, 'flutter') || enabled(profile, 'connectors')) && !/oauthProvider\s*\(/.test(nextCode)) rawFinding(findings, 'AUTH-004', 'auth.oauth-provider', 'Configure Better Auth OAuth Provider for delegated clients', 'identity configuration')
259
- if (!/useSecureCookies\s*:\s*true/.test(nextCode)) rawFinding(findings, 'AUTH-003', 'auth.secure-cookies-code', 'Force secure browser cookies in production', 'identity configuration')
260
- }
261
-
262
- if (profile) {
263
- for (const [name, capability] of Object.entries(profile.capabilities ?? {})) if (capability.status === 'enabled' && ['shared-managed', 'external-managed'].includes(capability.ownership)) findings.push(issue('NOT_VERIFIED', 'RUN-003', `contract.${name}.qualification`, `${name} contract is declared but live compatibility, isolation, recovery and exit behavior were not reproduced`, { path: profileRelative, line: 1, type: 'declared-control' }, { capability: name, verificationType: 'manual-qualification', reason: 'environment-bound service behavior is not statically testable', risk: 'dependency contract may not meet declared boundary', owner: capability.contract?.incidentOwner, nextAction: 'run contract, failure and exit qualification' }))
264
- if (!profile.objectives) findings.push(issue('NOT_VERIFIED', 'REL-001', 'reliability.objectives', 'No measured production objectives are declared', { path: profileRelative, line: 1, type: 'declared-control' }, { verificationType: 'manual-qualification', reason: 'project objectives were not provided', risk: 'capacity and recovery decisions cannot be qualified', owner: 'project owner', nextAction: 'confirm applicable SLI/SLO, RPO and RTO objectives' }))
265
- if (Object.values(profile.capabilities ?? {}).some(capability => capability.status === 'enabled')) findings.push(issue('NOT_VERIFIED', 'DEL-001', 'qualification.runtime', 'Static sentinels and declarations do not prove runtime isolation, replay, failover, restore, provider security or recovery', { path: profileRelative, line: 1, type: 'static-sentinel' }, { verificationType: 'manual-qualification', reason: 'no environment-bound tests were executed by this checker', risk: 'declared controls may fail at runtime', owner: 'project owner', nextAction: 'run the scoped qualification plan for enabled capabilities' }))
266
- }
267
-
268
- const unique = findings.filter((finding, index, all) => all.findIndex(candidate => candidate.status === finding.status && candidate.rule === finding.rule && candidate.control === finding.control && candidate.path === finding.path && candidate.evidence?.line === finding.evidence?.line && candidate.message === finding.message) === index)
269
- const resolved = capFindings(applyExceptions(unique, exceptionStates, profileRelative))
270
- const knownRules = await canonicalRuleIds()
271
- const catalogPath = resolve(scriptDirectory, '../references/control-catalog.json')
272
- const catalog = JSON.parse(await readFile(catalogPath, 'utf8'))
273
- const knownControl = finding => catalog.controls.some(item => item.rule === finding.rule && (item.id === finding.control || (item.idPattern && new RegExp(item.idPattern).test(finding.control))))
274
- for (const finding of resolved) {
275
- if (!knownRules.has(finding.rule)) throw new Error(`Checker emitted unknown canonical rule ${finding.rule}`)
276
- if (!knownControl(finding)) throw new Error(`Checker emitted unknown control ${finding.rule}/${finding.control}`)
277
- }
278
- if (!resolved.some(item => item.status === 'FAIL')) resolved.unshift(issue('PASS', 'FOUND-004', 'profile.capability-alignment', 'No unexcepted machine-detectable architecture violations were found', { path: profileRelative, line: 1, type: 'static-sentinel' }))
279
- if (legacyProfileName && profile) console.error(`deprecation: ${profileRelative} uses the legacy .yaml name for a JSON document; rename to .vegastack/architecture.json`)
280
- return resolved
281
- }
282
-
283
- export function summarize(findings) {
284
- const counts = Object.fromEntries(['PASS', 'FAIL', 'EXCEPTED', 'NOT_VERIFIED'].map(status => [status, findings.filter(item => item.status === status).length]))
285
- const suppressed = findings.reduce((total, item) => total + (item.suppressedCount ?? 0), 0)
286
- return { status: counts.FAIL ? 'FAIL' : 'PASS', verdict: counts.FAIL || counts.EXCEPTED ? 'GUARDIAN VERDICT: REJECT' : 'GUARDIAN VERDICT: ACCEPT', counts, ...(suppressed ? { suppressedFindings: suppressed } : {}) }
287
- }
288
-
289
- const usage = `Usage: architecture-check.mjs [DIR] [--json | --summary] [--help]
290
-
291
- Deterministic, read-only architecture checks for the directory (default: cwd).
292
- Profile discovery: .vegastack/architecture.json (preferred) or legacy .vegastack/architecture.yaml.
293
- Add repository-relative path prefixes to a .guardianignore file (one per line, # comments) to exclude paths.
294
-
295
- Output: --summary prints the verdict, counts, and the first 10 findings; --json prints everything.
296
- Exit codes: 0 = no FAIL findings; 1 = FAIL findings present; 2 = usage or tool error.`
297
-
298
- if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
299
- const argv = process.argv.slice(2)
300
- const flags = new Set(argv.filter(argument => argument.startsWith('-')))
301
- const positional = argv.filter(argument => !argument.startsWith('-'))
302
- const known = new Set(['--json', '--summary', '--help', '-h'])
303
- const unknown = [...flags].filter(flag => !known.has(flag))
304
- if (flags.has('--help') || flags.has('-h')) { console.log(usage); process.exit(0) }
305
- if (unknown.length || positional.length > 1) { console.error(`error: unknown arguments: ${[...unknown, ...positional.slice(1)].join(' ')}\n\n${usage}`); process.exit(2) }
306
- const root = resolve(positional[0] ?? process.cwd())
307
- let findings
308
- try { findings = await checkArchitecture(root) } catch (error) { console.error(`error: ${error.message}`); process.exit(2) }
309
- const summary = summarize(findings)
310
- if (flags.has('--json')) console.log(JSON.stringify({ schemaVersion: 1, root, ...summary, findings }, null, 2))
311
- else if (flags.has('--summary')) {
312
- console.log(summary.verdict)
313
- console.log(`architecture-check: ${Object.entries(summary.counts).map(([key, value]) => `${value} ${key}`).join(', ')}${summary.suppressedFindings ? `, ${summary.suppressedFindings} suppressed` : ''}`)
314
- const ranked = [...findings].sort((a, b) => (a.status === 'FAIL' ? 0 : 1) - (b.status === 'FAIL' ? 0 : 1))
315
- for (const finding of ranked.slice(0, 10)) console.log(`${finding.status} ${finding.rule}/${finding.control}: ${finding.message}${finding.path ? ` (${finding.path}${finding.evidence?.line ? `:${finding.evidence.line}` : ''})` : ''}`)
316
- if (findings.length > 10) console.log(`(+${findings.length - 10} more findings; use --json for all)`)
317
- } else {
318
- console.log(summary.verdict)
319
- for (const finding of findings) console.log(`${finding.status} ${finding.rule}/${finding.control}: ${finding.message}${finding.path ? ` (${finding.path}${finding.evidence?.line ? `:${finding.evidence.line}` : ''})` : ''}${finding.suppressedCount ? ` (+${finding.suppressedCount} more suppressed)` : ''}`)
320
- console.log(`architecture-check: ${Object.entries(summary.counts).map(([key, value]) => `${value} ${key}`).join(', ')}`)
321
- }
322
- if (summary.status === 'FAIL') process.exitCode = 1
323
- }