refactor(plugin-sdk): replace hash manifest with JSONL contract (#121473)

* refactor(plugin-sdk): use JSONL API baseline contract

* chore(plugin-sdk): add generated JSONL API baseline
This commit is contained in:
Peter Steinberger
2026-08-10 03:49:17 -07:00
committed by GitHub
parent 0a6a1d9419
commit e37614c83f
12 changed files with 5204 additions and 262 deletions
@@ -87,12 +87,12 @@ git diff --name-status "$baseline_sha"..HEAD -- \
scripts/lib/plugin-sdk-deprecated-public-subpaths.json \
scripts/generate-plugin-sdk-api-baseline.ts \
scripts/generate-config-doc-baseline.ts \
docs/.generated/plugin-sdk-api-baseline.sha256 \
docs/.generated/plugin-sdk-api-baseline.jsonl \
docs/.generated/config-baseline.sha256 \
docs/.generated/config-baseline.counts.json
git diff --numstat "$baseline_sha"..HEAD -- \
docs/.generated/plugin-sdk-api-baseline.sha256 \
docs/.generated/plugin-sdk-api-baseline.jsonl \
docs/.generated/config-baseline.sha256 \
docs/.generated/config-baseline.counts.json
```
@@ -104,10 +104,11 @@ review context, not a warning by itself. A recorded decision is not a reusable
waiver.
Do not use a SHA of all SDK/config source as an automated warning: it would
noise on harmless implementation-only repairs. The two generated hash manifests
are the stable public-contract signal. If this becomes CI, run the comparison
after `pnpm release:prep` and annotate the staging PR with changed records and
the required maintainer decision; do not add a caller-controlled bypass.
noise on harmless implementation-only repairs. The generated SDK JSONL contract
and config hash manifest are the stable public-contract signals. If this becomes
CI, run the comparison after `pnpm release:prep` and annotate the staging PR with
changed records and the required maintainer decision; do not add a
caller-controlled bypass.
## Resolve the Active Line
+1
View File
@@ -2,3 +2,4 @@
CLAUDE.md -text
src/gateway/server-methods/CLAUDE.md -text
ui/src/i18n/.i18n/* linguist-generated
docs/.generated/plugin-sdk-api-baseline.jsonl linguist-generated=true
+1
View File
@@ -191,6 +191,7 @@ ui/.vitest-attachments
docs/.generated/*.json
!docs/.generated/config-baseline.counts.json
docs/.generated/*.jsonl
!docs/.generated/plugin-sdk-api-baseline.jsonl
# Deprecated changelog fragment workflow
changelog/fragments/
+7 -8
View File
@@ -1,19 +1,19 @@
# Generated Docs Artifacts
SHA-256 files are tracked drift-detection artifacts. Full generated snapshots
remain local inspection artifacts.
Generated contract files are tracked drift-detection artifacts. Full pretty
snapshots remain local inspection artifacts.
**Tracked (committed to git):**
- `config-baseline.sha256` — hashes of config baseline JSON artifacts.
- `config-baseline.counts.json` — maximum entry counts for each config baseline kind.
- `plugin-sdk-api-baseline.sha256` — one hash per Plugin SDK entrypoint.
- `plugin-sdk-api-baseline.jsonl` — one content-derived record per Plugin SDK module and export.
- `sqlite-session-transcript-schema-baseline.sha256` — hash of the sessions/transcripts SQLite schema baseline.
**Local only (gitignored):**
- `config-baseline.json`, `config-baseline.core.json`, `config-baseline.channel.json`, `config-baseline.plugin.json`
- `plugin-sdk-api-baseline.json`, `plugin-sdk-api-baseline.jsonl`
- `plugin-sdk-api-baseline.json`
- `.artifacts/sqlite-session-transcript-schema-baseline.sql`
Do not edit any of these files by hand.
@@ -23,10 +23,9 @@ Do not edit any of these files by hand.
- Regenerate Plugin SDK API baseline: `pnpm plugin-sdk:api:gen`
- Validate Plugin SDK API contract manifest: `pnpm plugin-sdk:api:check`
The Plugin SDK manifest hashes each entrypoint independently. PRs changing
separate public modules therefore update separate records instead of racing to
overwrite one whole-surface checksum. Concurrent changes to the same entrypoint
remain a real merge conflict and require regeneration against combined source.
The Plugin SDK contract sorts modules by import specifier and exports by kind
then name. Its line-delimited records keep concurrent changes to separate
exports mergeable while preserving the declaration closure in reviewable text.
- Regenerate SQLite sessions/transcripts schema baseline: `pnpm sqlite:sessions-schema:gen`
- Validate SQLite sessions/transcripts schema baseline: `pnpm sqlite:sessions-schema:check`
File diff suppressed because one or more lines are too long
@@ -1,151 +0,0 @@
89a78a090a26617858696f9c8866faab1ce42a54d9e4c1b9f3b75c28c509182e module/account-core
7dc96f021fd4fb610fa5f71f8cc1610eb5607483cd4f8f2d1409464f052857a3 module/account-helpers
71522995185b956a0cc4927a472cc8d1153e5e998874bfd9a750513175174713 module/account-id
cfc06c91dd67ebaf0fe918434904defff29ce14964a85c2be7b6f2d450aa1ff2 module/account-resolution
4fbb1c87e99399f842a20d75d5e35a4b7064a1b7f02115c23f9a2a7cdcfb57ee module/agent-config-primitives
5e5c6ca972df6c19a44608c520a971adf36443c87902c92b3dd985d733308748 module/agent-harness
b3035dcf1d7640de7b1593346db17eed78462cbe08218c8685c794cb0aceff12 module/agent-harness-runtime
64004b88f8d3f85e6c129d99a0b6dc4a06d278e5bd6c95f823417bd44cafbab3 module/agent-media-payload
c2598f0760bf2bfe1731a38ed9d9aa502657ea78b25b41a3f962a9cb51f326a3 module/agent-runtime
9454ff96e4c7d8218de937c5c145300bd1f87820a1bd4676b8c406dce697221c module/agent-scope-runtime
8fecb210e22bce4532b6ab649b09465f0bd2c857a44abf40db7d683d6491e6da module/allow-from
5df7f149d8ac7c47acfd9d011b2dccdfd6900fd3277e39c14bb53ea92636c4e5 module/allowlist-config-edit
8d8c09953c7d7c445e009aab2141ff5653085b43d6729ac8dcb8850e414fe964 module/approval-auth-runtime
3c739fda98d03da6680d2cc378556697369c26e5c85e83135863ae100d2d2c83 module/approval-client-runtime
e72705221b0beeedc71f38e38377368987262e0973dd879a0bfd03cf577eb7fc module/approval-delivery-runtime
3d23cb8757e53f99936cc1e659c27b2c4d3a794446cc49f99df8f81d26165ba4 module/approval-gateway-runtime
086135157c1873897569d3356cd8412be45ded7d038d1f8cdaa5349d13ef6ba2 module/approval-handler-adapter-runtime
7a1be47d91917b65c09d07b44342274feeaadcfde11b80e5d2ab811a872ae133 module/approval-handler-runtime
b3216dae8929c399a3500c076e408c097be3e184ece8067be8f97a2f71db708a module/approval-native-runtime
ca5e618bed204e43bb9eb57811c5ab2454538b14a1116e55064aa12f31b3a325 module/approval-reply-runtime
0bf52ea3e5eb08c701eb290791df58a73e93f174c6245a7bb460a6ed01230f86 module/approval-runtime
01ca912836b8dec672f705e294f72d346e778557e4c591317d67558ea7669c0b module/archive
d7e53de63b0ac11a266e4abdc18ba6e9401b80309f5c8f5f6a72a00f65dfe3bd module/boolean-param
cb857ebc136481cf9f1e33f8734f866268d571bea98851654671172bef878760 module/channel-actions
a8495ce489f80cdb63a1c2b24398f2f61a104329e2f5a8f525779bf7fcdd9b42 module/channel-config-helpers
c2cc71d5070b6071c51248b0648d1ad1a9468d3737df890adc77ec02025e8853 module/channel-config-primitives
484894c32a2fa1f6ca75538d854569dafbfe48e30c081fc3231f813b3054686c module/channel-config-schema
0c235d458307ff0c4569b7066dafc4793cdb28c16424cc50a210aa7fb744941f module/channel-contract
e5624ce3a3dc4d047435b02b2a7cdadcdd6a7391dd6779899cb1c1d31b3a8d2a module/channel-core
7fd7c6da37cf83582b5d1c1bf7cd185c48ec52aab0e75c6fd26dbe247b8acb81 module/channel-dm-policy
0b642564fa67c91b51980bd2bd2a818391ccb39cf3a5187dfbd7405f06ad37ab module/channel-entry-contract
59f6d44b31d4cba5738f16d270c3c3227ae29f51921f491952c4f29d48d0f86c module/channel-feedback
7e1089c7818c13c358c0d93b6aae399e7ab8ab0e3ed199fa2b5f321b59f99a55 module/channel-inbound
8781927f60d726ec2e03f9225c1a34506b16feff45613cc36b5b6885c335f297 module/channel-inbound-debounce
5e4e745555ef5640eb2a24d5af93416d6fbcb6b918f48597990e17bee990085e module/channel-ingress-runtime
e0e00497e22e413a0502529c2c215a4e45aadd4bac259039db4df99cc9ee5990 module/channel-lifecycle
0e47457e38d1df0bd572e1408cde2ca6a788b65205f43c585316b5ad3a8f2f16 module/channel-logging
13942428b382fcb7fb5cd92f042596826433a17a6262eb8885101fd50c5f13b3 module/channel-message
bb782210aa586ed37f7f88392f349c99d41a7983e5f9b95d0f2606819f2b90fd module/channel-outbound
b6ad67a681421c9042b695172e19139fcc7c4d7825f7fe3182b53bbcc93c98c2 module/channel-pairing
ab58d4891e0fb3ede16481743c099394fcda26532d222f68dd05574382907f8d module/channel-plugin-common
e3920dd8c81268386bbac71158b4540cd259c69cb097e4bc4fc03b7cf246bf3e module/channel-policy
554425777d686c76a7f26c608e8a914fea84945d6f328f5f41f3100ab42393e0 module/channel-reply-pipeline
482370e60135db9bfaf07f24bab549e5fde09ab265a6061a1f587c5d93929e91 module/channel-runtime-context
e988cd8606415fa6a6f49acde7a6b0984408972b2f2cfcde0a5d597d8f030776 module/channel-secret-basic-runtime
70552599e1d2a6959feba95f0bb3155d98ee377027a956414da9c5acba90fcf1 module/channel-secret-runtime
dbb2bf9e63bfe914c3e1d8c0d9e78bdb48d1692e28512a612362d33d24c09863 module/channel-send-result
6fa564bfcb0b84da63b68decbb4653ebd737ac1d7fb202973b57642f01848a0b module/channel-setup
f395f472baedc054431f2376135824ffb9e01ec2784b750b9c67c177aa1238b6 module/channel-status
b227a529438b8765fef0a1af03effeed1b8594152a0e8cfb222a14caa4020a56 module/channel-streaming
b2f920ff4a6b4190e6d6ea0a3effb001751e092f0e3ac0cf296721ff8c383d86 module/channel-streaming-config
fdeffe356c7c4edeec9f8fd03edcadc375eabc7a9412e582b10c3180e3ef40fc module/cli-argv
ad12670dbfe538f8d0ebf4fb2b68080e93a760278278e6b1ce9bb129d4b2d533 module/collection-runtime
ad486bd560ee32f2bcb9ecd3e76d5ffb937f855d6fc4656b632908c0906b97de module/command-auth
7155990bfa4f3b93b80308f582be699abc9f6a433a492dcc295882b80f04f7ea module/command-auth-native
dae206921890e3fe3b722b8c76a8fd35d451dd2cf462058074000bbc158943fe module/command-detection
68403cefe336402ee7e2b47ba55367f7e79765bd5f95d726b37a8b3084bf63f9 module/command-primitives-runtime
8a1433ceb3864c14900b1bc8cc149511d491089963bd2d837efd11bc65b65d02 module/command-status
e2e40bee9b8406c004d579df5704223c5cdd2cb84c3f6e8795cee3f8ca1bdae9 module/config-contracts
3719e5143fd7732aa8db717acb0c3b075e15140422ab8b533157f668f85a2080 module/config-mutation
6af580c0b66ef3940b7a96fced371a7afda4912577d45550aa8d62f9447bbb97 module/config-runtime
883cb53ac9dc6bed411f6a4d4a97345365857adc5fc46395c5d05b36beaadd76 module/conversation-runtime
01781ceb5acf5296de41822d7db2d359a9b622e95e6c6ae291b4fe3a900f6dfd module/core
a28621c0a26372f95851fac43729e8ac6198f6d7c09e7d6cb64bbe93fdab93ec module/dedupe-runtime
ebef0e650ab45e44c9335e2b3e15588c968cea6dadd125364a076f9c50ad1e8c module/device-bootstrap
8546c29222f65dffe162ae543657bd632400b2691e0aa7890dfecb7f7f25664c module/diagnostic-runtime
b5f9a2bd44fa90d8c4212f73d12989a55669fe2971513c8bd21fcb173dbb6f35 module/directory-runtime
d41a1bfd64023359a857cda843309d86a32bd1cf4799a8f9027b5de55c5ea292 module/discord
aea6c70b74cf24a9dd34c9e127e39623bb86cae13de809e0103aff456c623eec module/error-runtime
05caf0ebadfd51ae6fda842484392c142d66f47524bded2d4843aaeb6ebe23c0 module/extension-shared
dd9f6e0fd33cc88b22543c1ee30cc09cf4de4d8f30dff7b7f9cebef885c21543 module/gateway-method-runtime
30d5e1a52ccd32b2adfed890f97d6614f79d9c6b524c3405707a849d18341618 module/gateway-runtime
1b1c6bd5bfc0cfb0c5bb9bd97f8ac1928750cb87232a3415dd066cc21d7b48b7 module/group-access
45fac7ba15b10002f331321336c8c176746a185998b77383c588a98b8948250d module/health
ced757b895c88172b44cd876ac05b49edfd526c566f2576c54ba54fe95c519e5 module/hook-runtime
112224f263b0fe8413dfeef48c2ffe31c3b1b102984eb0e6f2646c3170603465 module/inbound-envelope
4928af5d2509f696b896f53ac790303a0742202dbcdae3e44fe6d1b434a9c1ba module/inbound-event-delivery
ebbc8539a60bf3eac8f3936930396501d63172205993c3051557056d926cdfdf module/inbound-reply-dispatch
c76a09319f8417c1380072a7726137379095e023c834757e5737f4a4ad2bd997 module/infra-runtime
ce73721421f1b903dd04ead4df173582e59ea3e9990248102c448b419cc6d272 module/ingress-effect-once
c85dfa7f61f04dec284af1516ce9cb923458a54f7827b78f1a507546553f3ed1 module/interactive-runtime
408d257ab5cc4b88a22b7e7595039cb8fc524b261c44141b294fbd0100ba62ee module/json-store
e907fd3a98185f2c261f2aafcaa5a19ee1d7b459d519a498397d629f84c68312 module/lazy-runtime
8cca40cd8f8430d8b9ce3d9d79358e1be8f7474180960c2b40cd57b5c144c1d0 module/logging-core
f1ca4ced4305d0769c2d8cc1291137ac7002fe0e6eaec2c1a71edad2204c8311 module/matrix
d32041b6c9d8801c78aa233cd232b0cff13dbe1a340de599ae6e4a005421ecf4 module/media-local-roots
f74d7295fe716aa140aa0bc9300d6259d71dab826de0808fca6bb02592bf5d6e module/media-mime
5dd079f78d1d96359e556853e5cd4f82ce10e0564fd82885bfd4cb44b9065c7d module/media-runtime
6a52f93107335f88751704352cc01e62add06f854a5b7d765e2a5ee87c0313b6 module/media-store
246c8b2812f4353e039de0fc997c58d7b461456d1bf5b04d0aa32fb280a364ea module/media-understanding
639d2ab2837f00621fa657e29b86eeb3b9b672c91a2f28623d754921603dcfd5 module/media-understanding-runtime
36c3dbb38106e9cf2df5fb04afb12de4ddf02c77901b5fd36d3b1e2ad72a5233 module/meeting-runtime
c10be4c48e5ac37b43206b9d55a4306f50ff73735be323ae93f23692e530ee3a module/memory-core-host-engine-foundation
79d2ed79a8696ad1674dc3a503ec6f46cafe669999a569b956462547d5b67d55 module/memory-host-core
1efa0aadc4261d1c6073058cbf3dcc9fa681424819bdd14333e19b249bbc4b18 module/messaging-targets
2a573fa218a44c27ddc65735c5fb49fe1ef691bea72d52ce502c5afcc45ce47c module/model-session-runtime
6a0eb4b0c0ed099ecc6b490be90d6e15340998bcc3e0cd9e969632d3e3d78c41 module/models-provider-runtime
8c73f1d05264e1d3cb5c0a3b52aafd4669a9633b60122f45dff1a5a3e1812565 module/native-command-config-runtime
e3b6d6bf3bfdc79b9817574db4174dff11ccf306ab521e2547e14a0dc9fff8c7 module/native-command-registry
6fbafa5e99257462e1766d04e9aa08e22229cf132de51924457743d9741a32fd module/param-readers
ca7a56bb1a6169b4cf9befbf5aa21da280a8086fdc49fca4eec520a7a7c98549 module/persistent-dedupe
f806b7326c4462fbbfc7407ff5d0eea831dbda4f87f73d0cdb193c1baf9e195d module/plugin-config-runtime
beb6923354b3046a7c552a3476eb9c0c33f3c994de69e4b10a97a3eeaf4ce3fe module/plugin-entry
d54879d527a9de84af4820bae79e66ad207896c595119f12330eca641151a2ce module/plugin-runtime
d16722b00152d27c415ee5efd7a901a9f42f7b68c969cd8ff7bd53cea1456bb1 module/provider-auth
6798bbe969215d0600d13b429098ca37133fa455d0589890ae79a9fd40a6d37d module/provider-catalog-runtime
8131147d699394bd06503e2ea2f5f1a50b1594a87dded6d118b74a8d0328c8f6 module/proxy-capture
d077971d6208c5459ee2234283cae14ac4cfd74a8362de461800ed64ab5b3bc3 module/question-gateway-runtime
d8836fcad8b49d6f2965cfda99dc31fe3d0229d97ecfeca494dc3bebf3a81be0 module/reply-chunking
ee62553e9d036c1766e86551cee8716886538eea5f300735e1487a7e67db7f9f module/reply-dispatch-runtime
73f861fa3179d5af1159853c5acab0eec7a6c8f9398dcb75ea770e784fca6727 module/reply-history
383b1bcefcbcb768dd20a4a2c3e54a96f7a75ad14eb9a3cc4665468b0e1159b5 module/reply-payload
ce7fc1c4130ad37229ed5435542c072877330dcc98d8fc9c9bb8e7fb254ea744 module/reply-runtime
aa07d85d99fdd2b1e0cbe9975fb6dcae66b8bdce2607c6bd5402ae68bb15118c module/root-walk
1404e950e93a7adbb2d2e5d3246b31476ea722846a4b2a6cd16f1af9ca0e87f5 module/routing
7877a7e58fa32a64107154e5b714c6d165e96989d4aa5f43e0afac085a187af0 module/run-command
07e624c859c3f91fe010aac0e6992456e00c033c883ed0fba0760dfac68ab8f1 module/runtime
4aeeb72d62ee0429395b127f9593539c4fe33ed3752c15f9cb1f29a8f3c568e7 module/runtime-config-snapshot
300a1d4b0d0d1b68274510e3dc16b4e336319bd8ac4e8358a0a5251af9e841d7 module/runtime-env
49e9b6a8195c89704eaa80656f176444af7cacbf639b759f41f2c78ae6bfcfd9 module/runtime-group-policy
2aae1e2770ca4970e07745eba226cdb6c93fc9cbad70309706d52ddf31b4bfa7 module/runtime-store
d17862c40825af1ddf0257b44f1e1cbb9c375e8e5ed668fae75d530d1a465cf9 module/secret-file
8e2ac4d3973d8d8ce4478e3440d66ee5c0d9213b0fe9e927c421d14fd31e5e86 module/secret-input
9490b04fb0167b8e505e90625e3464a8c21807aa06d8d57b68bf95c7806eee2a module/secret-input-runtime
25ec161b94736377563c08f9cda400a7133434ad72e74eeb553c31542509a214 module/secret-ref-runtime
0f5596e9b55521ebde283839d15cc2f3d491dbff9bb8067ec4e5b7b884677c3a module/security-runtime
73c493015363b289377349367199715d1a282ee9082baa99bb792b15deb9199c module/session-catalog
5a00f670509141d28dcfd9475e2926ad8d4ae6ded2654f9f54933a09d66d4f7f module/session-discussion
834ed405f3c02c20b564ae1d69a0d29dd758e99c9c3a9bef7710cb3e31340601 module/session-store-runtime
2645f8827c4751b57167d67ab4f423a1e7edce079c3a973981a3f753fa0834d1 module/setup
f292edb93a7bc7e499490dc9bf316015775e68edf63be858673b5c2bdbb90799 module/setup-runtime
d0cb4c5abb7484352088f556c1ba7c7b147d7b57977b8246e5cb7187937768b7 module/setup-tools
81c3d0a5194ec8bc6ee8635f2910bee69815635ddd6aeef88e988d4effabc0b7 module/skill-commands-runtime
0bbda17999433ca8dd0e01ddffb6760096a3d90813d34c6c371ce9a62e0361d0 module/speech-settings
e5096ff1a61bec0413590be28a9254ff4773c350d5095ff82fb9be0be691f9bc module/ssrf-policy
393b98970842cfff3209c6e146604e9e5a72ea11a87fb3e622a1f0bb36007fae module/ssrf-runtime
bc3f1c2d9aeb4a0890905fcea07d4662bf65a3687a4590cc5c34fca7d55510b4 module/state-paths
6920a1afa8347a4cef78214df539fc5e0e2b46769a814812791b36e507a8ba16 module/status-helpers
f097d0096b21c8a052f0f649b7512ecf2aba4744ae6956f001950e053828b309 module/string-coerce-runtime
0dd45b013da7e164d3d7177332bc268a18104eae9c153416b94a2c981dbdda6b module/telegram-account
aef35bee2502cd6ed8765409b758e452aff8ac9469fd773e6a2a44c9a1bc3f66 module/temp-path
87fa81b9e58d8fc04a4b4202d2d37fca339615f5225687d9db905151439e0f4d module/text-chunking
19e1bfca1b06768d86835c0fe416ca8c7c67e63d5f2e00a4ad902a7ada70bb9c module/text-runtime
c4075684cd95ea5a80143cb2e45a7bdc6bca8dc22b8eff394d9c55a0ddcf4c40 module/tool-plugin
dc1a073c59ab61e2789533b777b3f0cb9af689d64a97796b10e8aa82552510db module/tool-results
dd6611db9ee085fd7c7410e85c4a472b2e3a0dec91df0343aaf6efd81a64cafb module/tool-send
cda105b721d498df23a554c6b68be150b8fe66b8b9172185c31a0b3b0646b1dc module/web-media
790b99e8523e0190c8bf908d37f1aee4d1833bc7c96e765f03996eed7d770768 module/webhook-ingress
7f12f9054eabfac5eeff0d06b3e8400a90f7a5424cd4ca36113224306d60081b module/webhook-request-guards
de59e86e126b75d13251cba7ebbe27b44d9b5588785d98df5ff4d6722374c81f module/widget-html
9161b36ec0ab062ea41b363c894fcd672a7727f21cb726739f99f9c184fce69d module/zod
+1 -1
View File
@@ -96,7 +96,7 @@ const DOCTOR_CONTRACT_OWNER_TEST_PATH_RE =
const SQLITE_SESSION_SCHEMA_BASELINE_PATH_RE =
/^(?:src\/state\/openclaw-agent-schema\.sql|scripts\/(?:generate-sqlite-session-schema-baseline\.ts|lib\/sqlite-session-schema-baseline\.ts)|test\/scripts\/sqlite-session-schema-baseline\.test\.ts|docs\/\.generated\/sqlite-session-transcript-schema-baseline\.sha256)$/u;
const PLUGIN_SDK_API_BASELINE_PATH_RE =
/^(?:src\/|packages\/|extensions\/|pnpm-lock\.yaml$|tsconfig\.json$|scripts\/(?:generate-plugin-sdk-api-baseline\.ts|lib\/plugin-sdk-(?:doc-metadata\.ts|entries\.mts|entrypoints\.json|private-local-only-subpaths\.json))|docs\/\.generated\/plugin-sdk-api-baseline\.sha256$)/u;
/^(?:src\/|packages\/|extensions\/|pnpm-lock\.yaml$|tsconfig\.json$|scripts\/(?:generate-plugin-sdk-api-baseline\.ts|lib\/plugin-sdk-(?:doc-metadata\.ts|entries\.mts|entrypoints\.json|private-local-only-subpaths\.json))|docs\/\.generated\/plugin-sdk-api-baseline\.jsonl$)/u;
const PLUGIN_SDK_SURFACE_PATH_RE =
/^(?:package\.json$|src\/plugin-sdk\/|packages\/plugin-sdk\/|scripts\/(?:plugin-sdk-surface-report\.mts|sync-plugin-sdk-exports\.mts|lib\/plugin-sdk-(?:declaration-budget\.mts|deprecated-barrel-subpaths\.json|deprecated-public-subpaths\.json|entries\.mts|entrypoints\.json|private-local-only-subpaths\.json)))/u;
const DEPRECATION_HYGIENE_PATH_RE =
+14 -5
View File
@@ -18,24 +18,33 @@ async function main(): Promise<void> {
const result = await writePluginSdkApiBaselineArtifacts({ repoRoot, check: checkOnly });
if (checkOnly) {
if (result.changed) {
const contractPath = path.relative(repoRoot, result.contractPath);
const diff = result.contractDiff;
console.error(
[
"Plugin SDK API contract drift detected.",
`Manifest mismatch: ${path.relative(repoRoot, result.hashPath)}`,
"If this Plugin SDK surface change is intentional, run `pnpm plugin-sdk:api:gen` and commit the updated manifest.",
`Contract mismatch: ${contractPath}`,
`--- ${contractPath} (committed)`,
`+++ ${contractPath} (generated)`,
...(diff?.previewLines ?? []),
`Changed JSONL lines: ${diff?.changedLineCount ?? 0}${
diff && diff.shownLineCount < diff.changedLineCount
? ` (showing first ${diff.shownLineCount})`
: ""
}`,
"If this Plugin SDK surface change is intentional, run `pnpm plugin-sdk:api:gen` and commit the updated contract.",
"If not intentional, fix the plugin-sdk exports or metadata first.",
].join("\n"),
);
process.exit(1);
}
console.log(`OK ${path.relative(repoRoot, result.hashPath)}`);
console.log(`OK ${path.relative(repoRoot, result.contractPath)}`);
return;
}
console.log(
[
`Wrote ${path.relative(repoRoot, result.hashPath)}`,
`Wrote ${path.relative(repoRoot, result.contractPath)}`,
`Wrote ${path.relative(repoRoot, result.jsonPath)} (gitignored, local only)`,
`Wrote ${path.relative(repoRoot, result.statefilePath)} (gitignored, local only)`,
].join("\n"),
);
}
+88
View File
@@ -0,0 +1,88 @@
import { diffLines } from "diff";
const CONTRACT_DIFF_LINE_LIMIT = 40;
/** Bounded unified-style preview of changed JSONL contract records. */
export type PluginSdkApiBaselineContractDiff = {
/** Total added and removed JSONL lines. A modified record counts as two lines. */
changedLineCount: number;
/** Unified-style hunk headers and changed lines, capped for terminal output. */
previewLines: string[];
/** Number of added and removed lines included in the preview. */
shownLineCount: number;
};
function describeContractLine(line: string): { identity: string | null; label: string } {
try {
const record = JSON.parse(line) as {
entrypoint?: unknown;
exportName?: unknown;
recordType?: unknown;
};
if (typeof record.entrypoint === "string" && record.recordType === "module") {
return {
identity: `module\0${record.entrypoint}`,
label: `entrypoint=${record.entrypoint}`,
};
}
if (
typeof record.entrypoint === "string" &&
typeof record.exportName === "string" &&
record.recordType === "export"
) {
return {
identity: `export\0${record.entrypoint}\0${record.exportName}`,
label: `entrypoint=${record.entrypoint} exportName=${record.exportName}`,
};
}
} catch {
// Invalid committed JSONL still appears in the bounded raw-line diff.
}
return { identity: null, label: "unparseable record" };
}
export function diffPluginSdkApiBaselineContract(
current: string | null,
next: string,
): PluginSdkApiBaselineContractDiff {
const changes = diffLines(current ?? "", next, { oneChangePerToken: true })
.filter((change) => change.added || change.removed)
.map((change) => {
const line = change.value.replace(/(?:\r?\n)$/u, "");
const description = describeContractLine(line);
return {
change,
identity: description.identity,
label: description.label,
line,
};
});
const addedIdentities = new Set(
changes.flatMap(({ change, identity }) => (change.added && identity ? [identity] : [])),
);
const removedIdentities = new Set(
changes.flatMap(({ change, identity }) => (change.removed && identity ? [identity] : [])),
);
const structuralChanges = new Set(
changes.filter(
({ change, identity }) =>
identity &&
((change.added && !removedIdentities.has(identity)) ||
(change.removed && !addedIdentities.has(identity))),
),
);
const preview = [
...structuralChanges,
...changes.filter((change) => !structuralChanges.has(change)),
].slice(0, CONTRACT_DIFF_LINE_LIMIT);
const previewLines = preview.flatMap(({ change, label, line }) => [
`@@ ${label} @@`,
`${change.added ? "+" : "-"}${line}`,
]);
return {
changedLineCount: changes.length,
previewLines,
shownLineCount: preview.length,
};
}
+37 -37
View File
@@ -8,13 +8,12 @@ import { afterEach, beforeAll, describe, expect, it } from "vitest";
import { publicPluginSdkEntrypoints } from "../../scripts/lib/plugin-sdk-entries.mts";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
import {
computePluginSdkApiBaselineHashFileContent,
formatPluginSdkApiTypeAlias,
listPluginSdkApiBaselineEntrypoints,
normalizePluginSdkApiDeclarationText,
normalizePluginSdkApiSourcePath,
renderPluginSdkApiBaseline,
renderPluginSdkApiBaselineModules,
writeRenderedPluginSdkApiBaselineArtifacts,
type PluginSdkApiBaselineRender,
} from "./api-baseline.js";
@@ -124,9 +123,11 @@ function createTupleAliasFixture(tuple: string, warmup: string, prewarm: boolean
describe("Plugin SDK API baseline", () => {
let rendered: PluginSdkApiBaselineRender;
// Rendering builds a TS program across SDK entrypoints. Loaded CI runners can
// exceed the default hook budget; this work is compile-bound, not a hang.
beforeAll(async () => {
rendered = await renderPluginSdkApiBaseline({ entrypoints: TEST_ENTRYPOINTS });
});
}, 300_000);
it("normalizes declaration import paths to repo-relative paths", () => {
const repoRoot = process.cwd();
@@ -272,40 +273,47 @@ describe("Plugin SDK API baseline", () => {
expect(rendered.jsonl).not.toContain('"sourceLine":');
});
it("renders snapshots independently of entrypoint discovery order", () => {
const reverse = renderPluginSdkApiBaselineModules(rendered.baseline.modules.toReversed());
it("renders byte-identical JSONL deterministically", async () => {
const firstRender = await renderPrivateDeclarationFixture();
const secondRender = await renderPrivateDeclarationFixture();
expect(reverse.json).toBe(rendered.json);
expect(reverse.jsonl).toBe(rendered.jsonl);
expect(secondRender.jsonl).toBe(firstRender.jsonl);
});
it("hashes entrypoints independently so unrelated API changes merge", () => {
const target = rendered.baseline.modules[0];
expect(target?.exports.length).toBeGreaterThan(0);
const changed = renderPluginSdkApiBaselineModules(
rendered.baseline.modules.map((moduleSurface) =>
moduleSurface === target
? {
...moduleSurface,
exports: moduleSurface.exports.map((exportSurface, index) =>
index === 0
? { ...exportSurface, declaration: `${exportSurface.declaration ?? ""} changed` }
: exportSurface,
),
}
: moduleSurface,
),
);
const before = computePluginSdkApiBaselineHashFileContent(rendered).split("\n");
const after = computePluginSdkApiBaselineHashFileContent(changed).split("\n");
it("fails checks on contract drift and passes after write", async () => {
const outputDir = tempDirs.make("openclaw-plugin-sdk-api-output-");
const contractPath = path.join(outputDir, "plugin-sdk-api-baseline.jsonl");
const jsonPath = path.join(outputDir, "plugin-sdk-api-baseline.json");
fs.writeFileSync(contractPath, "stale\n");
const options = {
contractPath,
jsonPath,
rendered,
} as const;
expect(after[0]).not.toBe(before[0]);
expect(after.slice(1)).toEqual(before.slice(1));
const drifted = await writeRenderedPluginSdkApiBaselineArtifacts({
...options,
check: true,
});
expect(drifted).toEqual(expect.objectContaining({ changed: true, wrote: false }));
await writeRenderedPluginSdkApiBaselineArtifacts(options);
const current = await writeRenderedPluginSdkApiBaselineArtifacts({
...options,
check: true,
});
expect(current).toEqual(expect.objectContaining({ changed: false, wrote: false }));
expect(fs.readFileSync(contractPath, "utf8")).toContain(
'"importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"',
);
expect(fs.readFileSync(jsonPath, "utf8")).toContain(
'"generatedBy": "scripts/generate-plugin-sdk-api-baseline.ts"',
);
});
it("captures transitive private declaration changes deterministically", async () => {
const baseline = await renderPrivateDeclarationFixture();
const unchanged = await renderPrivateDeclarationFixture();
const optionChanged = await renderPrivateDeclarationFixture({ optionalOption: true });
const resultChanged = await renderPrivateDeclarationFixture({ optionalResult: true });
const declaration = baseline.baseline.modules[0]?.exports[0];
@@ -323,19 +331,11 @@ describe("Plugin SDK API baseline", () => {
expect(declaration?.declaration).not.toContain("required: string;");
expect(declaration?.declaration).not.toContain("value: string;");
expect(declaration?.declaration).not.toContain("externalOnly: string;");
expect(unchanged.json).toBe(baseline.json);
expect(unchanged.jsonl).toBe(baseline.jsonl);
expect(computePluginSdkApiBaselineHashFileContent(unchanged)).toBe(
computePluginSdkApiBaselineHashFileContent(baseline),
);
for (const changed of [optionChanged, resultChanged]) {
expect(changed.baseline.modules[0]?.exports[0]?.declaration).not.toBe(
declaration?.declaration,
);
expect(computePluginSdkApiBaselineHashFileContent(changed)).not.toBe(
computePluginSdkApiBaselineHashFileContent(baseline),
);
}
});
});
+48 -52
View File
@@ -1,5 +1,4 @@
// API baseline helpers hash public SDK exports for contract drift checks.
import { createHash } from "node:crypto";
// API baseline helpers render public SDK exports for contract drift checks.
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
@@ -9,6 +8,10 @@ import {
type PluginSdkDocCategory,
type PluginSdkDocEntrypoint,
} from "../../scripts/lib/plugin-sdk-doc-metadata.ts";
import {
diffPluginSdkApiBaselineContract,
type PluginSdkApiBaselineContractDiff,
} from "./api-baseline-contract.js";
import {
attachPluginSdkDeclarationClosures,
createDeclarationClosureRenderer,
@@ -90,22 +93,21 @@ export type PluginSdkApiBaselineRender = {
/** Result returned when writing SDK API baseline artifacts. */
export type PluginSdkApiBaselineWriteResult = {
/** True when the generated contract manifest differs from disk. */
/** True when the generated JSONL contract differs from disk. */
changed: boolean;
/** Bounded record-level diff when a check finds contract drift. */
contractDiff: PluginSdkApiBaselineContractDiff | null;
/** Committed JSONL contract path. */
contractPath: string;
/** True when generated artifacts were actually written. */
wrote: boolean;
/** JSON baseline artifact path. */
jsonPath: string;
/** JSONL statefile artifact path. */
statefilePath: string;
/** Per-record SHA-256 contract manifest path. */
hashPath: string;
};
const GENERATED_BY = "scripts/generate-plugin-sdk-api-baseline.ts" as const;
const DEFAULT_JSON_OUTPUT = "docs/.generated/plugin-sdk-api-baseline.json";
const DEFAULT_STATEFILE_OUTPUT = "docs/.generated/plugin-sdk-api-baseline.jsonl";
const DEFAULT_HASH_OUTPUT = "docs/.generated/plugin-sdk-api-baseline.sha256";
const DEFAULT_CONTRACT_OUTPUT = "docs/.generated/plugin-sdk-api-baseline.jsonl";
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
@@ -675,7 +677,7 @@ export async function renderPluginSdkApiBaseline(params?: {
repoRoot,
entrypoints,
);
const modules = entrypoints.map((entrypoint) =>
const modules = [...entrypoints].toSorted(compareText).map((entrypoint) =>
buildModuleSurface({
checker,
declarationClosure,
@@ -718,22 +720,6 @@ async function loadCurrentFile(filePath: string): Promise<string | null> {
}
}
function sha256(content: string): string {
return createHash("sha256").update(content, "utf8").digest("hex");
}
/** Build a mergeable per-entrypoint sha256 manifest for the Plugin SDK API contract. */
export function computePluginSdkApiBaselineHashFileContent(
rendered: PluginSdkApiBaselineRender,
): string {
return `${rendered.baseline.modules
.map((moduleSurface) => {
const label = `module/${encodeURIComponent(moduleSurface.entrypoint)}`;
return `${sha256(JSON.stringify(moduleSurface))} ${label}`;
})
.join("\n")}\n`;
}
function validateMetadata(): void {
const canonicalEntrypoints = new Set<string>(publicPluginSdkEntrypoints);
const metadataEntrypoints = new Set<string>(Object.keys(pluginSdkDocMetadata));
@@ -746,44 +732,54 @@ function validateMetadata(): void {
}
}
/** Write or check SDK API contract artifacts used by CI and release checks. */
export async function writePluginSdkApiBaselineArtifacts(params?: {
repoRoot?: string;
/** Compare or write an already-rendered SDK API contract. */
export async function writeRenderedPluginSdkApiBaselineArtifacts(params: {
check?: boolean;
jsonPath?: string;
statefilePath?: string;
hashPath?: string;
contractPath: string;
jsonPath: string;
rendered: PluginSdkApiBaselineRender;
}): Promise<PluginSdkApiBaselineWriteResult> {
const repoRoot = params?.repoRoot ?? resolveRepoRoot();
const jsonPath = path.resolve(repoRoot, params?.jsonPath ?? DEFAULT_JSON_OUTPUT);
const statefilePath = path.resolve(repoRoot, params?.statefilePath ?? DEFAULT_STATEFILE_OUTPUT);
const hashPath = path.resolve(repoRoot, params?.hashPath ?? DEFAULT_HASH_OUTPUT);
const rendered = await renderPluginSdkApiBaseline({ repoRoot });
const nextHashContent = computePluginSdkApiBaselineHashFileContent(rendered);
const currentHashContent = await loadCurrentFile(hashPath);
const changed = currentHashContent !== nextHashContent;
const currentContract = await loadCurrentFile(params.contractPath);
const changed = currentContract !== params.rendered.jsonl;
if (params?.check) {
if (params.check) {
return {
changed,
contractDiff: changed
? diffPluginSdkApiBaselineContract(currentContract, params.rendered.jsonl)
: null,
contractPath: params.contractPath,
wrote: false,
jsonPath,
statefilePath,
hashPath,
jsonPath: params.jsonPath,
};
}
await fs.mkdir(path.dirname(hashPath), { recursive: true });
await fs.writeFile(hashPath, nextHashContent, "utf8");
await fs.mkdir(path.dirname(jsonPath), { recursive: true });
await fs.writeFile(jsonPath, rendered.json, "utf8");
await fs.writeFile(statefilePath, rendered.jsonl, "utf8");
await fs.mkdir(path.dirname(params.contractPath), { recursive: true });
await fs.writeFile(params.contractPath, params.rendered.jsonl, "utf8");
await fs.mkdir(path.dirname(params.jsonPath), { recursive: true });
await fs.writeFile(params.jsonPath, params.rendered.json, "utf8");
return {
changed,
contractDiff: null,
contractPath: params.contractPath,
wrote: true,
jsonPath,
statefilePath,
hashPath,
jsonPath: params.jsonPath,
};
}
/** Render, then write or check SDK API contract artifacts used by CI and release checks. */
export async function writePluginSdkApiBaselineArtifacts(params?: {
repoRoot?: string;
check?: boolean;
contractPath?: string;
jsonPath?: string;
}): Promise<PluginSdkApiBaselineWriteResult> {
const repoRoot = params?.repoRoot ?? resolveRepoRoot();
return writeRenderedPluginSdkApiBaselineArtifacts({
check: params?.check,
contractPath: path.resolve(repoRoot, params?.contractPath ?? DEFAULT_CONTRACT_OUTPUT),
jsonPath: path.resolve(repoRoot, params?.jsonPath ?? DEFAULT_JSON_OUTPUT),
rendered: await renderPluginSdkApiBaseline({ repoRoot }),
});
}
+2 -2
View File
@@ -1232,7 +1232,7 @@ describe("scripts/changed-lanes", () => {
it("delegates generated docs baselines with heavy owner checks", () => {
for (const changedPath of [
"docs/.generated/plugin-sdk-api-baseline.sha256",
"docs/.generated/plugin-sdk-api-baseline.jsonl",
"docs/.generated/sqlite-session-transcript-schema-baseline.sha256",
]) {
const result = detectChangedLanes([changedPath]);
@@ -1834,7 +1834,7 @@ describe("scripts/changed-lanes", () => {
"scripts/generate-plugin-sdk-api-baseline.ts",
"scripts/lib/plugin-sdk-doc-metadata.ts",
"scripts/lib/plugin-sdk-entries.mts",
"docs/.generated/plugin-sdk-api-baseline.sha256",
"docs/.generated/plugin-sdk-api-baseline.jsonl",
]),
).toBe(true);
expect(shouldRunPluginSdkApiBaselineCheck(["docs/help/troubleshooting.md"])).toBe(false);