@vegastack/skills 0.1.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.
- package/README.md +27 -7
- package/dist/index.js +195 -99
- package/package.json +16 -5
- package/skill/arch-guardian/SKILL.md +84 -0
- package/skill/arch-guardian/agents/openai.yaml +4 -0
- package/skill/arch-guardian/assets/adr-template.md +25 -0
- package/skill/arch-guardian/assets/answers-example.json +10 -0
- package/skill/arch-guardian/assets/architecture-profile.json +13 -0
- package/skill/arch-guardian/assets/architecture-profile.schema.json +31 -0
- package/skill/arch-guardian/references/advisory-report.md +65 -0
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/ai-cost.md +2 -0
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/ai-data-boundaries.md +2 -0
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/ai-evals.md +2 -0
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/connectors-sandbox.md +2 -2
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/delivery-operations.md +5 -5
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/durable-execution.md +3 -1
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/flutter.md +1 -1
- package/skill/arch-guardian/references/architecture/foundation.md +31 -0
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/hosting-reliability.md +1 -1
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/identity-tenancy.md +7 -7
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/model-lifecycle.md +5 -3
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/models-observability.md +1 -1
- package/skill/arch-guardian/references/architecture/security-privacy.md +27 -0
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/topology-monorepo.md +1 -1
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/web.md +1 -1
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/foundation-compatibility.json +1 -1
- package/skill/arch-guardian/references/profile-governance.md +40 -0
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/rule-model.json +2 -2
- package/skill/{vegastack-arch-guardian → arch-guardian}/references/workflows.md +15 -12
- package/skill/{vegastack-arch-guardian → arch-guardian}/refresh/REFRESH.md +10 -3
- package/skill/{vegastack-arch-guardian → arch-guardian}/refresh/sources.json +34 -22
- package/skill/{vegastack-arch-guardian → arch-guardian}/scripts/lib.mjs +1 -10
- package/skill/{vegastack-arch-guardian → arch-guardian}/scripts/profile-tool.mjs +43 -49
- package/skill/{vegastack-arch-guardian → arch-guardian}/scripts/refresh-evidence.mjs +46 -5
- package/skill/arch-guardian/scripts/validate-profile.mjs +65 -0
- package/skill/{vegastack-arch-guardian → arch-guardian}/scripts/verify-corpus.mjs +1 -13
- package/skill/skill-maintainer/SKILL.md +79 -0
- package/skill/skill-maintainer/agents/openai.yaml +4 -0
- package/skill/skill-maintainer/references/release-ops.md +63 -0
- package/skill/skill-maintainer/references/standards.md +92 -0
- package/skill/skill-maintainer/refresh/REFRESH.md +40 -0
- package/skill/skill-maintainer/refresh/sources.json +103 -0
- package/skill/skillify/SKILL.md +154 -0
- package/skill/skillify/agents/openai.yaml +4 -0
- package/skill/skillify/assets/templates/README.md.template +26 -0
- package/skill/skillify/assets/templates/REFRESH.md.template +21 -0
- package/skill/skillify/assets/templates/SKILL.md.template +26 -0
- package/skill/skillify/assets/templates/openai.yaml.template +4 -0
- package/skill/skillify/assets/templates/skill.test.ts.template +40 -0
- package/skill/skillify/assets/templates/sources.json.template +6 -0
- package/skill/skillify/references/authoring.md +72 -0
- package/skill/skillify/references/eval-playbook.md +62 -0
- package/skill/skillify/refresh/REFRESH.md +24 -0
- package/skill/skillify/refresh/sources.json +6 -0
- package/skill/skillify/scripts/scaffold-skill.mjs +138 -0
- package/skill-integrity.json +74 -45
- package/skill/vegastack-arch-guardian/SKILL.md +0 -96
- package/skill/vegastack-arch-guardian/agents/openai.yaml +0 -4
- package/skill/vegastack-arch-guardian/assets/adr-template.md +0 -40
- package/skill/vegastack-arch-guardian/assets/answers-example.json +0 -20
- package/skill/vegastack-arch-guardian/assets/architecture-profile.json +0 -25
- package/skill/vegastack-arch-guardian/assets/architecture-profile.schema.json +0 -213
- package/skill/vegastack-arch-guardian/references/architecture/foundation.md +0 -33
- package/skill/vegastack-arch-guardian/references/architecture/security-privacy.md +0 -23
- package/skill/vegastack-arch-guardian/references/control-catalog.json +0 -55
- package/skill/vegastack-arch-guardian/references/profile-governance.md +0 -54
- package/skill/vegastack-arch-guardian/scripts/architecture-check.mjs +0 -323
- package/skill/vegastack-arch-guardian/scripts/validate-profile.mjs +0 -241
- /package/skill/{vegastack-arch-guardian → arch-guardian}/assets/deployment-review-template.md +0 -0
- /package/skill/{vegastack-arch-guardian → arch-guardian}/assets/service-design-template.md +0 -0
- /package/skill/{vegastack-arch-guardian → arch-guardian}/assets/threat-model-template.md +0 -0
- /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/agent-product.md +0 -0
- /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/data-memory.md +0 -0
- /package/skill/{vegastack-arch-guardian → arch-guardian}/references/architecture/realtime-channels.md +0 -0
- /package/skill/{vegastack-arch-guardian → arch-guardian}/references/golden-patterns.md +0 -0
- /package/skill/{vegastack-arch-guardian → arch-guardian}/scripts/schema-validate.mjs +0 -0
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
Discover confirmed facts before recommending. Ask no more than three material questions at once and branch only when an answer activates a topic:
|
|
6
6
|
|
|
7
|
-
1. Product objective, users, lifecycle stage, access and
|
|
7
|
+
1. Product objective, users, lifecycle stage, access, tenancy mode — and the **tier** (prototype / production / enterprise), a deliberate choice the user confirms.
|
|
8
8
|
2. Clients: web, Flutter, public API, connectors or channels.
|
|
9
9
|
3. Agents, automation, ordinary jobs, untrusted execution and external effects.
|
|
10
|
-
4. Data classes, residency, retention, deletion, RPO/RTO and availability/latency objectives.
|
|
10
|
+
4. Data classes, residency, retention, deletion, RPO/RTO and availability/latency objectives (production tier and above).
|
|
11
11
|
5. Hosting/self-hosting, workload/growth, team/on-call/cost, milestone, and deferrable choices.
|
|
12
12
|
|
|
13
|
-
If facts are unavailable, name bounded assumptions and choose the simplest applicable default. Recommend one capability set and topology
|
|
13
|
+
If facts are unavailable, name bounded assumptions and choose the simplest applicable default. Recommend one capability set and topology sized by the minimum-viable-architecture principle: every proposed moving service names the trigger that justifies it, and every deferred one names the trigger that would. State immediate and deferred decisions, risks, implementation order, and what evidence would qualify the design at the declared tier. Offer a slim profile draft; write only after confirmation.
|
|
14
14
|
|
|
15
15
|
## Brownfield
|
|
16
16
|
|
|
@@ -20,26 +20,29 @@ Separate:
|
|
|
20
20
|
|
|
21
21
|
- current observed state;
|
|
22
22
|
- intended state from committed artifacts;
|
|
23
|
-
- target recommendation;
|
|
23
|
+
- target recommendation at the declared tier;
|
|
24
24
|
- immediate security/correctness risks;
|
|
25
25
|
- required migrations and rollback;
|
|
26
26
|
- optional improvements;
|
|
27
|
-
- runtime evidence that is
|
|
27
|
+
- runtime evidence that is not verified.
|
|
28
28
|
|
|
29
29
|
Prefer incremental migration with compatibility windows and rollback over needless rewrites. Never convert detection heuristics into claims of absence or compliance.
|
|
30
30
|
|
|
31
|
+
## Reviews
|
|
32
|
+
|
|
33
|
+
Identify the decision scope, gather evidence (read the relevant files; use the evidence recipes in the [advisory report contract](advisory-report.md)), and produce an advisory report: per-area grades, severity-ranked findings with evidence, questions, and not-verified items. Findings above the project's tier report as that tier's gate, not as defects.
|
|
34
|
+
|
|
31
35
|
## Other lifecycle tasks
|
|
32
36
|
|
|
33
|
-
For questions and explanations, answer directly: the
|
|
37
|
+
For questions and explanations, answer directly: the recommendation and at most one material risk. The full response contract applies only to design reviews, ADRs, and migration plans.
|
|
34
38
|
|
|
35
39
|
- **Explanation:** answer the decision directly; load only references needed to explain it.
|
|
36
|
-
- **
|
|
37
|
-
- **
|
|
38
|
-
- **
|
|
39
|
-
- **
|
|
40
|
-
- **Source drift:** refresh affected entries only; qualification state changes only through review.
|
|
40
|
+
- **ADR:** a decision record, not a waiver — capture owner, decision, alternatives, risks, and revisit trigger.
|
|
41
|
+
- **Threat model:** model only enabled/exposed surfaces and their trust boundaries, at the tier's depth.
|
|
42
|
+
- **Deployment:** compare declared hosting with deployables, data, secrets, recovery and rollback.
|
|
43
|
+
- **Source drift:** refresh affected entries only; recommendations change only through review.
|
|
41
44
|
- **Migration/removal:** distinguish current/target state, preserve compatibility, and clean durable data, credentials, queues and contracts.
|
|
42
45
|
|
|
43
46
|
## Decision horizons
|
|
44
47
|
|
|
45
|
-
Sequence work as: irreversible security/data/identity boundaries; contracts and durable ownership; deployable placement and recovery; delivery scaffolding; measured optimization. Every phase names owner, verification, rollback and any
|
|
48
|
+
Sequence work as: irreversible security/data/identity boundaries; contracts and durable ownership; deployable placement and recovery; delivery scaffolding; measured optimization. Every phase names owner, verification, rollback and any decision record to update.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Refresh contract —
|
|
1
|
+
# Refresh contract — arch-guardian
|
|
2
2
|
|
|
3
3
|
Instructions for the scheduled refresh agent (and any human running a manual refresh). This file, together with `sources.json`, is the complete freshness contract for this skill.
|
|
4
4
|
|
|
@@ -14,12 +14,14 @@ Instructions for the scheduled refresh agent (and any human running a manual ref
|
|
|
14
14
|
|
|
15
15
|
1. **Deterministic pass first** (no LLM judgment):
|
|
16
16
|
`node scripts/refresh-evidence.mjs --registry refresh/sources.json`
|
|
17
|
-
drift/version-drift/stale/unavailable results are the work-list. Exit 1 with a critical entry means fail-closed: the run must not be silently skipped.
|
|
17
|
+
drift/version-drift/stale/unavailable results are the work-list. Exit 1 with a critical entry means fail-closed: the run must not be silently skipped. The same pass also:
|
|
18
|
+
- queries OSV.dev for every pinned npm/PyPI package (`advisories` in the report; an advisory against a critical source's pin fails closed — the remediation is a human pin-bump decision, never silence);
|
|
19
|
+
- warns when a `reviewBy` date in `references/foundation-compatibility.json` has passed (`reviewOverdue`) — the baseline-adoption decision is overdue, and the warning repeats every run until a human makes it.
|
|
18
20
|
2. **Accept verified changes** in the same code path:
|
|
19
21
|
`node scripts/refresh-evidence.mjs --registry refresh/sources.json --accept-baselines`
|
|
20
22
|
This writes registry, cache, and drift report together — never hand-edit checksums, versions, or timestamps; they must always come from a run.
|
|
21
23
|
3. **Semantic verification** only for sources the deterministic pass flagged: read the changed source (WebFetch the registry URLs), decide whether any `<!-- source: X -->` marked sentence or compatibility pin is now wrong, and propose the minimal edit.
|
|
22
|
-
4. **One
|
|
24
|
+
4. **One standing refresh PR**, branch `refresh/weekly`, force-updated on every run (never stacked duplicates; the weekly workflow .github/workflows/refresh.yml maintains it). PR body lists: each changed source, old→new version/checksum, links to the evidence, and which marked sentences changed and why.
|
|
23
25
|
5. Changes to durable rules, scripts, SKILL.md, assets, or tests are out of scope for a refresh PR — CI enforces this (refresh-guard workflow).
|
|
24
26
|
|
|
25
27
|
## Cadence and thresholds
|
|
@@ -31,6 +33,11 @@ Weekly scheduled run. Every `thresholdDays` in `sources.json` is ≥ 14 (2× cad
|
|
|
31
33
|
- A pinned package/spec version changed (npm/pypi detection) → update `currentVersion`; if the supported baseline should move, that is a **separate human decision**, flagged in the PR body, never auto-applied to `foundation-compatibility.json` families.
|
|
32
34
|
- A doc page's text-scope checksum changed → read the page; if the mechanism a marked sentence names changed, propose the sentence edit; if it is editorial churn, accept the baseline silently. Known per-request-churn sources (Google properties: `FCM-DOCS`, `GOOGLE-MODELS`) drift on nearly every fetch — their checksum drift alone is never a semantic signal; rely on their version/manual review instead.
|
|
33
35
|
- A critical source is unavailable or a redirect leaves the approved host set → investigate immediately; host changes require a deliberate `approvedHosts` update in `scripts/refresh-evidence.mjs` (out of refresh-PR scope — open an issue).
|
|
36
|
+
- An OSV advisory against a pinned package → propose the pin bump (with its qualification implications) in the PR body; never drop the advisory from the report.
|
|
37
|
+
|
|
38
|
+
## Research index
|
|
39
|
+
|
|
40
|
+
Registry entries carry an optional `llms` URL (the vendor's published `llms.txt`) alongside `docsIndex`. Agents answering questions that need current detail beyond a pinned claim should fetch the source's `llms` or `docsIndex` URL (or use an available docs MCP) — the registry doubles as the live research index, not just freshness metadata.
|
|
34
41
|
|
|
35
42
|
## Never
|
|
36
43
|
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"service": "EVE",
|
|
23
23
|
"kind": "pinned-source-package",
|
|
24
24
|
"pinnedVersion": "0.29.5",
|
|
25
|
-
"currentVersion": "0.31.
|
|
26
|
-
"versionCheckedAt": "2026-08-
|
|
25
|
+
"currentVersion": "0.31.3",
|
|
26
|
+
"versionCheckedAt": "2026-08-08T07:53:01.687Z",
|
|
27
27
|
"commit": "8988139097d1e1a1f1aca9b2828e63408848fccf",
|
|
28
28
|
"stability": "beta-protocol-critical",
|
|
29
29
|
"thresholdDays": 14,
|
|
@@ -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",
|
|
@@ -639,7 +646,7 @@
|
|
|
639
646
|
"service": "Turborepo",
|
|
640
647
|
"kind": "official-docs",
|
|
641
648
|
"pinnedVersion": "2.5.6",
|
|
642
|
-
"currentVersion": "2.10.
|
|
649
|
+
"currentVersion": "2.10.9",
|
|
643
650
|
"stability": "tooling",
|
|
644
651
|
"thresholdDays": 30,
|
|
645
652
|
"critical": false,
|
|
@@ -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",
|
|
@@ -666,7 +674,7 @@
|
|
|
666
674
|
"rule:PKG-002",
|
|
667
675
|
"ref:topology-monorepo"
|
|
668
676
|
],
|
|
669
|
-
"versionCheckedAt": "2026-08-
|
|
677
|
+
"versionCheckedAt": "2026-08-08T07:53:01.687Z"
|
|
670
678
|
},
|
|
671
679
|
{
|
|
672
680
|
"id": "FLUTTER-DOCS",
|
|
@@ -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"
|
|
@@ -1047,7 +1058,7 @@
|
|
|
1047
1058
|
"service": "AI SDK",
|
|
1048
1059
|
"kind": "official-docs-package",
|
|
1049
1060
|
"pinnedVersion": "7.x",
|
|
1050
|
-
"currentVersion": "7.0.
|
|
1061
|
+
"currentVersion": "7.0.58",
|
|
1051
1062
|
"stability": "model-library",
|
|
1052
1063
|
"thresholdDays": 30,
|
|
1053
1064
|
"critical": false,
|
|
@@ -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",
|
|
@@ -1075,7 +1087,7 @@
|
|
|
1075
1087
|
"rule:MODEL-001",
|
|
1076
1088
|
"ref:models-observability"
|
|
1077
1089
|
],
|
|
1078
|
-
"versionCheckedAt": "2026-08-
|
|
1090
|
+
"versionCheckedAt": "2026-08-08T07:53:01.687Z"
|
|
1079
1091
|
},
|
|
1080
1092
|
{
|
|
1081
1093
|
"id": "ANTHROPIC-MODELS",
|
|
@@ -1152,8 +1164,8 @@
|
|
|
1152
1164
|
"ref:model-lifecycle",
|
|
1153
1165
|
"ref:models-observability"
|
|
1154
1166
|
],
|
|
1155
|
-
"checksum": "
|
|
1156
|
-
"retrievedAt": "2026-08-
|
|
1167
|
+
"checksum": "5f2224358c1f3f47ba6cdd5e46b2d695a2c8bfcad93faedcab549290fcada1e6",
|
|
1168
|
+
"retrievedAt": "2026-08-08T07:53:01.687Z"
|
|
1157
1169
|
}
|
|
1158
1170
|
]
|
|
1159
1171
|
}
|
|
@@ -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(['ai-sdk.dev', 'ai.google.dev', 'api.flutter.dev', 'aws.amazon.com', 'better-auth.com', 'bun.sh', 'developer.apple.com', 'developers.cloudflare.com', 'developers.openai.com', 'docs.aws.amazon.com', 'docs.flutter.dev', 'firebase.google.com', 'git.postgresql.org', 'github.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 —
|
|
@@ -309,17 +344,23 @@ export async function refreshEvidence(options) {
|
|
|
309
344
|
Object.assign(source, update)
|
|
310
345
|
report.acceptedBaselines.push({ id: source.id, ...update })
|
|
311
346
|
}
|
|
347
|
+
// A manual-review flag raised earlier in this same run is satisfied by the acceptance that
|
|
348
|
+
// just refreshed the source's retrievedAt — it must not fail-close the run that fixed it.
|
|
349
|
+
for (const entry of report.manualVersionReview) if (baselineUpdates.get(entry.id)?.retrievedAt) { entry.due = false; entry.ageDays = 0 }
|
|
312
350
|
await atomicJson(options.registry, registry)
|
|
313
351
|
}
|
|
314
352
|
await atomicJson(options.cache, cache)
|
|
315
353
|
await atomicJson(options.report, report)
|
|
316
|
-
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)
|
|
317
355
|
return { report, failClosed }
|
|
318
356
|
}
|
|
319
357
|
|
|
320
358
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
321
359
|
const options = args(process.argv.slice(2))
|
|
322
360
|
const { report, failClosed } = await refreshEvidence(options)
|
|
323
|
-
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}` : ''}`)
|
|
324
365
|
if (failClosed) process.exitCode = 1
|
|
325
366
|
}
|