@vegastack/skills 0.2.0 → 0.4.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 +2 -2
- package/dist/index.js +9 -9
- package/package.json +2 -2
- package/skill/arch-guardian/SKILL.md +27 -39
- package/skill/arch-guardian/agents/openai.yaml +2 -2
- package/skill/arch-guardian/assets/adr-template.md +10 -25
- package/skill/arch-guardian/assets/answers-example.json +8 -18
- package/skill/arch-guardian/assets/architecture-profile.json +10 -22
- package/skill/arch-guardian/assets/architecture-profile.schema.json +13 -195
- package/skill/arch-guardian/references/advisory-report.md +65 -0
- package/skill/arch-guardian/references/architecture/ai-cost.md +2 -0
- package/skill/arch-guardian/references/architecture/ai-data-boundaries.md +2 -0
- package/skill/arch-guardian/references/architecture/ai-evals.md +2 -0
- package/skill/arch-guardian/references/architecture/connectors-sandbox.md +2 -2
- package/skill/arch-guardian/references/architecture/delivery-operations.md +5 -5
- package/skill/arch-guardian/references/architecture/durable-execution.md +3 -1
- package/skill/arch-guardian/references/architecture/flutter.md +1 -1
- package/skill/arch-guardian/references/architecture/foundation.md +17 -19
- package/skill/arch-guardian/references/architecture/hosting-reliability.md +1 -1
- package/skill/arch-guardian/references/architecture/identity-tenancy.md +7 -7
- package/skill/arch-guardian/references/architecture/model-lifecycle.md +5 -3
- package/skill/arch-guardian/references/architecture/models-observability.md +1 -1
- package/skill/arch-guardian/references/architecture/security-privacy.md +9 -5
- package/skill/arch-guardian/references/architecture/topology-monorepo.md +1 -1
- package/skill/arch-guardian/references/architecture/web.md +1 -1
- package/skill/arch-guardian/references/foundation-compatibility.json +1 -1
- package/skill/arch-guardian/references/profile-governance.md +25 -39
- package/skill/arch-guardian/references/rule-model.json +2 -2
- package/skill/arch-guardian/references/workflows.md +15 -12
- package/skill/arch-guardian/refresh/REFRESH.md +9 -2
- package/skill/arch-guardian/refresh/sources.json +26 -14
- package/skill/arch-guardian/scripts/lib.mjs +1 -10
- package/skill/arch-guardian/scripts/profile-tool.mjs +43 -49
- package/skill/arch-guardian/scripts/refresh-evidence.mjs +43 -5
- package/skill/arch-guardian/scripts/validate-profile.mjs +20 -196
- package/skill/arch-guardian/scripts/verify-corpus.mjs +1 -13
- package/skill/skill-maintainer/refresh/REFRESH.md +1 -1
- package/skill-integrity.json +34 -35
- package/skill/arch-guardian/references/control-catalog.json +0 -55
- package/skill/arch-guardian/scripts/architecture-check.mjs +0 -323
|
@@ -153,7 +153,8 @@
|
|
|
153
153
|
"changelog": "https://github.com/vercel/next.js/releases",
|
|
154
154
|
"package": "https://www.npmjs.com/package/next/v/16.3.0",
|
|
155
155
|
"repository": "https://github.com/vercel/next.js/tree/d73f5622e226358dcef8cf7a8a373333ff265ae7",
|
|
156
|
-
"security": "https://github.com/vercel/next.js/security"
|
|
156
|
+
"security": "https://github.com/vercel/next.js/security",
|
|
157
|
+
"llms": "https://nextjs.org/docs/llms.txt"
|
|
157
158
|
},
|
|
158
159
|
"versionDetection": {
|
|
159
160
|
"type": "npm",
|
|
@@ -227,7 +228,8 @@
|
|
|
227
228
|
"docsIndex": "https://developers.cloudflare.com/workers/",
|
|
228
229
|
"api": "https://developers.cloudflare.com/api/",
|
|
229
230
|
"changelog": "https://developers.cloudflare.com/changelog/",
|
|
230
|
-
"security": "https://www.cloudflare.com/trust-hub/"
|
|
231
|
+
"security": "https://www.cloudflare.com/trust-hub/",
|
|
232
|
+
"llms": "https://developers.cloudflare.com/llms.txt"
|
|
231
233
|
},
|
|
232
234
|
"versionDetection": {
|
|
233
235
|
"type": "http-hash-and-changelog"
|
|
@@ -265,7 +267,8 @@
|
|
|
265
267
|
"changelog": "https://developers.cloudflare.com/changelog/",
|
|
266
268
|
"package": "https://www.npmjs.com/package/@cloudflare/sandbox/v/0.12.4",
|
|
267
269
|
"repository": "https://github.com/cloudflare/sandbox-sdk/tree/798afcf33ead69fdbfb6598ca345f396deb4d6b2",
|
|
268
|
-
"security": "https://github.com/cloudflare/sandbox-sdk/security"
|
|
270
|
+
"security": "https://github.com/cloudflare/sandbox-sdk/security",
|
|
271
|
+
"llms": "https://developers.cloudflare.com/llms.txt"
|
|
269
272
|
},
|
|
270
273
|
"versionDetection": {
|
|
271
274
|
"type": "npm",
|
|
@@ -302,7 +305,8 @@
|
|
|
302
305
|
"primary": "https://developers.cloudflare.com/hyperdrive/concepts/connection-pooling/",
|
|
303
306
|
"docsIndex": "https://developers.cloudflare.com/hyperdrive/",
|
|
304
307
|
"api": "https://developers.cloudflare.com/api/resources/hyperdrive/",
|
|
305
|
-
"changelog": "https://developers.cloudflare.com/changelog/"
|
|
308
|
+
"changelog": "https://developers.cloudflare.com/changelog/",
|
|
309
|
+
"llms": "https://developers.cloudflare.com/llms.txt"
|
|
306
310
|
},
|
|
307
311
|
"versionDetection": {
|
|
308
312
|
"type": "http-hash-and-changelog"
|
|
@@ -337,7 +341,8 @@
|
|
|
337
341
|
"docsIndex": "https://developers.cloudflare.com/ai-gateway/",
|
|
338
342
|
"api": "https://developers.cloudflare.com/api/resources/ai-gateway/",
|
|
339
343
|
"changelog": "https://developers.cloudflare.com/changelog/",
|
|
340
|
-
"security": "https://www.cloudflare.com/trust-hub/"
|
|
344
|
+
"security": "https://www.cloudflare.com/trust-hub/",
|
|
345
|
+
"llms": "https://developers.cloudflare.com/llms.txt"
|
|
341
346
|
},
|
|
342
347
|
"versionDetection": {
|
|
343
348
|
"type": "http-hash-and-changelog"
|
|
@@ -372,7 +377,8 @@
|
|
|
372
377
|
"changelog": "https://modal.com/docs/reference/changelog",
|
|
373
378
|
"package": "https://pypi.org/project/modal/1.5.3/",
|
|
374
379
|
"repository": "https://github.com/modal-labs/modal-client/tree/e8454377aa7c94e562d525c700a95a23b3d8838a",
|
|
375
|
-
"security": "https://modal.com/security"
|
|
380
|
+
"security": "https://modal.com/security",
|
|
381
|
+
"llms": "https://modal.com/llms.txt"
|
|
376
382
|
},
|
|
377
383
|
"versionDetection": {
|
|
378
384
|
"type": "pypi",
|
|
@@ -415,7 +421,8 @@
|
|
|
415
421
|
"changelog": "https://better-auth.com/changelog",
|
|
416
422
|
"package": "https://www.npmjs.com/package/better-auth/v/1.6.26",
|
|
417
423
|
"repository": "https://github.com/better-auth/better-auth/tree/a16b30e8437927c08350194000178073b06af8cf",
|
|
418
|
-
"security": "https://github.com/better-auth/better-auth/blob/a16b30e8437927c08350194000178073b06af8cf/SECURITY.md"
|
|
424
|
+
"security": "https://github.com/better-auth/better-auth/blob/a16b30e8437927c08350194000178073b06af8cf/SECURITY.md",
|
|
425
|
+
"llms": "https://better-auth.com/llms.txt"
|
|
419
426
|
},
|
|
420
427
|
"versionDetection": {
|
|
421
428
|
"type": "npm-suite",
|
|
@@ -649,7 +656,8 @@
|
|
|
649
656
|
"changelog": "https://github.com/vercel/turborepo/releases",
|
|
650
657
|
"package": "https://www.npmjs.com/package/turbo",
|
|
651
658
|
"repository": "https://github.com/vercel/turborepo",
|
|
652
|
-
"security": "https://github.com/vercel/turborepo/security"
|
|
659
|
+
"security": "https://github.com/vercel/turborepo/security",
|
|
660
|
+
"llms": "https://turborepo.dev/llms.txt"
|
|
653
661
|
},
|
|
654
662
|
"versionDetection": {
|
|
655
663
|
"type": "npm",
|
|
@@ -683,7 +691,8 @@
|
|
|
683
691
|
"api": "https://api.flutter.dev/",
|
|
684
692
|
"changelog": "https://docs.flutter.dev/release/release-notes",
|
|
685
693
|
"repository": "https://github.com/flutter/flutter",
|
|
686
|
-
"security": "https://github.com/flutter/flutter/security"
|
|
694
|
+
"security": "https://github.com/flutter/flutter/security",
|
|
695
|
+
"llms": "https://docs.flutter.dev/llms.txt"
|
|
687
696
|
},
|
|
688
697
|
"versionDetection": {
|
|
689
698
|
"type": "release-index"
|
|
@@ -777,8 +786,8 @@
|
|
|
777
786
|
"versionDetection": {
|
|
778
787
|
"type": "http-hash-and-changelog"
|
|
779
788
|
},
|
|
780
|
-
"retrievedAt": "2026-08-
|
|
781
|
-
"checksum": "
|
|
789
|
+
"retrievedAt": "2026-08-08T10:20:35.016Z",
|
|
790
|
+
"checksum": "3e1d7ed893bbc41eff69d7009bd1269c811ea773a9edfb2a05e9e3c68af91343",
|
|
782
791
|
"previousRetrievalChecksum": "d8117a391c5875a7b4d352a3f8b8b89ad12e6617a91038228f7279c732d0cd98",
|
|
783
792
|
"topics": [
|
|
784
793
|
"notifications",
|
|
@@ -994,7 +1003,8 @@
|
|
|
994
1003
|
"api": "https://bun.sh/docs/api",
|
|
995
1004
|
"changelog": "https://bun.sh/blog",
|
|
996
1005
|
"repository": "https://github.com/oven-sh/bun",
|
|
997
|
-
"security": "https://github.com/oven-sh/bun/security"
|
|
1006
|
+
"security": "https://github.com/oven-sh/bun/security",
|
|
1007
|
+
"llms": "https://bun.sh/llms.txt"
|
|
998
1008
|
},
|
|
999
1009
|
"versionDetection": {
|
|
1000
1010
|
"type": "runtime-and-github-release"
|
|
@@ -1026,7 +1036,8 @@
|
|
|
1026
1036
|
"docsIndex": "https://modelcontextprotocol.io/docs",
|
|
1027
1037
|
"changelog": "https://modelcontextprotocol.io/specification/versioning",
|
|
1028
1038
|
"repository": "https://github.com/modelcontextprotocol/modelcontextprotocol",
|
|
1029
|
-
"security": "https://modelcontextprotocol.io/specification/2026-07-28/basic/security_best_practices"
|
|
1039
|
+
"security": "https://modelcontextprotocol.io/specification/2026-07-28/basic/security_best_practices",
|
|
1040
|
+
"llms": "https://modelcontextprotocol.io/llms.txt"
|
|
1030
1041
|
},
|
|
1031
1042
|
"versionDetection": {
|
|
1032
1043
|
"type": "spec-version"
|
|
@@ -1058,7 +1069,8 @@
|
|
|
1058
1069
|
"changelog": "https://github.com/vercel/ai/releases",
|
|
1059
1070
|
"package": "https://www.npmjs.com/package/ai",
|
|
1060
1071
|
"repository": "https://github.com/vercel/ai",
|
|
1061
|
-
"security": "https://github.com/vercel/ai/security"
|
|
1072
|
+
"security": "https://github.com/vercel/ai/security",
|
|
1073
|
+
"llms": "https://ai-sdk.dev/llms.txt"
|
|
1062
1074
|
},
|
|
1063
1075
|
"versionDetection": {
|
|
1064
1076
|
"type": "npm",
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
2
|
import { lstat, readFile, readdir } from 'node:fs/promises'
|
|
3
|
-
import { join, relative
|
|
3
|
+
import { join, relative } from 'node:path'
|
|
4
4
|
|
|
5
|
-
export const LABELS = ['OBSERVED', 'DOCUMENTED', 'REPRODUCED', 'INFERRED', 'RECOMMENDED', 'NOT VERIFIED']
|
|
6
5
|
export const sha256 = body => createHash('sha256').update(body).digest('hex')
|
|
7
6
|
|
|
8
7
|
// Canonical profile basename first; legacy name accepted with a deprecation notice at call sites.
|
|
@@ -47,11 +46,3 @@ export async function listFiles(root, predicate = () => true) {
|
|
|
47
46
|
return output.sort((a, b) => relative(root, a).localeCompare(relative(root, b)))
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
export function slashRelative(root, path) {
|
|
51
|
-
return relative(root, path).split(sep).join('/')
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function issue(status, rule, control, message, evidence, extra = {}) {
|
|
55
|
-
const severity = status === 'FAIL' ? 'fail' : status === 'EXCEPTED' ? 'accepted-risk' : status === 'NOT_VERIFIED' ? 'warning' : 'pass'
|
|
56
|
-
return { status, severity, rule, control, message, verificationType: extra.verificationType ?? 'static-sentinel', ...(evidence ? { evidence, path: evidence.path } : {}), ...extra }
|
|
57
|
-
}
|
|
@@ -5,18 +5,21 @@ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep }
|
|
|
5
5
|
import { pathToFileURL } from 'node:url'
|
|
6
6
|
import { pathExists, readJsonYaml } from './lib.mjs'
|
|
7
7
|
|
|
8
|
-
const capabilityNames = ['
|
|
8
|
+
const capabilityNames = ['web', 'flutter', 'agents', 'jobs', 'sandbox', 'connectors', 'knowledge', 'models', 'enterprise-identity', 'realtime', 'notifications']
|
|
9
9
|
const observedListCap = 25
|
|
10
|
+
// Observation patterns are heuristic evidence, never proof. `secrets` is observed as advisory
|
|
11
|
+
// evidence but is not a declarable capability — secret custody guidance activates whenever
|
|
12
|
+
// production secrets exist.
|
|
10
13
|
const patterns = {
|
|
11
|
-
|
|
14
|
+
web: /(?:next|@opennextjs\/cloudflare)/i,
|
|
12
15
|
flutter: /(?:pubspec\.yaml|package:flutter)/i,
|
|
13
16
|
agents: /(?:\beve\b|@workflow\/world|AgentRun)/i,
|
|
14
17
|
jobs: /(?:pg-boss|PgBoss)/i,
|
|
15
18
|
sandbox: /(?:@cloudflare\/sandbox|SandboxProvider|enableInternet)/i,
|
|
16
19
|
connectors: /(?:\bMCP\b|webhook|connector)/i,
|
|
17
20
|
knowledge: /(?:pgvector|embedding|\bknowledge\b)/i,
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
models: /(?:ai-gateway|generateText|streamText|BYOK)/i,
|
|
22
|
+
'enterprise-identity': /(?:\bSCIM\b|\bSSO\b|saml)/i,
|
|
20
23
|
realtime: /(?:EventSource|text\/event-stream|WebSocket)/i,
|
|
21
24
|
notifications: /(?:firebase_messaging|APNs|notification)/i,
|
|
22
25
|
secrets: /(?:OPENBAO|VAULT_ADDR|CLIENT_SECRET|DATABASE_URL)/i
|
|
@@ -40,18 +43,17 @@ async function inspectableFiles(root) {
|
|
|
40
43
|
|
|
41
44
|
function baseDraft() {
|
|
42
45
|
return {
|
|
43
|
-
schemaVersion:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
exceptions: []
|
|
46
|
+
schemaVersion: 4,
|
|
47
|
+
foundationVersion: '0.4.0',
|
|
48
|
+
project: { name: 'REQUIRED-CONFIRMED-PROJECT-NAME', kind: 'REQUIRED-CONFIRMED-KIND', tier: 'REQUIRED-CONFIRMED-TIER', tenancy: 'REQUIRED-CONFIRMED-TENANCY' },
|
|
49
|
+
hosting: 'REQUIRED-CONFIRMED-HOSTING',
|
|
50
|
+
capabilities: [],
|
|
51
|
+
notes: []
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
// Only literally exact versions are recorded as facts; ranges ("^1.2.3") are not laundered into
|
|
54
|
-
// exact pins —
|
|
56
|
+
// exact pins — they stay observational.
|
|
55
57
|
function exact(value) {
|
|
56
58
|
const normalized = String(value ?? '').trim().replace(/^v/, '')
|
|
57
59
|
return /^\d+\.\d+(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(normalized) ? normalized : null
|
|
@@ -69,7 +71,7 @@ async function inspect(root) {
|
|
|
69
71
|
for (const [name, value] of Object.entries({ ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) })) if (exact(value)) packageVersions[name] = exact(value)
|
|
70
72
|
} catch { /* malformed manifests remain observational unknowns */ }
|
|
71
73
|
}
|
|
72
|
-
const observed = Object.fromEntries(
|
|
74
|
+
const observed = Object.fromEntries(Object.keys(patterns).map(name => [name, []]))
|
|
73
75
|
for (const path of files.filter(path => !/[\\/]\.vegastack[\\/]/.test(path) && (['.ts', '.tsx', '.js', '.mjs', '.cjs', '.json', '.yaml', '.yml', '.toml', '.sql', '.dart'].includes(extname(path)) || basename(path) === 'pubspec.yaml'))) {
|
|
74
76
|
const rel = relative(root, path).split(sep).join('/')
|
|
75
77
|
let body = ''
|
|
@@ -77,27 +79,14 @@ async function inspect(root) {
|
|
|
77
79
|
for (const [name, pattern] of Object.entries(patterns)) if (pattern.test(`${rel}\n${body}`)) observed[name].push(rel)
|
|
78
80
|
}
|
|
79
81
|
const profile = baseDraft()
|
|
80
|
-
|
|
81
|
-
const sourceRoot = evidence[0].includes('/') ? evidence[0].split('/').slice(0, -1).join('/') : '.'
|
|
82
|
-
const versions = {}
|
|
83
|
-
const candidates = {
|
|
84
|
-
webControlPlane: [['next', 'next'], ['openNext', '@opennextjs/cloudflare']],
|
|
85
|
-
agents: [['eve', 'eve'], ['workflowPostgres', '@workflow/world-postgres']],
|
|
86
|
-
jobs: [['pgBoss', 'pg-boss']],
|
|
87
|
-
sandbox: [['cloudflareSandbox', '@cloudflare/sandbox']]
|
|
88
|
-
}[name] ?? []
|
|
89
|
-
for (const [key, pkg] of candidates) if (packageVersions[pkg]) versions[key] = packageVersions[pkg]
|
|
90
|
-
profile.capabilities[name] = { status: 'enabled', ownership: 'REQUIRED-CONFIRMED-OWNERSHIP', versions: Object.keys(versions).length ? versions : { unconfirmed: 'REQUIRED-EXACT-VERSION' }, placement: 'REQUIRED-CONFIRMED-PLACEMENT', sourceRoots: [sourceRoot], controls: { observedSourceRoots: evidence } }
|
|
91
|
-
}
|
|
82
|
+
profile.capabilities = capabilityNames.filter(name => observed[name].length)
|
|
92
83
|
const artifacts = ['.vegastack/architecture.json']
|
|
93
|
-
if (
|
|
94
|
-
if (profile.environments.production.hosting !== 'none') artifacts.push('docs/architecture/deployment-review.md')
|
|
95
|
-
if (['connectors', 'sandbox', 'agents'].some(name => profile.capabilities[name].status === 'enabled')) artifacts.push('docs/architecture/threat-model.md')
|
|
84
|
+
if (['connectors', 'sandbox', 'agents'].some(name => observed[name].length)) artifacts.push('docs/architecture/threat-model.md')
|
|
96
85
|
const after = await inspectableFiles(root)
|
|
97
86
|
let changed = after.length !== before.size
|
|
98
87
|
for (const path of after) if (!before.has(path) || before.get(path) !== (await lstat(path)).mtimeMs) changed = true
|
|
99
88
|
if (changed) throw new Error('inspection mutation guard detected a changed file inventory')
|
|
100
|
-
return { mode: 'brownfield-observed', mutated: false, observed, packageVersions, profileDraft: profile, relevantArtifacts: artifacts, caveats: ['Detection is heuristic and does not prove absence,
|
|
89
|
+
return { mode: 'brownfield-observed', mutated: false, observed, packageVersions, profileDraft: profile, relevantArtifacts: artifacts, caveats: ['Detection is heuristic and does not prove absence, placement, compliance, runtime behavior, or project intent. Every REQUIRED field must be confirmed before use.'] }
|
|
101
90
|
}
|
|
102
91
|
|
|
103
92
|
// Compact context-friendly view for interactive agent use; --json prints the full result.
|
|
@@ -117,26 +106,31 @@ function capObserved(result) {
|
|
|
117
106
|
}
|
|
118
107
|
|
|
119
108
|
function fromAnswers(answers) {
|
|
120
|
-
if (answers.schemaVersion ===
|
|
109
|
+
if (answers.schemaVersion === 4) return answers
|
|
121
110
|
const profile = baseDraft()
|
|
122
|
-
for (const key of ['
|
|
111
|
+
for (const key of ['project', 'hosting', 'capabilities', 'notes', 'foundationVersion']) if (answers[key] !== undefined) profile[key] = answers[key]
|
|
123
112
|
return profile
|
|
124
113
|
}
|
|
125
114
|
|
|
126
|
-
|
|
127
|
-
|
|
115
|
+
// Deterministic v3 -> v4 migration: exceptions are dropped (the advisor records deliberate
|
|
116
|
+
// deviations in notes/ADR documents, never as suppression machinery), capability detail collapses
|
|
117
|
+
// to the enabled list, and versions return to lockfiles.
|
|
118
|
+
const v3CapabilityMap = { webControlPlane: 'web', flutter: 'flutter', agents: 'agents', jobs: 'jobs', sandbox: 'sandbox', connectors: 'connectors', knowledge: 'knowledge', modelRouting: 'models', enterpriseIdentity: 'enterprise-identity', realtime: 'realtime', notifications: 'notifications' }
|
|
119
|
+
const v3KindMap = { 'saas-product': 'saas', 'internal-product': 'internal-tool', 'public-product': 'saas', 'platform-service': 'api', 'shared-package': 'package' }
|
|
120
|
+
|
|
121
|
+
function migrateV3(old) {
|
|
122
|
+
if (old.schemaVersion !== 3) throw new Error('migrate requires a schemaVersion 3 profile; older versions need a fresh v4 profile from answers')
|
|
128
123
|
const profile = baseDraft()
|
|
129
|
-
profile.project = {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
profile.
|
|
136
|
-
profile.capabilities
|
|
137
|
-
|
|
138
|
-
profile.exceptions
|
|
139
|
-
return { profile, guidance: ['The v2 format implied a full platform, so all implied capabilities are enabled in this draft.', 'Confirm project kind/access/tenancy, Flutter and sandbox roots/versions, controls, capability ownership, data/objectives, and migrate each exception to exact v3 rule/control/path scope.', 'No source file or old profile was changed.'] }
|
|
124
|
+
profile.project = {
|
|
125
|
+
name: old.project?.name ?? 'REQUIRED-CONFIRMED-PROJECT-NAME',
|
|
126
|
+
kind: v3KindMap[old.project?.kind] ?? 'REQUIRED-CONFIRMED-KIND',
|
|
127
|
+
tier: 'REQUIRED-CONFIRMED-TIER',
|
|
128
|
+
tenancy: old.project?.tenancy ?? 'REQUIRED-CONFIRMED-TENANCY'
|
|
129
|
+
}
|
|
130
|
+
profile.hosting = old.environments?.production?.hosting ?? 'REQUIRED-CONFIRMED-HOSTING'
|
|
131
|
+
profile.capabilities = Object.entries(old.capabilities ?? {}).filter(([, value]) => value?.status === 'enabled').map(([name]) => v3CapabilityMap[name]).filter(Boolean)
|
|
132
|
+
profile.notes = (old.exceptions ?? []).map(exception => `migrated deviation (was exception ${exception.id} on ${exception.ruleId}): ${exception.decision ?? exception.rationale ?? 'see original ADR'}`)
|
|
133
|
+
return { profile, guidance: ['Confirm the tier deliberately — it decides which concerns apply, not which tools.', 'v3 exceptions were converted to notes; the advisor has no suppression machinery.', 'Versions now come from lockfiles at advice time.', 'No source file or old profile was changed.'] }
|
|
140
134
|
}
|
|
141
135
|
|
|
142
136
|
async function assertNoSymlink(path) {
|
|
@@ -199,18 +193,18 @@ function confinedOutput(dir, output) {
|
|
|
199
193
|
|
|
200
194
|
async function main() {
|
|
201
195
|
const options = parse(process.argv.slice(2))
|
|
202
|
-
if (options.command === 'help') return console.log('Usage: profile-tool.mjs inspect [DIR] [--json] | scaffold ANSWERS.json --dir DIR [--write] [--force] [--output PATH] | migrate
|
|
196
|
+
if (options.command === 'help') return console.log('Usage: profile-tool.mjs inspect [DIR] [--json] | scaffold ANSWERS.json --dir DIR [--write] [--force] [--output PATH] | migrate PROFILE --dir DIR [--write] [--force] [--output PATH]\n\ninspect prints a compact summary by default; --json prints the full draft and evidence.\nscaffold answers format: see assets/answers-example.json next to this skill.\nmigrate converts a v3 profile to a v4 draft (exceptions become notes).\nWrites are atomic, refuse symlinks, stay inside --dir, and require --force to replace differing content.')
|
|
203
197
|
if (options.command === 'inspect') {
|
|
204
198
|
const root = resolve(options.input ?? options.dir)
|
|
205
199
|
const result = await inspect(root)
|
|
206
200
|
return console.log(JSON.stringify(options.json ? capObserved(result) : summarizeInspection(result), null, 2))
|
|
207
201
|
}
|
|
208
|
-
if (!['scaffold', 'migrate
|
|
202
|
+
if (!['scaffold', 'migrate'].includes(options.command) || !options.input) throw new Error('A supported command and input file are required')
|
|
209
203
|
const source = resolve(options.input)
|
|
210
204
|
const parsed = await readJsonYaml(source)
|
|
211
|
-
const result = options.command === 'migrate
|
|
205
|
+
const result = options.command === 'migrate' ? migrateV3(parsed) : { profile: fromAnswers(parsed), guidance: ['Generated only from supplied answers; confirm draft facts before relying on them.'] }
|
|
212
206
|
const body = `${JSON.stringify(result.profile, null, 2)}\n`
|
|
213
|
-
const defaultOutput = options.command === 'migrate
|
|
207
|
+
const defaultOutput = options.command === 'migrate' ? '.vegastack/architecture.v4-draft.json' : '.vegastack/architecture.json'
|
|
214
208
|
const output = confinedOutput(options.dir, options.output ?? defaultOutput)
|
|
215
209
|
const payload = { mode: options.write ? 'authorized-write' : 'dry-run', output, profile: result.profile, guidance: result.guidance }
|
|
216
210
|
if (!options.write) return console.log(JSON.stringify(payload, null, 2))
|
|
@@ -220,4 +214,4 @@ async function main() {
|
|
|
220
214
|
|
|
221
215
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) main().catch(error => { console.error(`error: ${error.message}`); process.exitCode = 1 })
|
|
222
216
|
|
|
223
|
-
export { inspect,
|
|
217
|
+
export { inspect, migrateV3, fromAnswers, atomicWrite }
|
|
@@ -13,7 +13,7 @@ const defaultRegistry = join(skillRoot, 'refresh', 'sources.json')
|
|
|
13
13
|
const maximumBytes = 5 * 1024 * 1024
|
|
14
14
|
// Every host referenced by refresh/sources.json must appear here; loadRegistry enforces the
|
|
15
15
|
// consistency so the allowlist cannot silently drift from the registry.
|
|
16
|
-
const approvedHosts = new Set(['agentskills.io', 'ai-sdk.dev', 'ai.google.dev', 'api.flutter.dev', 'aws.amazon.com', 'better-auth.com', 'bun.sh', 'code.claude.com', 'developer.apple.com', 'developers.cloudflare.com', 'developers.openai.com', 'docs.aws.amazon.com', 'docs.flutter.dev', 'firebase.google.com', 'git.postgresql.org', 'github.com', 'hermes-agent.nousresearch.com', 'learn.chatgpt.com', 'modal.com', 'modelcontextprotocol.io', 'nextjs.org', 'openbao.org', 'openid.github.io', 'opennext.js.org', 'opentelemetry.io', 'platform.claude.com', 'pub.dev', 'pypi.org', 'raw.githubusercontent.com', 'registry.npmjs.org', 'riverpod.dev', 'turborepo.dev', 'workflow-sdk.dev', 'www.cloudflare.com', 'www.npmjs.com', 'www.postgresql.org'])
|
|
16
|
+
const approvedHosts = new Set(['agentskills.io', 'ai-sdk.dev', 'ai.google.dev', 'api.flutter.dev', 'api.osv.dev', 'aws.amazon.com', 'better-auth.com', 'bun.sh', 'code.claude.com', 'developer.apple.com', 'developers.cloudflare.com', 'developers.openai.com', 'docs.aws.amazon.com', 'docs.flutter.dev', 'firebase.google.com', 'git.postgresql.org', 'github.com', 'hermes-agent.nousresearch.com', 'learn.chatgpt.com', 'modal.com', 'modelcontextprotocol.io', 'nextjs.org', 'openbao.org', 'openid.github.io', 'opennext.js.org', 'opentelemetry.io', 'platform.claude.com', 'pub.dev', 'pypi.org', 'raw.githubusercontent.com', 'registry.npmjs.org', 'riverpod.dev', 'turborepo.dev', 'workflow-sdk.dev', 'www.cloudflare.com', 'www.npmjs.com', 'www.postgresql.org'])
|
|
17
17
|
|
|
18
18
|
function flagValue(argv, flag) {
|
|
19
19
|
const value = argv.shift()
|
|
@@ -22,7 +22,7 @@ function flagValue(argv, flag) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function args(argv) {
|
|
25
|
-
const result = { registry: defaultRegistry, cache: '.vegastack/evidence-cache.json', report: '.vegastack/evidence-drift.json', topics: [], offline: false, acceptBaselines: false, now: new Date().toISOString() }
|
|
25
|
+
const result = { registry: defaultRegistry, cache: '.vegastack/evidence-cache.json', report: '.vegastack/evidence-drift.json', topics: [], offline: false, acceptBaselines: false, now: new Date().toISOString(), compatibility: join(skillRoot, 'references', 'foundation-compatibility.json') }
|
|
26
26
|
while (argv.length) {
|
|
27
27
|
const flag = argv.shift()
|
|
28
28
|
if (flag === '--registry') result.registry = resolve(flagValue(argv, flag))
|
|
@@ -33,11 +33,31 @@ function args(argv) {
|
|
|
33
33
|
else if (flag === '--accept-baselines') result.acceptBaselines = true
|
|
34
34
|
else if (flag === '--verify-baselines') { /* explicit alias for the default online verification run */ }
|
|
35
35
|
else if (flag === '--now') result.now = flagValue(argv, flag)
|
|
36
|
+
else if (flag === '--compatibility') result.compatibility = resolve(flagValue(argv, flag))
|
|
36
37
|
else throw new Error(`Unknown option: ${flag}`)
|
|
37
38
|
}
|
|
38
39
|
return result
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
// Security-advisory watch: query OSV.dev for every pinned npm/PyPI package. Advisories against a
|
|
43
|
+
// pinned version are the highest-value freshness signal — they surface in the weekly report and
|
|
44
|
+
// fail closed for critical sources so a vulnerable pin is never silently kept.
|
|
45
|
+
async function osvAdvisories(source, allowHttpLocalhost) {
|
|
46
|
+
const detection = source.versionDetection ?? {}
|
|
47
|
+
const ecosystem = { npm: 'npm', 'npm-suite': 'npm', pypi: 'PyPI' }[detection.type]
|
|
48
|
+
const version = String(source.pinnedVersion ?? '')
|
|
49
|
+
if (!ecosystem || !/^\d/.test(version)) return []
|
|
50
|
+
const packages = detection.type === 'npm-suite' ? detection.packages : [detection.package]
|
|
51
|
+
const findings = []
|
|
52
|
+
for (const name of packages) {
|
|
53
|
+
const response = await safeFetch('https://api.osv.dev/v1/query', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ package: { name, ecosystem }, version }) }, allowHttpLocalhost)
|
|
54
|
+
if (!response.ok) throw new Error(`OSV HTTP ${response.status} for ${name}`)
|
|
55
|
+
const data = JSON.parse(new TextDecoder().decode(await readBounded(response)))
|
|
56
|
+
for (const vuln of data.vulns ?? []) findings.push({ package: name, version, id: vuln.id, summary: vuln.summary ?? null })
|
|
57
|
+
}
|
|
58
|
+
return findings
|
|
59
|
+
}
|
|
60
|
+
|
|
41
61
|
function privateAddress(address) {
|
|
42
62
|
if (address === '::1' || address === '::' || address.startsWith('fc') || address.startsWith('fd') || address.startsWith('fe80:')) return true
|
|
43
63
|
if (!isIP(address)) return true
|
|
@@ -240,7 +260,17 @@ export async function refreshEvidence(options) {
|
|
|
240
260
|
const now = new Date(options.now)
|
|
241
261
|
if (Number.isNaN(now.getTime())) throw new Error(`Invalid --now timestamp: ${options.now}`)
|
|
242
262
|
const selected = registry.sources.filter(source => !options.topics.length || source.topics.some(topic => options.topics.includes(topic)))
|
|
243
|
-
const report = { schemaVersion: 1, generatedAt: now.toISOString(), offline: options.offline, acceptBaselines: options.acceptBaselines, selected: selected.map(source => source.id), drift: [], versionDrift: [], stale: [], unavailable: [], unaffected: [], manualVersionReview: [], acceptedBaselines: [] }
|
|
263
|
+
const report = { schemaVersion: 1, generatedAt: now.toISOString(), offline: options.offline, acceptBaselines: options.acceptBaselines, selected: selected.map(source => source.id), drift: [], versionDrift: [], stale: [], unavailable: [], unaffected: [], manualVersionReview: [], acceptedBaselines: [], advisories: [], advisoryCheckFailed: [], reviewOverdue: [] }
|
|
264
|
+
// Baseline-adoption nags: a reviewBy date that passed without a human decision, or a critical
|
|
265
|
+
// pin lagging a known-newer current version, is how pins rot politely — surface both on every
|
|
266
|
+
// run (warning, never fail-closed).
|
|
267
|
+
if (options.compatibility && await pathExists(options.compatibility)) {
|
|
268
|
+
const compatibility = JSON.parse(await readFile(options.compatibility, 'utf8'))
|
|
269
|
+
for (const [name, baseline] of Object.entries(compatibility.baselines ?? {})) {
|
|
270
|
+
if (baseline.reviewBy && Date.parse(`${baseline.reviewBy}T23:59:59Z`) < now.getTime()) report.reviewOverdue.push({ baseline: name, reviewBy: baseline.reviewBy, state: baseline.state })
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
report.pinLag = registry.sources.filter(source => source.critical && /^\d/.test(String(source.pinnedVersion ?? '')) && /^\d/.test(String(source.currentVersion ?? '')) && source.pinnedVersion !== source.currentVersion).map(source => ({ id: source.id, pinned: source.pinnedVersion, current: source.currentVersion }))
|
|
244
274
|
const baselineUpdates = new Map()
|
|
245
275
|
for (const source of selected) {
|
|
246
276
|
const prior = cache.sources[source.id]
|
|
@@ -261,6 +291,11 @@ export async function refreshEvidence(options) {
|
|
|
261
291
|
else report.unaffected.push(source.id)
|
|
262
292
|
continue
|
|
263
293
|
}
|
|
294
|
+
try {
|
|
295
|
+
for (const advisory of await osvAdvisories(source, options.allowHttpLocalhost)) report.advisories.push(item(source, advisory))
|
|
296
|
+
} catch (error) {
|
|
297
|
+
report.advisoryCheckFailed.push(item(source, { error: error.message }))
|
|
298
|
+
}
|
|
264
299
|
const detectedVersion = await detectVersion(source, options.allowHttpLocalhost)
|
|
265
300
|
if (detectedVersion && detectedVersion !== source.currentVersion) {
|
|
266
301
|
// Under --accept-baselines a verified version change is accepted, not re-reported as drift —
|
|
@@ -316,13 +351,16 @@ export async function refreshEvidence(options) {
|
|
|
316
351
|
}
|
|
317
352
|
await atomicJson(options.cache, cache)
|
|
318
353
|
await atomicJson(options.report, report)
|
|
319
|
-
const failClosed = [...report.stale, ...report.unavailable, ...report.drift, ...report.versionDrift, ...report.manualVersionReview.filter(entry => entry.due)].some(entry => entry.critical || entry.integrityFailure)
|
|
354
|
+
const failClosed = [...report.stale, ...report.unavailable, ...report.drift, ...report.versionDrift, ...report.advisories, ...report.manualVersionReview.filter(entry => entry.due)].some(entry => entry.critical || entry.integrityFailure)
|
|
320
355
|
return { report, failClosed }
|
|
321
356
|
}
|
|
322
357
|
|
|
323
358
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
324
359
|
const options = args(process.argv.slice(2))
|
|
325
360
|
const { report, failClosed } = await refreshEvidence(options)
|
|
326
|
-
console.log(`refresh-evidence: selected=${report.selected.length} drift=${report.drift.length} version-drift=${report.versionDrift.length} stale=${report.stale.length} unavailable=${report.unavailable.length}${report.acceptedBaselines.length ? ` accepted=${report.acceptedBaselines.length}` : ''}`)
|
|
361
|
+
console.log(`refresh-evidence: selected=${report.selected.length} drift=${report.drift.length} version-drift=${report.versionDrift.length} stale=${report.stale.length} unavailable=${report.unavailable.length} advisories=${report.advisories.length}${report.reviewOverdue.length ? ` review-overdue=${report.reviewOverdue.length}` : ''}${report.acceptedBaselines.length ? ` accepted=${report.acceptedBaselines.length}` : ''}`)
|
|
362
|
+
for (const overdue of report.reviewOverdue) console.error(`warning: baseline ${overdue.baseline} (${overdue.state}) passed its reviewBy ${overdue.reviewBy} — a human adoption decision is overdue`)
|
|
363
|
+
for (const lag of report.pinLag ?? []) console.error(`warning: critical source ${lag.id} pin ${lag.pinned} lags current ${lag.current} — review adoption or record the deliberate hold`)
|
|
364
|
+
for (const advisory of report.advisories) console.error(`advisory: ${advisory.package}@${advisory.version} — ${advisory.id}${advisory.summary ? `: ${advisory.summary}` : ''}`)
|
|
327
365
|
if (failClosed) process.exitCode = 1
|
|
328
366
|
}
|