Author SHA1 Message Date
gamertan 1082d9d61e feat: publish the Hime-san Beta 2 LSP
Add the standard-library language server, editor-neutral protocol contract, additive version feature discovery, and bounded security regressions while leaving the Sando runtime unchanged. Material design and implementation assistance was provided by OpenAI Codex.

Signed-off-by: Cole Speelman <crspeelman@gmail.com>
2026-08-12 20:31:07 -04:00
gamertan e3a94fd16b docs: add the Beta module-cache recovery
Publish the tested non-destructive recovery for users who install the parent compiler before the nested sando runtime.

This commit is an exact sanitized export from the private development record. Material drafting and review were assisted by OpenAI Codex; Cole Speelman reviewed the change and accepts human responsibility.

Signed-off-by: Cole Speelman <crspeelman@gmail.com>
2026-08-12 15:21:42 -04:00
gamertan 532724baf7 docs: publish the v1 beta evidence
Publish the exact-candidate Windows and Linux results, signed-tag and clean-install status, provisional macOS boundary, and reliable runtime-first Beta 1 installation order. The post-publication verifier now cleans read-only module caches safely.

This commit is an exact sanitized export from the private development record. Material drafting and review were assisted by OpenAI Codex; Cole Speelman reviewed the changes and accepts human responsibility.

Signed-off-by: Cole Speelman <crspeelman@gmail.com>
2026-08-12 15:09:32 -04:00
30 changed files with 2588 additions and 195 deletions
+39 -9
View File
@@ -6,6 +6,31 @@ Sandwich Hime follows semantic versioning after final v1. Compiler and nested
runtime releases are versioned independently and listed together when they form runtime releases are versioned independently and listed together when they form
one coordinated release. one coordinated release.
## v1.0.0-beta.2 — 2026-08-12
Compiler-only release; the unchanged Apache runtime remains
`sando/v1.0.0-beta.1` with ABI `sando.v1`.
### Added
- Standard-library-only `himesan lsp --stdio` with one workspace per process,
full-document overlays, 200 ms edit debounce, cancellation, and bounded
indexing through the compiler's existing filesystem boundaries.
- Live diagnostics, trust warnings, duplicate/cycle reporting, UTF-16 LSP
positions, tag/context/component hover, document symbols, typed component
completion, and component go-to-definition.
- Additive `features: ["lsp-stdio"]` in `himesan version --json`.
- Protocol framing and malformed-input fuzzing; overlay, Unicode, CRLF, NUL,
deletion, nested-module, symlink, completion, definition, cancellation,
shutdown, no-write, and resource-limit tests.
### Boundaries
The server does not generate, run Go or project code, fetch dependencies,
access the network, start the dev supervisor, format, rename, add imports, or
delegate general Go completion to `gopls`. Generated-file freshness remains an
explicit `himesan check --json` workflow.
## v1.0.0-beta.1 — 2026-08-12 ## v1.0.0-beta.1 — 2026-08-12
This is the first installable public beta: `sando/v1.0.0-beta.1` for the This is the first installable public beta: `sando/v1.0.0-beta.1` for the
@@ -27,24 +52,29 @@ not a production-stability promise.
- Public beta support policy for evaluation and classroom use, including a - Public beta support policy for evaluation and classroom use, including a
provisional macOS lane and a community compatibility-reporting path. provisional macOS lane and a community compatibility-reporting path.
### Pre-beta verification baseline ### Release verification
Maintainer-run Linux and native Windows matrices passed on public commit The exact public Beta 1 commit
`113c95c21e57227b4675c9fda015ada59cc9e9a6` (tree `b7a84054d755e42285e50298e41e47f06a8325a5` (tree
`a2aeb4dac22853cb3894e3e487b94bbeff5051e5`) with Go 1.25.12 and Go `be9e118e38dfebed19f60403ededdadabe07d2aa`) passed maintainer-run
1.26.5. The tested golden output had the same SHA-256 on each tested host: executed Linux and native Windows matrices with Go 1.25.12 and Go 1.26.5. The
tested golden output had the same SHA-256 on each tested host:
`63fa75a3049a3a8a12d769d7f9b6b510dfe763baacf706775b75cef2c57a984f`. `63fa75a3049a3a8a12d769d7f9b6b510dfe763baacf706775b75cef2c57a984f`.
That commit is a pre-beta baseline, not evidence for the later Beta 1 commit. The complete release preflight passed, including race tests, bounded parser
The required matrix must be rerun from the exact candidate before its tags are fuzz smoke, known-vulnerability analysis of both zero-third-party-dependency
published. Native macOS execution remains pending and is explicitly provisional modules, candidate-version provenance, and six cross-builds. The signed runtime
for this beta. and compiler tags were published in that order. Fresh direct and public-proxy
runtime-first installs passed after normal proxy propagation. Native macOS
execution remains pending and is explicitly provisional for this beta.
### Known limitations ### Known limitations
- Source syntax, generated format, CLI details, and runtime API may change - Source syntax, generated format, CLI details, and runtime API may change
before final v1. before final v1.
- Native macOS behavior has not yet been maintainer-validated. - Native macOS behavior has not yet been maintainer-validated.
- In a shared fresh Go module cache, add the nested `sando` runtime before
installing the parent compiler module at the same Beta 1 version.
- Prebuilt binary artifacts, checksums, SBOMs, reproducible archives, - Prebuilt binary artifacts, checksums, SBOMs, reproducible archives,
key-recovery rehearsal, systematic browser differential, long fuzz, key-recovery rehearsal, systematic browser differential, long fuzz,
benchmark, and final compatibility gates remain work toward the release benchmark, and final compatibility gates remain work toward the release
+1 -1
View File
@@ -1 +1 @@
{"schema_version":2,"project":"sandwich-hime","export_policy":"exact-allowlist-v1","export_mode":"release","file_count":80,"allowlist_sha256":"e40a56b3431efdd99b3a78c38c387722de347216640383fc849569a817edf5c6","manifest_sha256":"93870a8c1e91602754de257c30249892821356ea9bf8d89e1b4502b62d769b06"} {"schema_version":2,"project":"sandwich-hime","export_policy":"exact-allowlist-v1","export_mode":"release","file_count":90,"allowlist_sha256":"393ee598dc7e12cdbb603887bf06599e46b40d7c19e4ff693a818cc32afb01ec","manifest_sha256":"655bfadbddf5967288147f5b1762debd3f4b106d86c044706c41cf6aabca5e00"}
+28 -18
View File
@@ -2,7 +2,7 @@
658ba4b4645426f8c3249337f47669074ae9249a31703dcd9ea4c1afec45e20b ./.gitattributes 658ba4b4645426f8c3249337f47669074ae9249a31703dcd9ea4c1afec45e20b ./.gitattributes
d5ae411fb422b2388cac220f9655900eecbc49ece961b2bb2a6610347733b756 ./.gitignore d5ae411fb422b2388cac220f9655900eecbc49ece961b2bb2a6610347733b756 ./.gitignore
98f663ab0f376b4550094465ec2e06192d1e0b0707604ec6794f20b0d10952c1 ./AI_CONTRIBUTIONS.md 98f663ab0f376b4550094465ec2e06192d1e0b0707604ec6794f20b0d10952c1 ./AI_CONTRIBUTIONS.md
8bc6c7586a58bdc648c7d3df2db33fa6db6381a111da9d8fc2d5817b5e0517ad ./CHANGELOG.md 0828545d3aa440e1ec5dce4b934de6800413f55a925988b60923c5ee9b700a4e ./CHANGELOG.md
b696cab3cf482ff5737501371cca749369b119351383e698ced42bcdbcbfc8ae ./CLA.md b696cab3cf482ff5737501371cca749369b119351383e698ced42bcdbcbfc8ae ./CLA.md
797e884105738fc931b585b695424f43ec5f296d8ab9bba5191b096e87a9e2c2 ./CONTRIBUTING.md 797e884105738fc931b585b695424f43ec5f296d8ab9bba5191b096e87a9e2c2 ./CONTRIBUTING.md
86d7e49d5d90e0f98a4ad0f14b5d8b9f11ed09a1e29ecdf27388316b28e195e8 ./COPYRIGHT 86d7e49d5d90e0f98a4ad0f14b5d8b9f11ed09a1e29ecdf27388316b28e195e8 ./COPYRIGHT
@@ -13,35 +13,38 @@ a4570d054f072d33b8f17b0c8b162a6ee0ca37d7df2b1aee7e4b728ab350a892 ./GOVERNANCE.m
47d857e49f89596bac9b09fc8ca57a668a33d01e2b51508acfc92ed321cdc27f ./LICENSES.md 47d857e49f89596bac9b09fc8ca57a668a33d01e2b51508acfc92ed321cdc27f ./LICENSES.md
b6aa08e5ccaec3c5dccdc19d7cd7f54a70adae4d57966263c7aa353c7ba70e08 ./MAINTAINERS.md b6aa08e5ccaec3c5dccdc19d7cd7f54a70adae4d57966263c7aa353c7ba70e08 ./MAINTAINERS.md
6638db2f1fba831c79de835ce95c847a5b36c5b5c693b99a28655b2d096cc440 ./OUTPUT_EXCEPTION.md 6638db2f1fba831c79de835ce95c847a5b36c5b5c693b99a28655b2d096cc440 ./OUTPUT_EXCEPTION.md
dda0ffee17bc1586ac16cc9707999cde2909a116c98cd43aeab85e0d3da7b636 ./README.md c3ac873ae2e6248e1d86dd542a11557b24b8dba80e4785f3bc1018152020235c ./README.md
9895823f9f7ba88ddf048c53c23ed32298b151ab049a9c4470366389d9498ff4 ./RELEASE.md 2751674c180f15a42c1d2b40cf149be4138aa6cf247d7be176f1f0c468103c24 ./RELEASE.md
6d1e49dd72ba9592a3fd0562fcca1857803a3dd86dc097b36043e20f5d5cd591 ./ROADMAP.md 209decb6769646eb2f58e312fbcd9c497c26234f3d3115bae3f20493b8178584 ./ROADMAP.md
50a24995b39a957e47e0e181f8771a87141302b4d308fa8d8bfdc2e70121c2b9 ./SECURITY.md 0fef473ac46b71215d1eb7922da4594210ffbbb8bb2dedd5e531fb3bd09396e1 ./SECURITY.md
53bd6eda804d6b782bdb07115ec197c890813cf2d5d0125dfe8f47f5f92f75b0 ./SPEC.md 53bd6eda804d6b782bdb07115ec197c890813cf2d5d0125dfe8f47f5f92f75b0 ./SPEC.md
3d9e680cdfe147df7cc9ff29ecf1d3e566e9cd559ae84db4880e559b9c7c7205 ./TRADEMARKS.md 3d9e680cdfe147df7cc9ff29ecf1d3e566e9cd559ae84db4880e559b9c7c7205 ./TRADEMARKS.md
136a6d82db842547b342f8b0c9ffdc7c04f7c9b473b4ef6dca9dbc940cb24b54 ./cmd/himesan/main.go 8cd8db68e1300f9b78cc7235855853cbc7aeb499a921419e23a22e4d22826fcb ./cmd/himesan/main.go
3011a435aaeb572d34e2d2b582865fd63268c92a9ae3f0ae1a56d1a59d83cd43 ./cmd/himesan/main_test.go 0709990e28d58d50e49219690e435338b269284efb6c2d2e2c95ed6b242580df ./cmd/himesan/main_test.go
1ecbba46f8b1b2d548a01d7e98afae17b2dd17a814338ff1f88db885655d1c07 ./docs/ARCHITECTURE.md 1ecbba46f8b1b2d548a01d7e98afae17b2dd17a814338ff1f88db885655d1c07 ./docs/ARCHITECTURE.md
9c598559a89fa4a9bdd2311bd1ed8330992d0a0f74ec8b29ac151fc0ff8fef16 ./docs/BENCHMARKS.md 9c598559a89fa4a9bdd2311bd1ed8330992d0a0f74ec8b29ac151fc0ff8fef16 ./docs/BENCHMARKS.md
5c3a62fed80ca28d56558b8c75e8b5be8ba7d2554127adf4609d96da314e85b0 ./docs/BRAND.md 5c3a62fed80ca28d56558b8c75e8b5be8ba7d2554127adf4609d96da314e85b0 ./docs/BRAND.md
4f7b04b3f74a2e90fc69019cad78a1287f4806ae84e7207cfdf000971702395d ./docs/COMPATIBILITY.md 2b815d3b815b8d338560183c6f0af46f761c2465309783c93870b8ac8d022d03 ./docs/COMPATIBILITY.md
5f4ac209a16ab110baeaa64a40c19d9239c903e17550c3f05e1e1473ddcc33a3 ./docs/DEVELOPMENT_SERVER.md 5f4ac209a16ab110baeaa64a40c19d9239c903e17550c3f05e1e1473ddcc33a3 ./docs/DEVELOPMENT_SERVER.md
51aa57a81131b64f76c45552122de842f22be92d81c8bba8f6fd38a18a7670d6 ./docs/DIAGNOSTICS.md 51aa57a81131b64f76c45552122de842f22be92d81c8bba8f6fd38a18a7670d6 ./docs/DIAGNOSTICS.md
9bd43853d91b841c4879dac94dda5aafefa25bcdf4a02763445e2506fc618b03 ./docs/SECURITY_EVIDENCE.md a62cc7174f3c92d8ef77e4bd9607fbf5d4b80bc514ff05bd433c02a9b0578f18 ./docs/LANGUAGE_SERVER.md
965a6ae57a8162c3af81f4987771e88617247f903088d210720ea2afab152cc9 ./docs/SECURITY_EVIDENCE.md
d969c7b5486ee93e54232fd69d9db06f3b4dc1bba63001596ec48545073c2680 ./docs/THREAT_MODEL.md d969c7b5486ee93e54232fd69d9db06f3b4dc1bba63001596ec48545073c2680 ./docs/THREAT_MODEL.md
82107c57043af40b9e3ec03f4ed9efddcf3bcdda1765b99ec413f835be4a46e3 ./docs/V1_RELEASE_PLAN.md 738258ba8f7e5ffea67d3f00eb70839590171971a9946a55b013ca95baf7aafb ./docs/V1_RELEASE_PLAN.md
f27c46ca63707bb8cc570eab1ea521824e94bc59b1d153998a5e91c2c7340d16 ./go.mod f27c46ca63707bb8cc570eab1ea521824e94bc59b1d153998a5e91c2c7340d16 ./go.mod
ca0bf5051d356d2602f46201fb1637ce48b629ad42161877eec13f743f215dc5 ./internal/compiler/abi_test.go ca0bf5051d356d2602f46201fb1637ce48b629ad42161877eec13f743f215dc5 ./internal/compiler/abi_test.go
d891b9b075617050471b2ca34de73d926aaebde4ec638a5039b0d5001d3172f4 ./internal/compiler/analysis.go
c123bf8c8fd847962e147041172b71fef7e852819ccd7b27d4648748e02d522f ./internal/compiler/analysis_test.go
6ef6a0f15a5aca1c8708cbf24218372e1fca9c6fead1a5a75d261faa69651af7 ./internal/compiler/backend.go 6ef6a0f15a5aca1c8708cbf24218372e1fca9c6fead1a5a75d261faa69651af7 ./internal/compiler/backend.go
4d309392a7739b26e41cd9d9db71b52077bb8198573634ddf793ad531acc81cf ./internal/compiler/compiler_test.go 4d309392a7739b26e41cd9d9db71b52077bb8198573634ddf793ad531acc81cf ./internal/compiler/compiler_test.go
dbba23e360bd6dd1e8f42953a49a7cfcc241aa3ac76f5ce505ec8f8558833c84 ./internal/compiler/context.go dbba23e360bd6dd1e8f42953a49a7cfcc241aa3ac76f5ce505ec8f8558833c84 ./internal/compiler/context.go
b2a96ef1ad572ad9cd0e9247328ca261de6f9f3689da41e3f3e111d405a6dee6 ./internal/compiler/diagnostic.go b2a96ef1ad572ad9cd0e9247328ca261de6f9f3689da41e3f3e111d405a6dee6 ./internal/compiler/diagnostic.go
45562a41ef9ab1116746e4962ce8f93d4d8651e1e468a38122c626f8a34a2874 ./internal/compiler/discover.go 6a3dccd0c7caf92fe2690b350f1db6f34037609b1fff6d3e68e0e229241950e5 ./internal/compiler/discover.go
50a8bad11deea99faf0c7c9de1266bc086b7a1247ff9dd275ffedfc69a3be81d ./internal/compiler/e2e_test.go 50a8bad11deea99faf0c7c9de1266bc086b7a1247ff9dd275ffedfc69a3be81d ./internal/compiler/e2e_test.go
eefb05a35bd07660a293c8af97949cd6f69a22709728f3fe2cc9132b863b7d5a ./internal/compiler/fuzz_test.go eefb05a35bd07660a293c8af97949cd6f69a22709728f3fe2cc9132b863b7d5a ./internal/compiler/fuzz_test.go
d166096f185d76b2698aa3ab3251f00e58f84cdedc3af667e88ddd528ca0cb76 ./internal/compiler/model.go 440580fcd844b2c80b1f4dddc4d83db82974e203cca298387a190e53e6a524f3 ./internal/compiler/model.go
01516c0330b14a1970019690d8efcc9004d2dcfa59e82fef1b67f98005b67532 ./internal/compiler/operation.go 01516c0330b14a1970019690d8efcc9004d2dcfa59e82fef1b67f98005b67532 ./internal/compiler/operation.go
d7d8181455d5f37ef9bcc6bdbf86e0630f20e8a5b3b81688d12742687b434c99 ./internal/compiler/parse.go 1f633caa019d8de9ae5bbad27e29679fcc01b3cb9a33d7d616be07140e06f38b ./internal/compiler/parse.go
80cf170514a3b955d24440cb086d34e19f3a305510e3c5db95cb897be91f922a ./internal/compiler/replace_unix.go 80cf170514a3b955d24440cb086d34e19f3a305510e3c5db95cb897be91f922a ./internal/compiler/replace_unix.go
0fff1c67447bf5353ed1df6e7dfc4b14581b67adc1bf02f7a4a7c1f2680c392f ./internal/compiler/replace_windows.go 0fff1c67447bf5353ed1df6e7dfc4b14581b67adc1bf02f7a4a7c1f2680c392f ./internal/compiler/replace_windows.go
f4ba01010ed5f5ba1e979702d82e95312bc0a4b13cc205c098926839be4ecb73 ./internal/compiler/testdata/golden/basic.sando f4ba01010ed5f5ba1e979702d82e95312bc0a4b13cc205c098926839be4ecb73 ./internal/compiler/testdata/golden/basic.sando
@@ -58,11 +61,18 @@ c0f76ef5c14b0a28ed1e68d8d518102ffcbf067285b087eed4d13cd3c87b0e00 ./internal/dev
7f1efbefea3a277f0f4d96a29219293efd78d9dc44823c09b9667b19d5042047 ./internal/devserver/proxy.go 7f1efbefea3a277f0f4d96a29219293efd78d9dc44823c09b9667b19d5042047 ./internal/devserver/proxy.go
aebf8388576d7bc9b047ceedf8a893acb3ace5fe16f44cb883efe63eef072ef9 ./internal/devserver/proxy_test.go aebf8388576d7bc9b047ceedf8a893acb3ace5fe16f44cb883efe63eef072ef9 ./internal/devserver/proxy_test.go
e6561e693138a3b77be06c1a98999e71494bbca0d0c72ccb9bff57b8e8575c0f ./internal/devserver/supervisor.go e6561e693138a3b77be06c1a98999e71494bbca0d0c72ccb9bff57b8e8575c0f ./internal/devserver/supervisor.go
b94103cd4b582968cdb0b61b0164f57ade006fa4e5187fcaa05944274192526a ./internal/devserver/supervisor_test.go 5c0ce1eedb08c0381dd048526e9f781a2ac8501cc1142648c43d4aa07ca62299 ./internal/devserver/supervisor_test.go
e0a682c0153bf4f2a1f26cc6095d7893ad96e6199cbe76d0150785fc996f1141 ./internal/devserver/watch.go e0a682c0153bf4f2a1f26cc6095d7893ad96e6199cbe76d0150785fc996f1141 ./internal/devserver/watch.go
b7a7fabf9a6c497f7ac2262628c5fb37a6bd00da676e1b7d5088d5f649c9f14c ./internal/devserver/watch_test.go b7a7fabf9a6c497f7ac2262628c5fb37a6bd00da676e1b7d5088d5f649c9f14c ./internal/devserver/watch_test.go
76609ee5c57b1c174076689d4953e6ec5ef914ca114c27f1ca1d6e7fcabbddcf ./internal/lsp/features.go
edd377bca36a04ae76620a146f986e25c954fd137bab0b0e49084dbcde216ce7 ./internal/lsp/protocol.go
73b2ff8d6fd8fe44abec287384f0051520fe9564a50b4764538474f336624eee ./internal/lsp/protocol_test.go
2a2151a0211a5048c3bc89f9a2901a3c208609f483e99144f3837f87941efff9 ./internal/lsp/security_test.go
3be838b4bb0b618f46c0eab40788a1e03121da25644dedfa7542bd0fa64571e1 ./internal/lsp/server.go
c919b132373d9ab715cdaf057789a3d23f27b1c9f0eded14c06d04d2a4c86ee3 ./internal/lsp/server_test.go
4c5ade5f649dffdd0a1b0a350c15488fc601b7eba582386f39e64412387b5c91 ./internal/lsp/workspace.go
d8c6f37c94ef426fc2d95c82331265f7d700d2e2a23100ad78c92849280ff6d8 ./internal/version/version.go d8c6f37c94ef426fc2d95c82331265f7d700d2e2a23100ad78c92849280ff6d8 ./internal/version/version.go
9b615038a1178eedbcab299d90548e259ac34d2f5c51deadd79ed16f51ef6911 ./internal/version/version_test.go a1ee0ec44dc45f01a3357cf93e13a81587af7f061497c25df374e2a88231f3b7 ./internal/version/version_test.go
e8a3026ec920d7312f843e2001e50ae4e34fd1ba5f9b2ae25a6113de1fa88385 ./sando/COPYRIGHT e8a3026ec920d7312f843e2001e50ae4e34fd1ba5f9b2ae25a6113de1fa88385 ./sando/COPYRIGHT
c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4 ./sando/LICENSE c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4 ./sando/LICENSE
b4a7bffe678a97209881e07989563a5085aa0ead9e1b67306087dac6b97bad70 ./sando/README.md b4a7bffe678a97209881e07989563a5085aa0ead9e1b67306087dac6b97bad70 ./sando/README.md
@@ -72,9 +82,9 @@ ff76daee5b642ad84af31701833246d68b54d09580192312d750a7a2e893a692 ./sando/go.mod
80ff53787919e809b8085d6ad9c3e183c9c7c1d74cfeda73369ac5c4607c236f ./sando/trust.go 80ff53787919e809b8085d6ad9c3e183c9c7c1d74cfeda73369ac5c4607c236f ./sando/trust.go
85621a44c730582f4410ac2c70418b739fb55e916f7e6b73a1a619982c459572 ./sando/write.go 85621a44c730582f4410ac2c70418b739fb55e916f7e6b73a1a619982c459572 ./sando/write.go
b188917e258890e6b6e4840a6fd946fc9a77cabc2068da3764f221e4a6a5df97 ./sando/write_test.go b188917e258890e6b6e4840a6fd946fc9a77cabc2068da3764f221e4a6a5df97 ./sando/write_test.go
504897b29686e0ea7adff8beb8ec91612df3ee169397309c3b6b69eb0393491b ./scripts/README.md c4a161faba46ce5b508c0788078256a520277a573a3ace0e85ae0c26b16d298b ./scripts/README.md
0bc796f71c863aa898674a26c56f055e3d81cf20629ca7b32fbae87d8841e0a8 ./scripts/check-licenses.sh 0bc796f71c863aa898674a26c56f055e3d81cf20629ca7b32fbae87d8841e0a8 ./scripts/check-licenses.sh
6c73ad46beb642836ae4d462f40e7ecc8d86a3cc194e71a5d0859dac73af0410 ./scripts/release-check.sh 1b003062799b99bfe271b47438397a8cce5875c60c982a0117eb11c3babcadf0 ./scripts/release-check.sh
9cd43005a7d0f3659b11c5c14e4e0b9e7f675b695da185f4ee97c54edebf0dc6 ./scripts/verify-public-install.sh 78a64c7fb3a039b15a1d08b4c0b873952852287a07f670247b081e59dbb09a30 ./scripts/verify-public-install.sh
24ed3c9a1d37e46a856cbbd68e5c58ae04c6c9852902b99ed675e1f428339a9f ./scripts/verify.ps1 24ed3c9a1d37e46a856cbbd68e5c58ae04c6c9852902b99ed675e1f428339a9f ./scripts/verify.ps1
f0cbd86759fa729064cb1c69991db2ac291792dadb6b1e1ba83794f2e390404d ./scripts/verify.sh f0cbd86759fa729064cb1c69991db2ac291792dadb6b1e1ba83794f2e390404d ./scripts/verify.sh
+32 -12
View File
@@ -42,30 +42,41 @@ request object, or production server.
## Status ## Status
`v1.0.0-beta.1` is the first installable public beta. It is supported for `v1.0.0-beta.2` is the current compiler beta. It is supported for
learning, classroom projects, evaluation, and compatibility feedback. It is a learning, classroom projects, evaluation, and compatibility feedback. It is a
semantic-version prerelease: source syntax, generated output, the runtime API, semantic-version prerelease: source syntax, generated output, the runtime API,
and CLI behavior may change before final v1, and this beta is not recommended and CLI behavior may change before final v1, and this beta is not recommended
for production deployment. for production deployment.
Maintainer-run testing has established a pre-beta baseline on native Windows The exact Beta 1 source passed maintainer-run native Windows and executed Linux
and on Linux with Go 1.25 and Go 1.26. That matrix must pass again on the exact matrices with Go 1.25.12 and Go 1.26.5. Native macOS validation is still
Beta 1 commit before its tags are published. Native macOS validation is still
pending, so macOS support is provisional in this beta. Mac learners and Go pending, so macOS support is provisional in this beta. Mac learners and Go
developers are warmly invited to try it and share their macOS version, developers are warmly invited to try it and share their macOS version,
architecture, Go version, command, and smallest useful reproduction. Community architecture, Go version, command, and smallest useful reproduction. Community
reports broaden the evidence; maintainers remain responsible for security reports broaden the evidence; maintainers remain responsible for security
review, triage, fixes, and release decisions. review, triage, fixes, and release decisions.
Install the beta compiler: Inside an application module, add the small runtime first:
```sh ```sh
go install gamertan.com/sandwich-hime/cmd/himesan@v1.0.0-beta.1 go get gamertan.com/sandwich-hime/sando@v1.0.0-beta.1
``` ```
Add the small runtime to an application module: Then install the current beta compiler:
```sh ```sh
go install gamertan.com/sandwich-hime/cmd/himesan@v1.0.0-beta.2
```
Keep that runtime-first order. It avoids path-selection ambiguity between the
parent compiler module and its independently tagged nested runtime.
If the compiler was installed first and `go get` reports that the parent module
does not contain `sando`, seed the exact nested module without clearing the
global cache, then retry:
```sh
go mod download gamertan.com/sandwich-hime/sando@v1.0.0-beta.1
go get gamertan.com/sandwich-hime/sando@v1.0.0-beta.1 go get gamertan.com/sandwich-hime/sando@v1.0.0-beta.1
``` ```
@@ -73,13 +84,14 @@ For a reproducible one-off or classroom invocation that does not depend on the
learner's `PATH`: learner's `PATH`:
```sh ```sh
go run gamertan.com/sandwich-hime/cmd/himesan@v1.0.0-beta.1 --help go run gamertan.com/sandwich-hime/cmd/himesan@v1.0.0-beta.2 --help
``` ```
The runtime is released first as `sando/v1.0.0-beta.1`; the compiler follows as The unchanged runtime remains `sando/v1.0.0-beta.1`; compiler versions are
`v1.0.0-beta.1`. If a newly announced version is not immediately available independent. Beta 2 adds `himesan lsp --stdio` without changing the runtime ABI.
through a module proxy, retry after the proxy has discovered the immutable tag Signed compiler tags, direct fetching, the public Go proxy, and the checksum
or use the canonical Gitea release instructions. database are verified after publication. A newly announced version may still
need a short propagation interval before every proxy sees its immutable tag.
For repository development: For repository development:
@@ -97,6 +109,14 @@ and serves it through `http://127.0.0.1:7331` with local-only reload
diagnostics. That is a Cole-shaped convenience, not a production server or a diagnostics. That is a Cole-shaped convenience, not a production server or a
requirement. Take the paved path—or don't. requirement. Take the paved path—or don't.
Beta 2 also provides a standard, editor-neutral language server. It analyzes
unsaved overlays with the compiler's real parser and context model, but never
generates, runs Go, executes a project, fetches a module, accesses the network,
or starts the dev supervisor. See
[the language-server contract](docs/LANGUAGE_SERVER.md). The portable Agent
Skill and VS Code preview live in the separate
[tooling repository](https://gitea.speelman.ca/gamertan/sandwich-hime-tooling).
Final-v1 installs will use the same paths with `@v1.0.0`. A version is Final-v1 installs will use the same paths with `@v1.0.0`. A version is
advertised as available only after its immutable tags, `gamertan.com` advertised as available only after its immutable tags, `gamertan.com`
metadata, and clean direct-fetch installation have been verified. metadata, and clean direct-fetch installation have been verified.
+42 -10
View File
@@ -68,14 +68,45 @@ Before `sando/v1.0.0-beta.1` and `v1.0.0-beta.1` are created:
generated fixtures are current under that exact binary. generated fixtures are current under that exact binary.
7. Create signed annotated tags and publish the runtime tag first, then the 7. Create signed annotated tags and publish the runtime tag first, then the
compiler tag, from the same reviewed commit. compiler tag, from the same reviewed commit.
8. Verify both documented installs from fresh `GOPROXY=direct` and public-proxy 8. Verify both documented runtime-first installs from fresh `GOPROXY=direct`
caches. Record propagation delays as delays, not test passes. and public-proxy caches. Record propagation delays as delays, not test
passes.
The passing public commit Beta 1 was published from public commit
`113c95c21e57227b4675c9fda015ada59cc9e9a6` (tree `b7a84054d755e42285e50298e41e47f06a8325a5` (tree
`a2aeb4dac22853cb3894e3e487b94bbeff5051e5`) is only the pre-beta `be9e118e38dfebed19f60403ededdadabe07d2aa`) after its exact-candidate
platform baseline. Any documentation, versioning, or code change produces a new matrix passed. Future prereleases require their own candidate evidence; this
candidate and requires the candidate matrix to run again before tagging. result cannot be relabeled for another commit.
## Beta 2 compiler publication gates
`v1.0.0-beta.2` is compiler-only. Do not create or move a runtime tag when the
`sando` subtree is byte-identical to `sando/v1.0.0-beta.1`.
Run its technical preflight with the retained runtime made explicit:
```sh
./scripts/release-check.sh \
--version v1.0.0-beta.2 \
--runtime-tag sando/v1.0.0-beta.1
```
In addition to every Beta 1 compiler/security/determinism gate:
1. verify protocol framing, malformed messages, shutdown, request
cancellation, UTF-16 positions, CRLF/NUL handling, bounded fuzz smoke,
overlays, source appearance/deletion, duplicates, cycles, symlink and
nested-module boundaries, completion scope, and component definitions;
2. prove the language-server package does not write, execute project code,
invoke Go, fetch, access the network, or start the development supervisor;
3. run the exact candidate on supported Go lines under executed Linux and
native Windows, with native macOS status stated explicitly;
4. build an exact version-stamped candidate and assert the additive
`features: ["lsp-stdio"]` JSON identity;
5. publish a signed annotated compiler tag only after the reviewed sanitized
public commit is current; and
6. verify direct, public-proxy, and checksum-database installation without
altering `v1.0.0-beta.1` or `sando/v1.0.0-beta.1`.
## RC and final gates ## RC and final gates
@@ -110,9 +141,10 @@ metadata exist, run:
scripts/verify-public-install.sh --version vX.Y.Z scripts/verify-public-install.sh --version vX.Y.Z
``` ```
That check exercises the documented `go install` and `go get` commands from That check adds the nested runtime before installing the parent compiler, then
fresh direct-fetch and public-proxy caches. It is separate from the pre-tag, exercises both commands from fresh direct-fetch and public-proxy caches. The
read-only `scripts/release-check.sh`. order avoids the Go module-cache ambiguity documented for Beta 1. It is
separate from the pre-tag, read-only `scripts/release-check.sh`.
Release notes report hardware, commit, datasets, commands, `ns/op`, Release notes report hardware, commit, datasets, commands, `ns/op`,
allocations, response latency, and methodology for any performance claim. allocations, response latency, and methodology for any performance claim.
+4 -3
View File
@@ -17,11 +17,12 @@ it is not a production-stability promise.
- [x] Establish a public pre-beta Linux/Windows matrix on Go 1.25 and Go 1.26. - [x] Establish a public pre-beta Linux/Windows matrix on Go 1.25 and Go 1.26.
- [x] Document macOS as provisional and invite useful community reports while - [x] Document macOS as provisional and invite useful community reports while
retaining maintainer responsibility for security and releases. retaining maintainer responsibility for security and releases.
- [ ] Rerun all required Windows/Linux checks and deterministic generation on - [x] Rerun all required Windows/Linux checks and deterministic generation on
the exact Beta 1 candidate. the exact Beta 1 candidate.
- [ ] Publish immutable `sando/v1.0.0-beta.1`, then - [x] Publish immutable `sando/v1.0.0-beta.1`, then
`v1.0.0-beta.1`, from the reviewed public commit. `v1.0.0-beta.1`, from the reviewed public commit.
- [ ] Verify clean direct and public-proxy installs after publication. - [x] Verify clean runtime-first direct and public-proxy installs after
publication.
- [ ] Complete native macOS maintainer validation. This is an RC/final gate, - [ ] Complete native macOS maintainer validation. This is an RC/final gate,
not a Beta 1 gate. not a Beta 1 gate.
+12 -5
View File
@@ -2,10 +2,11 @@
# Security policy # Security policy
Sandwich Hime `v1.0.0-beta.1` is supported within a deliberately narrow Sandwich Hime compiler `v1.0.0-beta.2` and runtime
scope: learning, classroom projects, evaluation, and compatibility testing. `sando/v1.0.0-beta.1` are supported within a deliberately narrow scope:
learning, classroom projects, evaluation, and compatibility testing.
Security reports are welcome and receive best-effort maintainer assessment and Security reports are welcome and receive best-effort maintainer assessment and
fixes while Beta 1 is the current prerelease. This is not production support, fixes while this pair is current. This is not production support,
an SLA, a fitness guarantee, or a promise that a fix will preserve beta APIs. an SLA, a fitness guarantee, or a promise that a fix will preserve beta APIs.
The community is invited to help find compatibility gaps, especially on macOS. The community is invited to help find compatibility gaps, especially on macOS.
@@ -17,7 +18,7 @@ advisories, and release decisions.
| Version | Security status | | Version | Security status |
| --- | --- | | --- | --- |
| `v1.0.0-beta.1` and `sando/v1.0.0-beta.1` | Current evaluation/classroom prerelease once published; best-effort security assessment and fixes; interfaces may change | | `v1.0.0-beta.2` compiler and `sando/v1.0.0-beta.1` runtime | Current evaluation/classroom prerelease pair; best-effort security assessment and fixes; interfaces may change |
| Public `main` | Development source; reports welcome, but no compatibility or production-support promise | | Public `main` | Development source; reports welcome, but no compatibility or production-support promise |
| Older prereleases | Superseded when a newer prerelease or final version is published; reports are still triaged to determine affected versions | | Older prereleases | Superseded when a newer prerelease or final version is published; reports are still triaged to determine affected versions |
@@ -133,7 +134,13 @@ an independent security audit, certification, or formal verification. Coverage
percentages, passing scanners, and a clean vulnerability database result are percentages, passing scanners, and a clean vulnerability database result are
evidence of specific checks—not proof that no vulnerability exists. evidence of specific checks—not proof that no vulnerability exists.
Beta 1 publication requires signed annotated source tags, but may precede the The Beta 2 language server is read-only and standard-library-only. Its tests
exercise bounded framing, malformed messages, cancellation, UTF-16 positions,
unsaved overlays, workspace re-indexing, and no generated writes. Source review
forbids process execution and network-client imports inside the server. Editor
workspace trust and executable resolution remain editor-client responsibilities.
Beta 1 uses signed annotated source tags, but precedes the
complete prebuilt-artifact and key-recovery system. Signed binaries, checksums, complete prebuilt-artifact and key-recovery system. Signed binaries, checksums,
an SBOM, reproducible archives, and complete source/build provenance are an SBOM, reproducible archives, and complete source/build provenance are
release-candidate and final-v1 gates. Their absence from a source-only beta must release-candidate and final-v1 gates. Their absence from a source-only beta must
+27 -1
View File
@@ -18,6 +18,7 @@ import (
"gamertan.com/sandwich-hime/internal/compiler" "gamertan.com/sandwich-hime/internal/compiler"
"gamertan.com/sandwich-hime/internal/devserver" "gamertan.com/sandwich-hime/internal/devserver"
"gamertan.com/sandwich-hime/internal/lsp"
"gamertan.com/sandwich-hime/internal/version" "gamertan.com/sandwich-hime/internal/version"
) )
@@ -38,6 +39,8 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
return runCompilerCommand(ctx, args[0], args[1:], stdout, stderr, compiler.Check) return runCompilerCommand(ctx, args[0], args[1:], stdout, stderr, compiler.Check)
case "dev": case "dev":
return runDev(ctx, args[1:], stdout, stderr) return runDev(ctx, args[1:], stdout, stderr)
case "lsp":
return runLSP(ctx, args[1:], os.Stdin, stdout, stderr)
case "version": case "version":
return runVersion(args[1:], stdout, stderr) return runVersion(args[1:], stdout, stderr)
case "help", "-h", "--help": case "help", "-h", "--help":
@@ -119,7 +122,8 @@ func runVersion(args []string, stdout, stderr io.Writer) int {
Compiler string `json:"compiler"` Compiler string `json:"compiler"`
RuntimeABI string `json:"runtime_abi"` RuntimeABI string `json:"runtime_abi"`
Go string `json:"go"` Go string `json:"go"`
}{Compiler: version.Compiler, RuntimeABI: version.RuntimeABI, Go: runtime.Version()} Features []string `json:"features"`
}{Compiler: version.Compiler, RuntimeABI: version.RuntimeABI, Go: runtime.Version(), Features: []string{"lsp-stdio"}}
if *jsonOutput { if *jsonOutput {
if err := json.NewEncoder(stdout).Encode(information); err != nil { if err := json.NewEncoder(stdout).Encode(information); err != nil {
fmt.Fprintf(stderr, "himesan: encode version: %v\n", err) fmt.Fprintf(stderr, "himesan: encode version: %v\n", err)
@@ -131,6 +135,27 @@ func runVersion(args []string, stdout, stderr io.Writer) int {
return 0 return 0
} }
func runLSP(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
flags := flag.NewFlagSet("lsp", flag.ContinueOnError)
flags.SetOutput(stderr)
stdio := flags.Bool("stdio", false, "serve Language Server Protocol JSON-RPC over stdin/stdout")
if err := flags.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return 0
}
return 2
}
if !*stdio || flags.NArg() != 0 {
fmt.Fprintln(stderr, "himesan lsp requires exactly --stdio")
return 2
}
if err := lsp.Run(ctx, lsp.Options{Input: stdin, Output: stdout, LogOutput: stderr}); err != nil {
fmt.Fprintf(stderr, "himesan lsp: %v\n", err)
return 1
}
return 0
}
type stringList []string type stringList []string
func (values *stringList) String() string { return strings.Join(*values, ",") } func (values *stringList) String() string { return strings.Join(*values, ",") }
@@ -312,6 +337,7 @@ func printHelp(output io.Writer) {
fmt.Fprintln(output, " himesan check [--json] [paths...] validate sources and committed output without writes") fmt.Fprintln(output, " himesan check [--json] [paths...] validate sources and committed output without writes")
fmt.Fprintln(output, " himesan bless [--json] [paths...] friendly read-only alias for check") fmt.Fprintln(output, " himesan bless [--json] [paths...] friendly read-only alias for check")
fmt.Fprintln(output, " himesan dev [flags] [package] [-- app-args...] run the loopback last-good supervisor") fmt.Fprintln(output, " himesan dev [flags] [package] [-- app-args...] run the loopback last-good supervisor")
fmt.Fprintln(output, " himesan lsp --stdio run the read-only language server")
fmt.Fprintln(output, " himesan version [--json] print compiler and runtime ABI versions") fmt.Fprintln(output, " himesan version [--json] print compiler and runtime ABI versions")
fmt.Fprintln(output) fmt.Fprintln(output)
fmt.Fprintln(output, "Templates use .sando; .san remains exclusively San language source.") fmt.Fprintln(output, "Templates use .sando; .san remains exclusively San language source.")
+12 -2
View File
@@ -30,14 +30,24 @@ func TestRunHelpVersionAndUnknownCommand(t *testing.T) {
if code := run(context.Background(), []string{"version", "--json"}, &stdout, &stderr); code != 0 { if code := run(context.Background(), []string{"version", "--json"}, &stdout, &stderr); code != 0 {
t.Fatalf("version exit code = %d: %s", code, stderr.String()) t.Fatalf("version exit code = %d: %s", code, stderr.String())
} }
var versionResult map[string]string var versionResult struct {
Compiler string `json:"compiler"`
RuntimeABI string `json:"runtime_abi"`
Features []string `json:"features"`
}
if err := json.Unmarshal(stdout.Bytes(), &versionResult); err != nil { if err := json.Unmarshal(stdout.Bytes(), &versionResult); err != nil {
t.Fatalf("version JSON: %v", err) t.Fatalf("version JSON: %v", err)
} }
if versionResult["compiler"] == "" || versionResult["runtime_abi"] != compiler.RuntimeABI { if versionResult.Compiler == "" || versionResult.RuntimeABI != compiler.RuntimeABI || len(versionResult.Features) != 1 || versionResult.Features[0] != "lsp-stdio" {
t.Fatalf("version result = %#v", versionResult) t.Fatalf("version result = %#v", versionResult)
} }
stdout.Reset()
stderr.Reset()
if code := run(context.Background(), []string{"lsp"}, &stdout, &stderr); code != 2 {
t.Fatalf("lsp without --stdio exit code = %d, want 2", code)
}
stdout.Reset() stdout.Reset()
stderr.Reset() stderr.Reset()
if code := run(context.Background(), []string{"rebuke"}, &stdout, &stderr); code != 2 { if code := run(context.Background(), []string{"rebuke"}, &stdout, &stderr); code != 2 {
+11 -11
View File
@@ -2,9 +2,9 @@
# Compatibility policy # Compatibility policy
## Beta 1 contract ## Beta contract
`v1.0.0-beta.1` and `sando/v1.0.0-beta.1` are semantic-version Compiler `v1.0.0-beta.2` and runtime `sando/v1.0.0-beta.1` are semantic-version
prereleases. They are supported for learning, classroom projects, evaluation, prereleases. They are supported for learning, classroom projects, evaluation,
and compatibility testing. Before final v1, source syntax, generated output, and compatibility testing. Before final v1, source syntax, generated output,
the runtime API, CLI behavior, diagnostics, and configuration may change the runtime API, CLI behavior, diagnostics, and configuration may change
@@ -32,29 +32,29 @@ payloads before final v1, or hand-edited generated files.
## Go and platform support ## Go and platform support
Beta 1 targets Go 1.25 and Go 1.26. Support is based on point-in-time, The current beta targets Go 1.25 and Go 1.26. Support is based on point-in-time,
maintainer-run release matrices, not an implication of continuous CI coverage. maintainer-run release matrices, not an implication of continuous CI coverage.
A Go support change is announced in release notes before it takes effect. A Go support change is announced in release notes before it takes effect.
The current public evidence is a **pre-beta baseline** on commit The current public evidence is the exact Beta 1 source at commit
`113c95c21e57227b4675c9fda015ada59cc9e9a6` (tree `b7a84054d755e42285e50298e41e47f06a8325a5` (tree
`a2aeb4dac22853cb3894e3e487b94bbeff5051e5`): `be9e118e38dfebed19f60403ededdadabe07d2aa`):
| Platform | Go lanes | Maintainer-run result | | Platform | Go lanes | Maintainer-run result |
| --- | --- | --- | | --- | --- | --- |
| Windows 11/amd64 on NTFS | 1.25.12, 1.26.5 | Native tests, race, vet, builds, generation, process cleanup, watcher boundaries, and temporary consumer compilation passed; privileged symlink and POSIX-only permission cases were not exercised | | Windows 11/amd64 on NTFS | 1.25.12, 1.26.5 | Native tests, race, vet, builds, generation, process cleanup, watcher boundaries, and temporary consumer compilation passed; privileged symlink and POSIX-only permission cases were not exercised |
| Linux/amd64 on WSL2 with an ext4 checkout | 1.25.12, 1.26.5 | Tests, race, vet, builds, generation, focused filesystem/development cases, and license checks passed | | Linux/amd64 on WSL2 with an ext4 checkout | 1.25.12, 1.26.5 | Tests, race, vet, builds, generation, focused filesystem/development cases, and license checks passed |
| Linux/amd64 in isolated containers on a Linux server | 1.25.12, 1.26.5 | Tests, race, vet, builds, deterministic generation, and license checks passed | | Linux/amd64 in isolated containers on a Linux server | 1.25.12, 1.26.5 | The earlier pre-beta baseline passed tests, race, vet, builds, deterministic generation, and license checks; this was not rerun on the exact Beta 1 commit |
| macOS | — | Native maintainer validation pending; provisional for Beta 1 | | macOS | — | Native maintainer validation pending; provisional for Beta 1 |
The golden generated file had SHA-256 The golden generated file had SHA-256
`63fa75a3049a3a8a12d769d7f9b6b510dfe763baacf706775b75cef2c57a984f` `63fa75a3049a3a8a12d769d7f9b6b510dfe763baacf706775b75cef2c57a984f`
on every tested Windows and Linux lane. on every tested Windows and Linux lane.
Because the Beta 1 candidate contains changes after that public baseline, the The signed Beta tags and fresh direct/public-proxy installation were verified
complete Windows/Linux matrix must be rerun on the exact candidate before the after publication. For Beta 1, add the nested runtime to an application module
tags are published. The baseline does not become beta evidence merely because before installing the parent compiler at the same version; this avoids a Go
its code is nearby in history. module-cache path-selection ambiguity observed in the reverse order.
## macOS feedback ## macOS feedback
+51
View File
@@ -0,0 +1,51 @@
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
# Hime-san language server
Hime-san `v1.0.0-beta.2` adds a reusable, read-only Language Server Protocol
surface:
```sh
himesan lsp --stdio
```
The server accepts one local workspace root per process. Editors with multiple
workspace folders start one process for each folder. Standard output contains
only framed JSON-RPC; bounded operational messages go to standard error and do
not include template source, environment values, or secrets.
## Beta 2 capabilities
- full-document synchronization and unsaved in-memory overlays;
- live compiler diagnostics, trust warnings, duplicate components, and
statically knowable component cycles;
- UTF-16 protocol positions without changing compiler CLI byte coordinates;
- hover help for tags, inferred output contexts, component signatures, and
trusted-output boundaries;
- document symbols for the declared component and template regions;
- delimiter/tag completion plus same-package and already-imported component
completion; and
- component go-to-definition.
The index honors the compiler's symlink, nested-module, VCS, vendor, and
filesystem boundaries. Open/save is analyzed immediately; ordinary edits are
debounced for 200 ms and superseded analyses are canceled. Appearance,
deletion, rename, and save notifications rebuild the bounded source index.
## Deliberate exclusions
The language server does not generate files, report generated-file freshness,
invoke Go or `gopls`, execute project code, fetch dependencies, access the
network, or start `himesan dev`. It provides no general Go or HTML completion,
formatting, rename, references, automatic imports, or live browser preview.
Use explicit `himesan check --json` for committed-output freshness and normal
Go tests/builds for type checking. Editors remain responsible for workspace
trust, process startup, and user-visible command policy.
## Resource limits
Protocol frames and individual documents are limited to 16 MiB, the indexed
workspace source set to 64 MiB and 10,000 `.sando` files. These are denial-of-
service guardrails for trusted local workspaces, not a sandbox for hostile
template authors.
+31 -31
View File
@@ -11,23 +11,20 @@ verification, or guarantee that no vulnerability exists.
| Field | Value | | Field | Value |
| --- | --- | | --- | --- |
| Assessment date | 2026-08-12 | | Assessment date | 2026-08-12 |
| Evidence sets | Clean security self-assessment plus an exact-commit pre-beta platform baseline; neither is evidence for the later Beta 1 candidate | | Evidence sets | Clean security self-assessment plus exact-commit Beta 1 platform, release, signing, and installation checks |
| Public commit | `113c95c21e57227b4675c9fda015ada59cc9e9a6` | | Public commit | `b7a84054d755e42285e50298e41e47f06a8325a5` |
| Public tree | `a2aeb4dac22853cb3894e3e487b94bbeff5051e5` | | Public tree | `be9e118e38dfebed19f60403ededdadabe07d2aa` |
| Maintainer-run environments | Windows 11/amd64 on NTFS; Ubuntu 20.04/amd64 under WSL2 on ext4; Linux/amd64 server containers | | Maintainer-run environments | Windows 11/amd64 on NTFS; Ubuntu 20.04/amd64 under WSL2 on ext4; supplementary pre-beta Linux/amd64 server containers |
| Supported Go lanes exercised | Go 1.25.12 and Go 1.26.5 | | Supported Go lanes exercised | Go 1.25.12 and Go 1.26.5 |
| Declared minimum Go | Go 1.25 | | Declared minimum Go | Go 1.25 |
| Assessor | Project maintainer with AI-assisted code review; human responsibility retained | | Assessor | Project maintainer with AI-assisted code review; human responsibility retained |
The named platform runs used the exact public commit and tree above. Hostnames, The named Windows and WSL2 platform runs used the exact public commit and tree
network addresses, account names, private paths, private repository identities, above. The isolated server-container matrix preceded the final candidate and
and private commit mappings are intentionally absent from this public ledger. is retained only as supplementary Linux evidence. Hostnames, network addresses,
account names, private paths, private repository identities, and private commit
Beta 1 necessarily changes the tree through versioning, provenance, mappings are intentionally absent from this public ledger. Native macOS
documentation, or source fixes. Therefore this baseline cannot be relabeled as execution remains pending and is provisional for the beta.
Beta 1 evidence. The required Windows/Linux campaign must pass again on the
exact Beta 1 candidate before either tag is published. Native macOS execution
remains pending and is provisional for the beta.
## Observed security self-assessment evidence ## Observed security self-assessment evidence
@@ -52,7 +49,7 @@ baseline commit.
| URL scheme handling | ordinary/trusted URL test matrix | Pass for enumerated cases | | URL scheme handling | ordinary/trusted URL test matrix | Pass for enumerated cases |
| Filesystem boundaries | symlink, nested-module, VCS, ownership, stale-output tests | Pass for tested cases; see open findings | | Filesystem boundaries | symlink, nested-module, VCS, ownership, stale-output tests | Pass for tested cases; see open findings |
| Development proxy browser boundary | Host, Origin, Fetch Metadata, CSP, fragment and response tests | Pass for tested cases | | Development proxy browser boundary | Host, Origin, Fetch Metadata, CSP, fragment and response tests | Pass for tested cases |
| Platform behavior | Native Windows and executed Linux matrices; macOS cross-compilation | Windows/Linux pass for tested lanes; native macOS pending | | Platform behavior | Exact-candidate native Windows and executed Linux matrices; macOS cross-compilation | Windows/Linux pass for tested lanes; native macOS pending |
Coverage measures statements executed by tests. It is not branch completeness Coverage measures statements executed by tests. It is not branch completeness
and is not evidence that the executed behavior is secure. and is not evidence that the executed behavior is secure.
@@ -61,7 +58,7 @@ and is not evidence that the executed behavior is secure.
and reachable through its analysis. A clean result cannot detect unknown flaws, and reachable through its analysis. A clean result cannot detect unknown flaws,
design errors, or vulnerabilities outside its model. design errors, or vulnerabilities outside its model.
## Pre-beta native compatibility matrix ## Beta 1 native compatibility matrix
These are maintainer-run, point-in-time results, not continuous CI and not an These are maintainer-run, point-in-time results, not continuous CI and not an
independent audit. independent audit.
@@ -70,7 +67,7 @@ independent audit.
| --- | --- | --- | --- | | --- | --- | --- | --- |
| Windows 11/amd64, NTFS | 1.25.12, 1.26.5 | Native PowerShell verifier with race; root/runtime tests, vet, trimpath build, freshness, two generation passes, process-tree cleanup, watcher boundaries, and temporary consumer compilation | Pass. Symlink-output rejection skipped because the test account lacked symlink privilege; the read-only-directory case is POSIX-only | | Windows 11/amd64, NTFS | 1.25.12, 1.26.5 | Native PowerShell verifier with race; root/runtime tests, vet, trimpath build, freshness, two generation passes, process-tree cleanup, watcher boundaries, and temporary consumer compilation | Pass. Symlink-output rejection skipped because the test account lacked symlink privilege; the read-only-directory case is POSIX-only |
| Ubuntu 20.04/amd64 under WSL2, native ext4 checkout | 1.25.12, 1.26.5 | Race-enabled verifier; root/runtime tests, vet, build, two generation passes, ten focused filesystem cases, five focused development-process/watcher cases, and license check | Pass. This is Linux execution under WSL2, not bare-metal or Linux/arm64 evidence | | Ubuntu 20.04/amd64 under WSL2, native ext4 checkout | 1.25.12, 1.26.5 | Race-enabled verifier; root/runtime tests, vet, build, two generation passes, ten focused filesystem cases, five focused development-process/watcher cases, and license check | Pass. This is Linux execution under WSL2, not bare-metal or Linux/arm64 evidence |
| Linux/amd64 server containers | 1.25.12, 1.26.5 | Root/runtime tests, vet, builds, race, licensing, and deterministic generation in sequential isolated official Go containers | Pass. Container resources were capped at 1 CPU and 2 GiB; this is not Linux/arm64 evidence | | Linux/amd64 server containers | 1.25.12, 1.26.5 | Earlier pre-beta root/runtime tests, vet, builds, race, licensing, and deterministic generation in sequential isolated official Go containers | Pass on the earlier baseline only. Container resources were capped at 1 CPU and 2 GiB; this is supplementary evidence, not an exact Beta 1 lane or Linux/arm64 evidence |
| macOS | — | Cross-compilation only | Native maintainer execution pending; provisional for Beta 1 | | macOS | — | Cross-compilation only | Native maintainer execution pending; provisional for Beta 1 |
The generated golden `basic.sando.go` was 1,399 bytes and had SHA-256 The generated golden `basic.sando.go` was 1,399 bytes and had SHA-256
@@ -144,6 +141,9 @@ go test ./internal/compiler -run '^$' \
-fuzz '^FuzzCompileNeverPanics$' -fuzztime=20s -fuzz '^FuzzCompileNeverPanics$' -fuzztime=20s
go test ./internal/compiler -run '^$' \ go test ./internal/compiler -run '^$' \
-fuzz '^FuzzGoDelimiterNeverPanics$' -fuzztime=20s -fuzz '^FuzzGoDelimiterNeverPanics$' -fuzztime=20s
./scripts/release-check.sh --version v1.0.0-beta.1
./scripts/verify-public-install.sh --version v1.0.0-beta.1
``` ```
The fuzz targets currently assert process robustness and result bounds. They do The fuzz targets currently assert process robustness and result bounds. They do
@@ -152,7 +152,7 @@ not yet prove semantic HTML safety.
## Assessment findings and remediation status ## Assessment findings and remediation status
The 2026-08-12 assessment identified six concrete gaps. Their status in the The 2026-08-12 assessment identified six concrete gaps. Their status in the
named public pre-beta baseline is recorded here: named public Beta 1 source is recorded here:
| Finding | Current remediation | Executable evidence | | Finding | Current remediation | Executable evidence |
| --- | --- | --- | | --- | --- | --- |
@@ -163,23 +163,23 @@ named public pre-beta baseline is recorded here:
| Trusted-value warnings were described more broadly than their analysis supports | Policy and threat-model copy now call them best-effort lexical audit hints rather than type or taint analysis | Documentation assertion and review | | Trusted-value warnings were described more broadly than their analysis supports | Policy and threat-model copy now call them best-effort lexical audit hints rather than type or taint analysis | Documentation assertion and review |
| Public copy implied a completed systematic `html/template` differential campaign | Policy and public security copy now describe fixed adversarial cases and list systematic differential work as open | Documentation assertion and review | | Public copy implied a completed systematic `html/template` differential campaign | Policy and public security copy now describe fixed adversarial cases and list systematic differential work as open | Documentation assertion and review |
The clean remediated assessment source passed the race-enabled repository The exact public Beta 1 source passed the race-enabled repository verifier,
verifier, sanitized-snapshot tests, both bounded fuzz-smoke targets, sanitized-snapshot tests, both bounded fuzz-smoke targets, compiler/runtime
compiler/runtime known-vulnerability scans, and Windows/macOS cross-compilation known-vulnerability scans, candidate-version provenance checks, native Windows
on 2026-08-12. Separately, the exact public pre-beta commit passed the native and executed Linux matrices, and Windows/macOS cross-compilation on 2026-08-12.
Windows and executed Linux matrices recorded above. These results still do not Signed annotated runtime and compiler tags were then published from that commit
become Beta 1 evidence: both sets of required checks must run on the exact in that order. Fresh runtime-first installation passed through both direct Git
candidate after all candidate changes. Native macOS and the other gaps below resolution and the public Go proxy after normal proxy propagation. Native
remain separate release decisions. macOS and the other gaps below remain separate release decisions.
## Open assurance gaps ## Open assurance gaps
- the exact Beta 1 candidate Windows/Linux matrix and post-tag install checks - delivery to `security@sandwichhime.com` is owner-confirmed through a
must still run; controlled domain catch-all; encrypted reporting, documented backup, and
- confidential mailbox delivery and response/recovery procedure must be tested; recovery rehearsal remain incomplete;
- SSH tag-signing rehearsal passed, but the candidate tags still require - the signed annotated Beta tags and their common peeled commit were verified;
post-publication verification; prebuilt-artifact signing, checksums, SBOM, prebuilt-artifact signing, checksums, SBOM, reproducible provenance, and key
reproducible provenance, and key recovery remain incomplete; recovery remain incomplete;
- native macOS, Linux/arm64, and Windows/arm64 execution remain outstanding; - native macOS, Linux/arm64, and Windows/arm64 execution remain outstanding;
- Windows symlink rejection was not natively exercised because the test account - Windows symlink rejection was not natively exercised because the test account
lacked symlink privilege; lacked symlink privilege;
+12 -15
View File
@@ -31,17 +31,14 @@ repeatable install for learners and evaluators without claiming that the final
v1 compatibility, native-platform, artifact, signing, or soak gates are v1 compatibility, native-platform, artifact, signing, or soak gates are
complete. complete.
### Demonstrated in the pre-beta public baseline ### Demonstrated for Beta 1
Public commit `113c95c21e57227b4675c9fda015ada59cc9e9a6` (tree Public commit `b7a84054d755e42285e50298e41e47f06a8325a5` (tree
`a2aeb4dac22853cb3894e3e487b94bbeff5051e5`) passed maintainer-run Go `be9e118e38dfebed19f60403ededdadabe07d2aa`) passed maintainer-run Go
1.25.12 and Go 1.26.5 matrices on native Windows/amd64, Linux/amd64 under WSL2, 1.25.12 and Go 1.26.5 matrices on native Windows/amd64, Linux/amd64 under WSL2,
and isolated Linux/amd64 server containers. The same generated golden SHA-256 with the earlier pre-beta server-container run retained only as supplementary
was observed across those lanes. Linux evidence. The same generated golden SHA-256 was observed across the exact
Beta Windows and Linux lanes.
That result is a pre-beta baseline only. The exact Beta 1 candidate must rerun
the required Windows/Linux matrix after all version, documentation, and source
changes and before tags are created.
Other demonstrated controls include: Other demonstrated controls include:
@@ -63,8 +60,7 @@ Other demonstrated controls include:
- complete real-browser development-supervisor evidence; - complete real-browser development-supervisor evidence;
- deterministic prebuilt archives, checksums, SBOMs, signed binaries, and - deterministic prebuilt archives, checksums, SBOMs, signed binaries, and
tested signing/recovery procedures; or tested signing/recovery procedures; or
- clean direct and public-proxy installation of the not-yet-published Beta 1 - native macOS installation of the published Beta 1 tags.
tags.
## Beta 1 publication lane ## Beta 1 publication lane
@@ -75,13 +71,14 @@ dependency, and its interfaces may change.
- [x] Define beta support, security, compatibility, and macOS-provisional - [x] Define beta support, security, compatibility, and macOS-provisional
language. language.
- [x] Establish the named public pre-beta Linux/Windows baseline. - [x] Establish the named public pre-beta Linux/Windows baseline.
- [ ] Rerun the supported Go matrix and deterministic generation on the exact - [x] Rerun the supported Go matrix and deterministic generation on the exact
Beta 1 candidate. Beta 1 candidate.
- [ ] Run the candidate-version freshness, bounded fuzz, vulnerability, and - [x] Run the candidate-version freshness, bounded fuzz, vulnerability, and
license gates. license gates.
- [ ] Publish immutable `sando/v1.0.0-beta.1`, then - [x] Publish immutable `sando/v1.0.0-beta.1`, then
`v1.0.0-beta.1`, from the same reviewed public commit. `v1.0.0-beta.1`, from the same reviewed public commit.
- [ ] Verify clean direct and public-proxy installs and record the result. - [x] Verify clean runtime-first direct and public-proxy installs and record the
result.
- [ ] Add native macOS maintainer evidence before RC; community reports inform - [ ] Add native macOS maintainer evidence before RC; community reports inform
that work but do not replace maintainer responsibility. that work but do not replace maintainer responsibility.
+163
View File
@@ -0,0 +1,163 @@
// SPDX-License-Identifier: AGPL-3.0-only
package compiler
import (
"context"
"fmt"
"path/filepath"
"sort"
)
// SourceInput supplies one in-memory .sando document for editor analysis.
type SourceInput struct {
Path string
Source []byte
}
// AnalysisImport describes one import already present in a .sando header.
type AnalysisImport struct {
Alias string `json:"alias,omitempty"`
Path string `json:"path"`
}
// AnalysisRegionKind identifies an author-visible template region.
type AnalysisRegionKind string
const (
AnalysisStatement AnalysisRegionKind = "statement"
AnalysisExpression AnalysisRegionKind = "expression"
AnalysisComponent AnalysisRegionKind = "component"
AnalysisComment AnalysisRegionKind = "comment"
)
// AnalysisRegion describes a Hime-san tag body using zero-based byte offsets.
// Line and Column retain the compiler's one-based byte-coordinate convention;
// protocol adapters convert them to UTF-16 where required.
type AnalysisRegion struct {
Kind AnalysisRegionKind `json:"kind"`
Text string `json:"text,omitempty"`
Context Context `json:"context,omitempty"`
Offset int `json:"offset"`
Length int `json:"length"`
Line int `json:"line"`
Column int `json:"column"`
}
// DocumentAnalysis is the compiler-owned semantic description consumed by
// read-only tools such as the language server.
type DocumentAnalysis struct {
Path string `json:"path"`
Package string `json:"package,omitempty"`
Component string `json:"component,omitempty"`
TypeParams string `json:"type_params,omitempty"`
Params string `json:"params,omitempty"`
Signature string `json:"signature,omitempty"`
ComponentOffset int `json:"component_offset,omitempty"`
ComponentLine int `json:"component_line,omitempty"`
ComponentColumn int `json:"component_column,omitempty"`
Imports []AnalysisImport `json:"imports,omitempty"`
Regions []AnalysisRegion `json:"regions,omitempty"`
Diagnostics []Diagnostic `json:"diagnostics,omitempty"`
}
// AnalyzeSources applies the normal parser, HTML-context analyzer, trust
// audit, backend validation, duplicate-component checks, and statically
// knowable cycle checks to an in-memory source set. It performs no I/O.
func AnalyzeSources(ctx context.Context, inputs []SourceInput) []DocumentAnalysis {
ordered := append([]SourceInput(nil), inputs...)
sort.SliceStable(ordered, func(i, j int) bool {
return filepath.Clean(ordered[i].Path) < filepath.Clean(ordered[j].Path)
})
analyses := make([]DocumentAnalysis, 0, len(ordered))
compiled := make([]CompiledFile, 0, len(ordered))
for _, input := range ordered {
if err := ctx.Err(); err != nil {
analyses = append(analyses, DocumentAnalysis{
Path: filepath.Clean(input.Path),
Diagnostics: []Diagnostic{diagnostic(input.Path, sourcePosition{Line: 1, Column: 1}, "HIM2001", "operation canceled: "+err.Error())},
})
continue
}
analysis, output := analyzeSource(input.Path, input.Source)
analyses = append(analyses, analysis)
if output.Code != nil {
compiled = append(compiled, output)
}
}
byPath := make(map[string][]Diagnostic)
for _, item := range detectComponentCycles(compiled) {
path := filepath.Clean(item.Path)
byPath[path] = append(byPath[path], item)
}
for index := range analyses {
path := filepath.Clean(analyses[index].Path)
analyses[index].Diagnostics = append(analyses[index].Diagnostics, byPath[path]...)
sortDiagnostics(analyses[index].Diagnostics)
}
return analyses
}
func analyzeSource(path string, source []byte) (DocumentAnalysis, CompiledFile) {
cleanPath := filepath.Clean(path)
analysis := DocumentAnalysis{Path: cleanPath}
file, diagnostics := parseSource(cleanPath, source)
if file == nil {
sortDiagnostics(diagnostics)
analysis.Diagnostics = diagnostics
return analysis, CompiledFile{}
}
analysis.Package = file.Package
analysis.Component = file.Name
analysis.TypeParams = file.TypeParams
analysis.Params = file.Params
analysis.Signature = fmt.Sprintf("func %s%s%s", file.Name, file.TypeParams, file.Params)
analysis.ComponentOffset = file.FunctionPos.Offset
analysis.ComponentLine = file.FunctionPos.Line
analysis.ComponentColumn = file.FunctionPos.Column
for _, imported := range file.Imports {
analysis.Imports = append(analysis.Imports, AnalysisImport{Alias: imported.Alias, Path: imported.Path})
}
diagnostics = append(diagnostics, analyzeContexts(file)...)
diagnostics = append(diagnostics, auditTrustCalls(file)...)
for _, node := range file.Nodes {
kind := AnalysisRegionKind("")
switch node.Kind {
case nodeStatement:
kind = AnalysisStatement
case nodeExpression:
kind = AnalysisExpression
case nodeComponent:
kind = AnalysisComponent
case nodeComment:
kind = AnalysisComment
default:
continue
}
analysis.Regions = append(analysis.Regions, AnalysisRegion{
Kind: kind, Text: node.Text, Context: node.Context,
Offset: node.Pos.Offset, Length: len(node.Text),
Line: node.Pos.Line, Column: node.Pos.Column,
})
}
if hasErrors(diagnostics) {
sortDiagnostics(diagnostics)
analysis.Diagnostics = diagnostics
return analysis, CompiledFile{}
}
code, backendDiagnostics := generateGo(file)
diagnostics = append(diagnostics, backendDiagnostics...)
sortDiagnostics(diagnostics)
analysis.Diagnostics = diagnostics
if hasErrors(diagnostics) {
return analysis, CompiledFile{}
}
return analysis, CompiledFile{
SourcePath: cleanPath,
OutputPath: cleanPath + ".go",
Package: file.Package,
Component: file.Name,
Code: code,
source: file,
}
}
+95
View File
@@ -0,0 +1,95 @@
// SPDX-License-Identifier: AGPL-3.0-only
package compiler
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
)
func TestAnalyzeSourcesUsesCompilerSemanticsWithoutIO(t *testing.T) {
t.Parallel()
directory := t.TempDir()
firstPath := filepath.Join(directory, "first.sando")
secondPath := filepath.Join(directory, "second.sando")
first := []byte("<?sando go\npackage views\nfunc First(name string)\n?>\n<div><?= name ?><?~ Second() ?></div>\n")
second := []byte("<?sando go\npackage views\nfunc Second()\n?>\n<section><?~ First(\"again\") ?></section>\n")
analyses := AnalyzeSources(context.Background(), []SourceInput{{Path: firstPath, Source: first}, {Path: secondPath, Source: second}})
if len(analyses) != 2 {
t.Fatalf("analysis count = %d, want 2", len(analyses))
}
for _, analysis := range analyses {
if analysis.Component == "" || analysis.Signature == "" || analysis.ComponentLine < 1 {
t.Fatalf("missing component metadata: %#v", analysis)
}
if !hasDiagnosticCode(analysis.Diagnostics, "HIM1501") {
t.Fatalf("cycle diagnostic missing for %s: %#v", analysis.Component, analysis.Diagnostics)
}
if _, err := os.Stat(analysis.Path + ".go"); !os.IsNotExist(err) {
t.Fatalf("analysis wrote generated output: %v", err)
}
}
if analyses[0].Regions[0].Context != ContextHTMLText {
t.Fatalf("expression context = %q, want %q", analyses[0].Regions[0].Context, ContextHTMLText)
}
}
func TestAnalyzeSourcesDuplicateAndMalformedDocuments(t *testing.T) {
t.Parallel()
directory := t.TempDir()
duplicate := "<?sando go\npackage views\nfunc Card()\n?>\n<p>card</p>\n"
malformed := "<?sando go\npackage views\nfunc Broken()\n?>\n<div>\x00"
analyses := AnalyzeSources(context.Background(), []SourceInput{
{Path: filepath.Join(directory, "a.sando"), Source: []byte(duplicate)},
{Path: filepath.Join(directory, "b.sando"), Source: []byte(duplicate)},
{Path: filepath.Join(directory, "broken.sando"), Source: []byte(malformed)},
})
if !hasDiagnosticCode(analyses[0].Diagnostics, "HIM1500") || !hasDiagnosticCode(analyses[1].Diagnostics, "HIM1500") {
t.Fatalf("duplicate diagnostics missing: %#v", analyses)
}
if !hasDiagnosticCode(analyses[2].Diagnostics, "HIM1002") {
t.Fatalf("NUL diagnostic missing: %#v", analyses[2].Diagnostics)
}
}
func TestDiscoverSourcesOmitsGeneratedFreshnessButKeepsBoundaries(t *testing.T) {
t.Parallel()
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module example.test/root\n\ngo 1.25\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "orphan.sando.go"), []byte(generatedPrefix+"\npackage root\n"), 0o600); err != nil {
t.Fatal(err)
}
nested := filepath.Join(root, "nested")
if err := os.Mkdir(nested, 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(nested, "go.mod"), []byte("module example.test/nested\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(nested, "hidden.sando"), []byte("ignored"), 0o600); err != nil {
t.Fatal(err)
}
paths, diagnostics := DiscoverSources(context.Background(), []string{root})
if len(paths) != 0 {
t.Fatalf("discovered nested source: %v", paths)
}
for _, item := range diagnostics {
if item.Code == "HIM2014" {
t.Fatalf("editor discovery reported generated freshness: %#v", diagnostics)
}
}
}
func hasDiagnosticCode(diagnostics []Diagnostic, code string) bool {
for _, item := range diagnostics {
if item.Code == code || strings.HasPrefix(item.Code, code) {
return true
}
}
return false
}
+14
View File
@@ -23,6 +23,17 @@ var excludedDirectories = map[string]bool{
} }
func discover(ctx context.Context, paths []string) ([]string, []Diagnostic) { func discover(ctx context.Context, paths []string) ([]string, []Diagnostic) {
return discoverWithOptions(ctx, paths, true)
}
// DiscoverSources finds .sando sources using the same filesystem, symlink,
// nested-module, VCS, and vendor boundaries as Generate and Check. It omits
// generated-output inspection because editor analysis does not own freshness.
func DiscoverSources(ctx context.Context, paths []string) ([]string, []Diagnostic) {
return discoverWithOptions(ctx, paths, false)
}
func discoverWithOptions(ctx context.Context, paths []string, inspectGenerated bool) ([]string, []Diagnostic) {
if len(paths) == 0 { if len(paths) == 0 {
paths = []string{"."} paths = []string{"."}
} }
@@ -121,6 +132,9 @@ func discover(ctx context.Context, paths []string) ([]string, []Diagnostic) {
return nil return nil
} }
if strings.HasSuffix(entry.Name(), ".sando.go") { if strings.HasSuffix(entry.Name(), ".sando.go") {
if !inspectGenerated {
return nil
}
entryInfo, statErr := entry.Info() entryInfo, statErr := entry.Info()
if statErr != nil { if statErr != nil {
diagnostics = append(diagnostics, diagnostic(path, sourcePosition{Line: 1, Column: 1}, "HIM2013", "cannot inspect possible generated output: "+statErr.Error())) diagnostics = append(diagnostics, diagnostic(path, sourcePosition{Line: 1, Column: 1}, "HIM2013", "cannot inspect possible generated output: "+statErr.Error()))
+1
View File
@@ -69,6 +69,7 @@ type sourceFile struct {
Nodes []rendererNode Nodes []rendererNode
Source []byte Source []byte
HeaderEnd int HeaderEnd int
FunctionPos sourcePosition
AST *ast.File AST *ast.File
} }
+3
View File
@@ -123,6 +123,7 @@ func parseSource(path string, source []byte) (*sourceFile, []Diagnostic) {
Imports: parsedHeader.Imports, Imports: parsedHeader.Imports,
Source: source, Source: source,
HeaderEnd: headerClose + 2, HeaderEnd: headerClose + 2,
FunctionPos: parsedHeader.FunctionPos,
AST: parsedHeader.AST, AST: parsedHeader.AST,
} }
@@ -144,6 +145,7 @@ type parsedHeader struct {
TypeParams string TypeParams string
Params string Params string
Imports []sourceImport Imports []sourceImport
FunctionPos sourcePosition
AST *ast.File AST *ast.File
} }
@@ -259,6 +261,7 @@ func parseHeader(path string, declarations []byte, sourceOffset int, table posit
TypeParams: typeParams, TypeParams: typeParams,
Params: params, Params: params,
Imports: imports, Imports: imports,
FunctionPos: table.at(sourceOffset + functionPosition.Offset),
AST: parsed, AST: parsed,
}, diagnostics }, diagnostics
} }
+19 -2
View File
@@ -23,6 +23,7 @@ func TestSupervisorBuildsSwapsAndCleansUp(t *testing.T) {
t.Skip("integration test builds temporary Go applications") t.Skip("integration test builds temporary Go applications")
} }
root := t.TempDir() root := t.TempDir()
disableParentVCSStamping(t)
if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module example.test/himesan-dev-test\n\ngo 1.25\n"), 0o600); err != nil { if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module example.test/himesan-dev-test\n\ngo 1.25\n"), 0o600); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -41,7 +42,10 @@ func TestSupervisorBuildsSwapsAndCleansUp(t *testing.T) {
generations.Add(1) generations.Add(1)
return nil return nil
}, },
OnEvent: func(event Event) { events <- event }, OnEvent: func(event Event) {
t.Logf("supervisor event: type=%s phase=%s message=%s", event.Type, event.Phase, event.Message)
events <- event
},
CacheDir: filepath.Join(t.TempDir(), "cache"), CacheDir: filepath.Join(t.TempDir(), "cache"),
PollInterval: 25 * time.Millisecond, PollInterval: 25 * time.Millisecond,
Debounce: 25 * time.Millisecond, Debounce: 25 * time.Millisecond,
@@ -101,6 +105,7 @@ func TestSupervisorClearsTargetWhenCurrentApplicationExits(t *testing.T) {
t.Skip("integration test builds a temporary Go application") t.Skip("integration test builds a temporary Go application")
} }
root := t.TempDir() root := t.TempDir()
disableParentVCSStamping(t)
if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module example.test/himesan-dev-exit-test\n\ngo 1.25\n"), 0o600); err != nil { if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module example.test/himesan-dev-exit-test\n\ngo 1.25\n"), 0o600); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -115,7 +120,10 @@ func TestSupervisorClearsTargetWhenCurrentApplicationExits(t *testing.T) {
RootDir: root, RootDir: root,
Config: cfg, Config: cfg,
Generate: func(context.Context) error { return nil }, Generate: func(context.Context) error { return nil },
OnEvent: func(event Event) { events <- event }, OnEvent: func(event Event) {
t.Logf("supervisor event: type=%s phase=%s message=%s", event.Type, event.Phase, event.Message)
events <- event
},
CacheDir: filepath.Join(t.TempDir(), "cache"), CacheDir: filepath.Join(t.TempDir(), "cache"),
PollInterval: 30 * time.Second, PollInterval: 30 * time.Second,
Debounce: 25 * time.Millisecond, Debounce: 25 * time.Millisecond,
@@ -163,6 +171,15 @@ func TestSupervisorClearsTargetWhenCurrentApplicationExits(t *testing.T) {
} }
} }
func disableParentVCSStamping(t *testing.T) {
t.Helper()
// A temporary standalone module can live under a parent directory that is
// itself a VCS checkout (including hardened test sandboxes). Its candidate
// must not inherit or depend on that unrelated repository's status.
flags := strings.TrimSpace(os.Getenv("GOFLAGS") + " -buildvcs=false")
t.Setenv("GOFLAGS", flags)
}
func TestGenerationFailureDoesNotMoveProxyTarget(t *testing.T) { func TestGenerationFailureDoesNotMoveProxyTarget(t *testing.T) {
t.Parallel() t.Parallel()
upstream := http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { upstream := http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+304
View File
@@ -0,0 +1,304 @@
// SPDX-License-Identifier: AGPL-3.0-only
package lsp
import (
"bytes"
"fmt"
"strings"
"unicode"
"unicode/utf8"
"gamertan.com/sandwich-hime/internal/compiler"
)
type completionItem struct {
Label string `json:"label"`
Kind int `json:"kind,omitempty"`
Detail string `json:"detail,omitempty"`
Documentation any `json:"documentation,omitempty"`
InsertText string `json:"insertText,omitempty"`
InsertTextFormat int `json:"insertTextFormat,omitempty"`
SortText string `json:"sortText,omitempty"`
}
type markupContent struct {
Kind string `json:"kind"`
Value string `json:"value"`
}
type hoverResult struct {
Contents markupContent `json:"contents"`
Range *Range `json:"range,omitempty"`
}
type documentSymbol struct {
Name string `json:"name"`
Detail string `json:"detail,omitempty"`
Kind int `json:"kind"`
Range Range `json:"range"`
SelectionRange Range `json:"selectionRange"`
Children []documentSymbol `json:"children,omitempty"`
}
func (server *Server) completions(request textDocumentPositionParams) any {
document, analysis, ok := server.snapshotDocument(request.TextDocument.URI)
if !ok {
return []completionItem{}
}
offset, ok := positionToOffset(document.Text, request.Position)
if !ok {
return []completionItem{}
}
open := bytes.LastIndex(document.Text[:offset], []byte("<?"))
close := bytes.LastIndex(document.Text[:offset], []byte("?>"))
if open >= 0 && open > close && bytes.HasPrefix(document.Text[open:offset], []byte("<?~")) {
server.mu.RLock()
snapshot := server.snapshot
server.mu.RUnlock()
prefix := strings.TrimSpace(string(document.Text[open+3 : offset]))
items := make([]completionItem, 0)
for _, target := range snapshot.componentsFor(document.Path, analysis) {
label := target.Label()
if prefix != "" && !strings.HasPrefix(label, prefix) {
continue
}
items = append(items, completionItem{
Label: label, Kind: 3, Detail: target.Signature,
Documentation: markupContent{Kind: "markdown", Value: "Typed `.sando` component. Hime-san emits an ordinary Go constructor."},
InsertText: label, InsertTextFormat: 1, SortText: "1-" + label,
})
}
return struct {
IsIncomplete bool `json:"isIncomplete"`
Items []completionItem `json:"items"`
}{Items: items}
}
return struct {
IsIncomplete bool `json:"isIncomplete"`
Items []completionItem `json:"items"`
}{Items: tagCompletions()}
}
func tagCompletions() []completionItem {
return []completionItem{
{Label: "<?sando go", Kind: 15, Detail: "component file header", InsertText: "<?sando go\npackage ${1:views}\nfunc ${2:Component}(${3:})\n?>", InsertTextFormat: 2, SortText: "0-header"},
{Label: "<? … ?>", Kind: 15, Detail: "Go statement", InsertText: "<? ${1:if condition {} } ?>", InsertTextFormat: 2, SortText: "0-statement"},
{Label: "<?= … ?>", Kind: 15, Detail: "contextually escaped expression", InsertText: "<?= ${1:value} ?>", InsertTextFormat: 2, SortText: "0-expression"},
{Label: "<?~ … ?>", Kind: 15, Detail: "typed component composition", InsertText: "<?~ ${1:Component()} ?>", InsertTextFormat: 2, SortText: "0-component"},
{Label: "<?# … ?>", Kind: 15, Detail: "Hime-san template comment", InsertText: "<?# ${1:comment} ?>", InsertTextFormat: 2, SortText: "0-comment"},
}
}
func (server *Server) hover(request textDocumentPositionParams) any {
document, analysis, ok := server.snapshotDocument(request.TextDocument.URI)
if !ok {
return nil
}
offset, ok := positionToOffset(document.Text, request.Position)
if !ok {
return nil
}
for _, region := range analysis.Regions {
if offset < region.Offset || offset > region.Offset+region.Length {
continue
}
if region.Kind == compiler.AnalysisComponent {
qualifier, name := referenceAt(region.Text, offset-region.Offset)
if name != "" {
if target, found := server.resolveComponent(document.Path, analysis, qualifier, name); found {
value := "```go\n" + target.Signature + "\n```\n\nTyped component composition. Handwritten `sando.Component` values are trusted output capabilities."
return hoverResult{Contents: markupContent{Kind: "markdown", Value: value}}
}
}
}
value := fmt.Sprintf("**Hime-san %s region**\n\nOutput context: `%s`.", region.Kind, region.Context)
if region.Context == compiler.ContextJS || region.Context == compiler.ContextCSS || strings.Contains(region.Text, "Trust") {
value += "\n\nTrusted output is an explicit security capability; audit its provenance and parser-state effects."
}
return hoverResult{Contents: markupContent{Kind: "markdown", Value: value}}
}
if marker, start, end := enclosingTag(document.Text, offset); marker != "" {
if text := tagDocumentation(marker); text != "" {
rangeValue := Range{Start: offsetToPosition(document.Text, start), End: offsetToPosition(document.Text, end)}
return hoverResult{Contents: markupContent{Kind: "markdown", Value: text}, Range: &rangeValue}
}
}
return nil
}
func tagDocumentation(marker string) string {
switch marker {
case "<?sando":
return "**`<?sando go … ?>`** declares the Go package, imports, and one typed component signature. It must be the first non-whitespace content."
case "<?~":
return "**`<?~ … ?>`** composes a typed `sando.Component` at an HTML content boundary. It is not template inheritance."
case "<?=":
return "**`<?= … ?>`** renders an expression through the helper selected by Hime-san's inferred HTML output context."
case "<?#":
return "**`<?# … ?>`** is a Hime-san comment. It emits no bytes and cannot change HTML parser state."
case "<?":
return "**`<? … ?>`** contains Go statements and is valid only at an HTML content boundary."
}
return ""
}
func enclosingTag(text []byte, offset int) (string, int, int) {
if offset < 0 || offset > len(text) {
return "", 0, 0
}
open := bytes.LastIndex(text[:offset], []byte("<?"))
if open < 0 {
return "", 0, 0
}
closeRelative := bytes.Index(text[open:], []byte("?>"))
if closeRelative < 0 || open+closeRelative+2 < offset {
return "", 0, 0
}
end := open + closeRelative + 2
marker := "<?"
for _, candidate := range []string{"<?sando", "<?~", "<?=", "<?#"} {
if bytes.HasPrefix(text[open:end], []byte(candidate)) {
marker = candidate
break
}
}
return marker, open, end
}
func (server *Server) definition(request textDocumentPositionParams) any {
document, analysis, ok := server.snapshotDocument(request.TextDocument.URI)
if !ok {
return nil
}
offset, ok := positionToOffset(document.Text, request.Position)
if !ok {
return nil
}
for _, region := range analysis.Regions {
if region.Kind != compiler.AnalysisComponent || offset < region.Offset || offset > region.Offset+region.Length {
continue
}
qualifier, name := referenceAt(region.Text, offset-region.Offset)
if name == "" {
return nil
}
target, found := server.resolveComponent(document.Path, analysis, qualifier, name)
if !found {
return nil
}
server.mu.RLock()
targetDocument, exists := server.snapshot.documents[target.Path]
server.mu.RUnlock()
if !exists {
return nil
}
start := target.Analysis.ComponentOffset
end := start + len(target.Analysis.Signature)
return Location{URI: targetDocument.URI, Range: Range{Start: offsetToPosition(targetDocument.Text, start), End: offsetToPosition(targetDocument.Text, end)}}
}
return nil
}
func (server *Server) resolveComponent(path string, analysis compiler.DocumentAnalysis, qualifier, name string) (componentTarget, bool) {
server.mu.RLock()
snapshot := server.snapshot
server.mu.RUnlock()
for _, target := range snapshot.componentsFor(path, analysis) {
if target.Qualifier == qualifier && target.Name == name {
return target, true
}
}
return componentTarget{}, false
}
func referenceAt(expression string, cursor int) (string, string) {
if cursor < 0 {
cursor = 0
}
if cursor > len(expression) {
cursor = len(expression)
}
if cursor == len(expression) && cursor > 0 {
cursor--
}
for cursor > 0 && cursor < len(expression) && !identifierByte(expression[cursor]) && expression[cursor] != '.' {
cursor--
}
start := cursor
for start > 0 && (identifierByte(expression[start-1]) || expression[start-1] == '.') {
start--
}
end := cursor
for end < len(expression) && (identifierByte(expression[end]) || expression[end] == '.') {
end++
}
reference := strings.Trim(expression[start:end], ".")
parts := strings.Split(reference, ".")
if len(parts) == 1 && validIdentifier(parts[0]) {
return "", parts[0]
}
if len(parts) == 2 && validIdentifier(parts[0]) && validIdentifier(parts[1]) {
return parts[0], parts[1]
}
return "", ""
}
func identifierByte(value byte) bool {
return value == '_' || value >= 'a' && value <= 'z' || value >= 'A' && value <= 'Z' || value >= '0' && value <= '9'
}
func validIdentifier(value string) bool {
if value == "" {
return false
}
for index, r := range value {
if index == 0 && !(r == '_' || unicode.IsLetter(r)) {
return false
}
if index != 0 && !(r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)) {
return false
}
}
return true
}
func (server *Server) documentSymbols(uri string) []documentSymbol {
document, analysis, ok := server.snapshotDocument(uri)
if !ok || analysis.Component == "" {
return []documentSymbol{}
}
documentRange := Range{Start: Position{}, End: offsetToPosition(document.Text, len(document.Text))}
selectionStart := analysis.ComponentOffset
selectionEnd := selectionStart + len(analysis.Component)
children := make([]documentSymbol, 0, len(analysis.Regions))
for index, region := range analysis.Regions {
end := region.Offset + region.Length
name := fmt.Sprintf("%s %d", region.Kind, index+1)
if region.Kind == compiler.AnalysisComponent {
_, componentName := referenceAt(region.Text, 0)
if componentName != "" {
name = "component " + componentName
}
}
rangeValue := Range{Start: offsetToPosition(document.Text, region.Offset), End: offsetToPosition(document.Text, end)}
children = append(children, documentSymbol{Name: name, Detail: string(region.Context), Kind: 13, Range: rangeValue, SelectionRange: rangeValue})
}
return []documentSymbol{{
Name: analysis.Component, Detail: analysis.Signature, Kind: 12,
Range: documentRange,
SelectionRange: Range{Start: offsetToPosition(document.Text, selectionStart), End: offsetToPosition(document.Text, selectionEnd)},
Children: children,
}}
}
func runeEnd(text []byte, offset int) int {
if offset >= len(text) {
return len(text)
}
_, size := utf8.DecodeRune(text[offset:])
if size < 1 {
size = 1
}
return offset + size
}
+174
View File
@@ -0,0 +1,174 @@
// SPDX-License-Identifier: AGPL-3.0-only
package lsp
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"strconv"
"strings"
"sync"
)
const (
maxMessageBytes = 16 << 20
maxHeaderBytes = 64 << 10
maxHeaderLines = 64
)
type rpcMessage struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id,omitempty"`
Method string `json:"method,omitempty"`
Params json.RawMessage `json:"params,omitempty"`
Result any `json:"result,omitempty"`
Error *rpcError `json:"error,omitempty"`
}
type rpcError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func validRequestID(id json.RawMessage) bool {
if len(id) == 0 {
return true
}
var value any
if err := json.Unmarshal(id, &value); err != nil {
return false
}
switch value := value.(type) {
case string:
return true
case float64:
return value == math.Trunc(value)
default:
return false
}
}
const (
errParse = -32700
errInvalidRequest = -32600
errMethodNotFound = -32601
errInvalidParams = -32602
errInternal = -32603
errRequestCancelled = -32800
)
type frameReader struct{ reader *bufio.Reader }
func newFrameReader(input io.Reader) *frameReader {
return &frameReader{reader: bufio.NewReaderSize(input, 64<<10)}
}
func (reader *frameReader) read() ([]byte, error) {
contentLength := -1
headerBytes := 0
headerLines := 0
for {
line, err := reader.reader.ReadString('\n')
if err != nil {
return nil, err
}
headerBytes += len(line)
headerLines++
if headerBytes > maxHeaderBytes || headerLines > maxHeaderLines {
return nil, errors.New("LSP headers exceed configured limits")
}
if len(line) > 8<<10 {
return nil, errors.New("LSP header line exceeds 8 KiB")
}
line = strings.TrimSuffix(strings.TrimSuffix(line, "\n"), "\r")
if line == "" {
break
}
name, value, ok := strings.Cut(line, ":")
if !ok {
return nil, errors.New("malformed LSP header")
}
if strings.EqualFold(strings.TrimSpace(name), "Content-Length") {
if contentLength >= 0 {
return nil, errors.New("duplicate Content-Length header")
}
parsed, parseErr := strconv.Atoi(strings.TrimSpace(value))
if parseErr != nil || parsed < 0 || parsed > maxMessageBytes {
return nil, errors.New("invalid or excessive Content-Length")
}
contentLength = parsed
}
}
if contentLength < 0 {
return nil, errors.New("missing Content-Length header")
}
payload := make([]byte, contentLength)
if _, err := io.ReadFull(reader.reader, payload); err != nil {
return nil, err
}
return payload, nil
}
type frameWriter struct {
mu sync.Mutex
output io.Writer
}
func (writer *frameWriter) write(message rpcMessage) error {
payload, err := json.Marshal(message)
if err != nil {
return err
}
var frame bytes.Buffer
fmt.Fprintf(&frame, "Content-Length: %d\r\n\r\n", len(payload))
frame.Write(payload)
writer.mu.Lock()
defer writer.mu.Unlock()
written, err := writer.output.Write(frame.Bytes())
if err == nil && written != frame.Len() {
return io.ErrShortWrite
}
return err
}
type Position struct {
Line int `json:"line"`
Character int `json:"character"`
}
type Range struct {
Start Position `json:"start"`
End Position `json:"end"`
}
type Location struct {
URI string `json:"uri"`
Range Range `json:"range"`
}
type lspDiagnostic struct {
Range Range `json:"range"`
Severity int `json:"severity,omitempty"`
Code string `json:"code,omitempty"`
Source string `json:"source,omitempty"`
Message string `json:"message"`
}
type textDocumentIdentifier struct {
URI string `json:"uri"`
}
type versionedTextDocumentIdentifier struct {
URI string `json:"uri"`
Version int `json:"version"`
}
type textDocumentPositionParams struct {
TextDocument textDocumentIdentifier `json:"textDocument"`
Position Position `json:"position"`
}
+155
View File
@@ -0,0 +1,155 @@
// SPDX-License-Identifier: AGPL-3.0-only
package lsp
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"testing"
)
type shortWriter struct{}
func (shortWriter) Write(value []byte) (int, error) {
if len(value) == 0 {
return 0, nil
}
return len(value) - 1, nil
}
func TestProtocolFramingRoundTripAndLimits(t *testing.T) {
t.Parallel()
var output bytes.Buffer
writer := &frameWriter{output: &output}
if err := writer.write(rpcMessage{JSONRPC: "2.0", ID: json.RawMessage("1"), Result: map[string]bool{"ok": true}}); err != nil {
t.Fatal(err)
}
payload, err := newFrameReader(&output).read()
if err != nil {
t.Fatal(err)
}
var decoded rpcMessage
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatal(err)
}
if string(decoded.ID) != "1" {
t.Fatalf("response ID = %s", decoded.ID)
}
invalid := "Content-Length: 999999999\r\n\r\n"
if _, err := newFrameReader(strings.NewReader(invalid)).read(); err == nil {
t.Fatal("excessive frame length was accepted")
}
duplicate := "Content-Length: 2\r\nContent-Length: 2\r\n\r\n{}"
if _, err := newFrameReader(strings.NewReader(duplicate)).read(); err == nil {
t.Fatal("duplicate Content-Length was accepted")
}
var excessive bytes.Buffer
for range maxHeaderLines + 1 {
excessive.WriteString("X-Test: value\r\n")
}
excessive.WriteString("Content-Length: 2\r\n\r\n{}")
if _, err := newFrameReader(&excessive).read(); err == nil {
t.Fatal("excessive header count was accepted")
}
if err := (&frameWriter{output: shortWriter{}}).write(rpcMessage{JSONRPC: "2.0", ID: json.RawMessage("1"), Result: true}); !errors.Is(err, io.ErrShortWrite) {
t.Fatalf("short writer error = %v, want io.ErrShortWrite", err)
}
}
func TestMalformedJSONProducesProtocolErrorAndContinues(t *testing.T) {
t.Parallel()
input := bytes.NewBuffer(nil)
input.WriteString("Content-Length: 1\r\n\r\n{")
exit, err := json.Marshal(rpcMessage{JSONRPC: "2.0", Method: "exit"})
if err != nil {
t.Fatal(err)
}
fmt.Fprintf(input, "Content-Length: %d\r\n\r\n", len(exit))
input.Write(exit)
var output bytes.Buffer
if err := Run(context.Background(), Options{Input: input, Output: &output}); err != nil {
t.Fatal(err)
}
payload, err := newFrameReader(&output).read()
if err != nil {
t.Fatal(err)
}
var response rpcMessage
if err := json.Unmarshal(payload, &response); err != nil {
t.Fatal(err)
}
if response.Error == nil || response.Error.Code != errParse {
t.Fatalf("parse error response = %#v", response)
}
}
func TestRequestIDValidation(t *testing.T) {
t.Parallel()
for _, id := range []string{`1`, `-2`, `"request"`} {
if !validRequestID(json.RawMessage(id)) {
t.Errorf("valid request ID rejected: %s", id)
}
}
for _, id := range []string{`null`, `true`, `1.5`, `{}`, `[]`, `not-json`} {
if validRequestID(json.RawMessage(id)) {
t.Errorf("invalid request ID accepted: %s", id)
}
}
}
func TestUTF16PositionsWithUnicodeAndCRLF(t *testing.T) {
t.Parallel()
text := []byte("a😀b\r\n雪c\n")
tests := []struct {
offset int
position Position
}{
{offset: 0, position: Position{Line: 0, Character: 0}},
{offset: 1, position: Position{Line: 0, Character: 1}},
{offset: 5, position: Position{Line: 0, Character: 3}},
{offset: 8, position: Position{Line: 1, Character: 0}},
{offset: 11, position: Position{Line: 1, Character: 1}},
}
for _, test := range tests {
if got := offsetToPosition(text, test.offset); got != test.position {
t.Errorf("offsetToPosition(%d) = %#v, want %#v", test.offset, got, test.position)
}
if got, ok := positionToOffset(text, test.position); !ok || got != test.offset {
t.Errorf("positionToOffset(%#v) = %d, %v; want %d, true", test.position, got, ok, test.offset)
}
}
if _, ok := positionToOffset(text, Position{Line: 0, Character: 2}); ok {
t.Fatal("position inside UTF-16 surrogate pair was accepted")
}
}
func FuzzFrameReaderNeverPanics(f *testing.F) {
f.Add([]byte("Content-Length: 2\r\n\r\n{}"))
f.Add([]byte("Content-Length: nope\r\n\r\n"))
f.Fuzz(func(t *testing.T, data []byte) {
if len(data) > 64<<10 {
t.Skip()
}
_, _ = newFrameReader(bytes.NewReader(data)).read()
})
}
func FuzzDocumentPositionNeverPanics(f *testing.F) {
f.Add("hello 😀\r\nworld", 0, 7)
f.Add("雪", 0, 1)
f.Fuzz(func(t *testing.T, text string, line, character int) {
if len(text) > 64<<10 || line < -10000 || line > 10000 || character < -10000 || character > 100000 {
t.Skip()
}
offset, ok := positionToOffset([]byte(text), Position{Line: line, Character: character})
if ok {
_ = offsetToPosition([]byte(text), offset)
}
})
}
+64
View File
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: AGPL-3.0-only
package lsp
import (
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
)
func TestLanguageServerSourceHasNoExecutionNetworkOrWriteCapability(t *testing.T) {
t.Parallel()
entries, err := os.ReadDir(".")
if err != nil {
t.Fatal(err)
}
forbiddenImports := map[string]bool{
"net": true, "net/http": true, "net/rpc": true,
"os/exec": true, "syscall": true,
}
forbiddenOSCalls := map[string]bool{
"Create": true, "CreateTemp": true, "Mkdir": true, "MkdirAll": true,
"OpenFile": true, "Remove": true, "RemoveAll": true, "Rename": true,
"WriteFile": true, "Chmod": true, "Chown": true,
}
for _, entry := range entries {
if entry.IsDir() || filepath.Ext(entry.Name()) != ".go" || strings.HasSuffix(entry.Name(), "_test.go") {
continue
}
parsed, err := parser.ParseFile(token.NewFileSet(), entry.Name(), nil, 0)
if err != nil {
t.Fatal(err)
}
for _, imported := range parsed.Imports {
path, err := strconv.Unquote(imported.Path.Value)
if err != nil {
t.Fatal(err)
}
if forbiddenImports[path] {
t.Errorf("%s imports forbidden capability %s", entry.Name(), path)
}
}
ast.Inspect(parsed, func(node ast.Node) bool {
call, ok := node.(*ast.CallExpr)
if !ok {
return true
}
selector, ok := call.Fun.(*ast.SelectorExpr)
if !ok || !forbiddenOSCalls[selector.Sel.Name] {
return true
}
identifier, ok := selector.X.(*ast.Ident)
if ok && identifier.Name == "os" {
t.Errorf("%s calls forbidden filesystem mutation os.%s", entry.Name(), selector.Sel.Name)
}
return true
})
}
}
+371
View File
@@ -0,0 +1,371 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Package lsp implements Hime-san's read-only Language Server Protocol
// adapter. It deliberately owns no generation, Go toolchain, HTTP, network,
// or project-execution behavior.
package lsp
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"time"
"gamertan.com/sandwich-hime/internal/compiler"
)
// Options configures one stdio language-server process.
type Options struct {
Input io.Reader
Output io.Writer
LogOutput io.Writer
Debounce time.Duration
}
// Server serves exactly one workspace root.
type Server struct {
context context.Context
cancel context.CancelFunc
reader *frameReader
writer *frameWriter
logs io.Writer
mu sync.RWMutex
root string
initialized bool
shutdown bool
overlays map[string]document
snapshot workspaceSnapshot
analysisCancel context.CancelFunc
analysisTimer *time.Timer
analysisGeneration uint64
debounce time.Duration
afterFunc func(time.Duration, func()) *time.Timer
requests map[string]context.CancelFunc
wait sync.WaitGroup
analysisWait sync.WaitGroup
}
// Run serves LSP JSON-RPC until the client sends exit, closes stdin, or the
// parent context is canceled.
func Run(parent context.Context, options Options) error {
if options.Input == nil || options.Output == nil {
return errors.New("LSP stdin and stdout are required")
}
if options.LogOutput == nil {
options.LogOutput = io.Discard
}
if options.Debounce <= 0 {
options.Debounce = 200 * time.Millisecond
}
ctx, cancel := context.WithCancel(parent)
server := &Server{
context: ctx, cancel: cancel,
reader: newFrameReader(options.Input), writer: &frameWriter{output: options.Output}, logs: options.LogOutput,
overlays: make(map[string]document), snapshot: workspaceSnapshot{documents: make(map[string]document), analyses: make(map[string]compiler.DocumentAnalysis)},
debounce: options.Debounce, afterFunc: time.AfterFunc, requests: make(map[string]context.CancelFunc),
}
defer func() {
cancel()
server.mu.Lock()
server.analysisGeneration++
if server.analysisTimer != nil {
server.analysisTimer.Stop()
}
if server.analysisCancel != nil {
server.analysisCancel()
}
for _, requestCancel := range server.requests {
requestCancel()
}
server.mu.Unlock()
server.wait.Wait()
server.analysisWait.Wait()
}()
for {
payload, err := server.reader.read()
if err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, context.Canceled) || ctx.Err() != nil {
return nil
}
return fmt.Errorf("read LSP frame: %w", err)
}
var message rpcMessage
if err := json.Unmarshal(payload, &message); err != nil {
_ = server.writer.write(rpcMessage{JSONRPC: "2.0", ID: json.RawMessage("null"), Error: &rpcError{Code: errParse, Message: "invalid JSON"}})
continue
}
if message.JSONRPC != "2.0" || message.Method == "" || !validRequestID(message.ID) {
_ = server.writer.write(rpcMessage{JSONRPC: "2.0", ID: responseID(message.ID), Error: &rpcError{Code: errInvalidRequest, Message: "invalid JSON-RPC request"}})
continue
}
if len(message.ID) == 0 {
if message.Method == "exit" {
server.cancel()
return nil
}
server.handleNotification(message.Method, message.Params)
continue
}
server.startRequest(message)
}
}
func responseID(id json.RawMessage) json.RawMessage {
if len(id) == 0 {
return json.RawMessage("null")
}
return append(json.RawMessage(nil), id...)
}
func (server *Server) startRequest(message rpcMessage) {
key := string(message.ID)
ctx, cancel := context.WithCancel(server.context)
server.mu.Lock()
server.requests[key] = cancel
server.mu.Unlock()
server.wait.Add(1)
go func() {
defer server.wait.Done()
defer cancel()
result, rpcErr := server.handleRequest(ctx, message.Method, message.Params)
if ctx.Err() != nil && rpcErr == nil {
rpcErr = &rpcError{Code: errRequestCancelled, Message: "request canceled"}
}
if rpcErr == nil && result == nil {
result = json.RawMessage("null")
}
server.mu.Lock()
delete(server.requests, key)
server.mu.Unlock()
_ = server.writer.write(rpcMessage{JSONRPC: "2.0", ID: responseID(message.ID), Result: result, Error: rpcErr})
}()
}
func (server *Server) handleRequest(ctx context.Context, method string, params json.RawMessage) (any, *rpcError) {
switch method {
case "initialize":
return server.initialize(params)
case "shutdown":
server.mu.Lock()
server.shutdown = true
if server.analysisCancel != nil {
server.analysisCancel()
}
server.mu.Unlock()
return nil, nil
}
server.mu.RLock()
ready := server.initialized && !server.shutdown
server.mu.RUnlock()
if !ready {
return nil, &rpcError{Code: errInvalidRequest, Message: "language server is not initialized"}
}
select {
case <-ctx.Done():
return nil, &rpcError{Code: errRequestCancelled, Message: "request canceled"}
default:
}
switch method {
case "textDocument/completion":
var request textDocumentPositionParams
if err := json.Unmarshal(params, &request); err != nil {
return nil, &rpcError{Code: errInvalidParams, Message: "invalid completion parameters"}
}
return server.completions(request), nil
case "textDocument/hover":
var request textDocumentPositionParams
if err := json.Unmarshal(params, &request); err != nil {
return nil, &rpcError{Code: errInvalidParams, Message: "invalid hover parameters"}
}
return server.hover(request), nil
case "textDocument/definition":
var request textDocumentPositionParams
if err := json.Unmarshal(params, &request); err != nil {
return nil, &rpcError{Code: errInvalidParams, Message: "invalid definition parameters"}
}
return server.definition(request), nil
case "textDocument/documentSymbol":
var request struct {
TextDocument textDocumentIdentifier `json:"textDocument"`
}
if err := json.Unmarshal(params, &request); err != nil {
return nil, &rpcError{Code: errInvalidParams, Message: "invalid document-symbol parameters"}
}
return server.documentSymbols(request.TextDocument.URI), nil
default:
return nil, &rpcError{Code: errMethodNotFound, Message: "method not supported"}
}
}
func (server *Server) initialize(params json.RawMessage) (any, *rpcError) {
var request struct {
RootURI string `json:"rootUri"`
RootPath string `json:"rootPath"`
WorkspaceFolders []struct {
URI string `json:"uri"`
} `json:"workspaceFolders"`
}
if err := json.Unmarshal(params, &request); err != nil {
return nil, &rpcError{Code: errInvalidParams, Message: "invalid initialize parameters"}
}
if len(request.WorkspaceFolders) > 1 {
return nil, &rpcError{Code: errInvalidParams, Message: "Hime-san accepts one workspace folder per language-server process"}
}
rootURI := request.RootURI
if len(request.WorkspaceFolders) == 1 {
rootURI = request.WorkspaceFolders[0].URI
}
var root string
var err error
if rootURI != "" {
root, err = fileURIToPath(rootURI)
} else if request.RootPath != "" {
root, err = filepath.Abs(request.RootPath)
} else {
root, err = os.Getwd()
}
if err != nil {
return nil, &rpcError{Code: errInvalidParams, Message: "workspace root is not a local filesystem path"}
}
info, err := os.Lstat(root)
if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return nil, &rpcError{Code: errInvalidParams, Message: "workspace root must be an existing non-symlink directory"}
}
evaluated, err := filepath.EvalSymlinks(root)
if err != nil || filepath.Clean(evaluated) != filepath.Clean(root) {
return nil, &rpcError{Code: errInvalidParams, Message: "workspace roots reached through symlinks are not supported"}
}
server.mu.Lock()
if server.initialized {
server.mu.Unlock()
return nil, &rpcError{Code: errInvalidRequest, Message: "initialize may be sent only once"}
}
server.root = filepath.Clean(root)
server.initialized = true
server.mu.Unlock()
return struct {
Capabilities any `json:"capabilities"`
ServerInfo any `json:"serverInfo"`
}{
Capabilities: map[string]any{
"positionEncoding": "utf-16",
"textDocumentSync": map[string]any{"openClose": true, "change": 1, "save": map[string]any{"includeText": true}},
"completionProvider": map[string]any{"triggerCharacters": []string{"<", "?", "~", "."}, "resolveProvider": false},
"hoverProvider": true, "definitionProvider": true, "documentSymbolProvider": true,
"workspace": map[string]any{"workspaceFolders": map[string]any{"supported": false, "changeNotifications": false}},
},
ServerInfo: map[string]any{"name": "himesan", "version": compiler.CompilerVersion},
}, nil
}
func (server *Server) handleNotification(method string, params json.RawMessage) {
switch method {
case "initialized":
server.scheduleReindex(false)
case "$/cancelRequest":
var request struct {
ID json.RawMessage `json:"id"`
}
if json.Unmarshal(params, &request) == nil {
server.mu.RLock()
cancel := server.requests[string(request.ID)]
server.mu.RUnlock()
if cancel != nil {
cancel()
}
}
case "textDocument/didOpen":
var request struct {
TextDocument struct {
URI string `json:"uri"`
Version int `json:"version"`
Text string `json:"text"`
} `json:"textDocument"`
}
if json.Unmarshal(params, &request) == nil {
server.updateOverlay(request.TextDocument.URI, request.TextDocument.Version, request.TextDocument.Text, true)
server.scheduleReindex(false)
}
case "textDocument/didChange":
var request struct {
TextDocument versionedTextDocumentIdentifier `json:"textDocument"`
ContentChanges []struct {
Range *Range `json:"range,omitempty"`
Text string `json:"text"`
} `json:"contentChanges"`
}
if json.Unmarshal(params, &request) == nil && len(request.ContentChanges) != 0 {
change := request.ContentChanges[len(request.ContentChanges)-1]
if change.Range == nil {
server.updateOverlay(request.TextDocument.URI, request.TextDocument.Version, change.Text, true)
server.scheduleReindex(true)
}
}
case "textDocument/didSave":
var request struct {
TextDocument textDocumentIdentifier `json:"textDocument"`
Text *string `json:"text,omitempty"`
}
if json.Unmarshal(params, &request) == nil {
if request.Text != nil {
server.updateOverlay(request.TextDocument.URI, -1, *request.Text, true)
}
server.scheduleReindex(false)
}
case "textDocument/didClose":
var request struct {
TextDocument textDocumentIdentifier `json:"textDocument"`
}
if json.Unmarshal(params, &request) == nil {
if path, err := fileURIToPath(request.TextDocument.URI); err == nil {
server.mu.Lock()
delete(server.overlays, path)
server.mu.Unlock()
server.scheduleReindex(false)
}
}
case "workspace/didChangeWatchedFiles":
server.scheduleReindex(false)
}
}
func (server *Server) updateOverlay(uri string, version int, text string, open bool) {
if len(text) > maxDocumentBytes || !strings.HasSuffix(strings.ToLower(uri), ".sando") {
return
}
path, err := fileURIToPath(uri)
if err != nil {
return
}
server.mu.Lock()
defer server.mu.Unlock()
if !editorPathAllowed(server.root, path) {
return
}
if previous, ok := server.overlays[path]; ok && version < 0 {
version = previous.Version
}
server.overlays[path] = document{URI: uri, Path: path, Text: []byte(text), Version: version, Open: open}
}
func (server *Server) notify(method string, params any) error {
payload, err := json.Marshal(params)
if err != nil {
return err
}
return server.writer.write(rpcMessage{JSONRPC: "2.0", Method: method, Params: payload})
}
func (server *Server) log(message string, count int) {
// Logs deliberately contain only fixed messages and counts. Source text,
// paths, environment values, and process details never cross this boundary.
fmt.Fprintf(server.logs, "himesan lsp: %s (%d)\n", message, count)
}
+317
View File
@@ -0,0 +1,317 @@
// SPDX-License-Identifier: AGPL-3.0-only
package lsp
import (
"bytes"
"context"
"encoding/json"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"gamertan.com/sandwich-hime/internal/compiler"
)
type protocolClient struct {
input *io.PipeWriter
output *frameReader
nextID int
lastPayload []byte
}
func newProtocolClient(t *testing.T, root string) (*protocolClient, <-chan error) {
t.Helper()
serverInput, clientInput := io.Pipe()
clientOutput, serverOutput := io.Pipe()
done := make(chan error, 1)
go func() {
done <- Run(context.Background(), Options{Input: serverInput, Output: serverOutput, LogOutput: io.Discard, Debounce: 10 * time.Millisecond})
_ = serverOutput.Close()
}()
client := &protocolClient{input: clientInput, output: newFrameReader(clientOutput)}
response := client.call(t, "initialize", map[string]any{"rootUri": pathToURI(root)})
if response.Error != nil {
t.Fatalf("initialize: %#v", response.Error)
}
client.notify(t, "initialized", map[string]any{})
return client, done
}
func (client *protocolClient) send(t *testing.T, message rpcMessage) {
t.Helper()
payload, err := json.Marshal(message)
if err != nil {
t.Fatal(err)
}
frame := append([]byte("Content-Length: "+itoa(len(payload))+"\r\n\r\n"), payload...)
if _, err := client.input.Write(frame); err != nil {
t.Fatal(err)
}
}
func (client *protocolClient) call(t *testing.T, method string, params any) rpcMessage {
t.Helper()
client.nextID++
id := client.nextID
payload, err := json.Marshal(params)
if err != nil {
t.Fatal(err)
}
client.send(t, rpcMessage{JSONRPC: "2.0", ID: json.RawMessage(itoa(id)), Method: method, Params: payload})
for {
message := client.read(t)
if string(message.ID) == itoa(id) {
return message
}
}
}
func (client *protocolClient) notify(t *testing.T, method string, params any) {
t.Helper()
payload, err := json.Marshal(params)
if err != nil {
t.Fatal(err)
}
client.send(t, rpcMessage{JSONRPC: "2.0", Method: method, Params: payload})
}
func (client *protocolClient) read(t *testing.T) rpcMessage {
t.Helper()
type result struct {
message rpcMessage
payload []byte
err error
}
ready := make(chan result, 1)
go func() {
payload, err := client.output.read()
if err != nil {
ready <- result{err: err}
return
}
var message rpcMessage
err = json.Unmarshal(payload, &message)
ready <- result{message: message, payload: payload, err: err}
}()
select {
case got := <-ready:
if got.err != nil {
t.Fatal(got.err)
}
client.lastPayload = got.payload
return got.message
case <-time.After(3 * time.Second):
t.Fatal("timed out waiting for language-server response")
return rpcMessage{}
}
}
func (client *protocolClient) waitDiagnostics(t *testing.T, uri string, wantCode string) []lspDiagnostic {
t.Helper()
deadline := time.Now().Add(3 * time.Second)
for time.Now().Before(deadline) {
message := client.read(t)
if message.Method != "textDocument/publishDiagnostics" {
continue
}
var published struct {
URI string `json:"uri"`
Diagnostics []lspDiagnostic `json:"diagnostics"`
}
if json.Unmarshal(message.Params, &published) != nil || published.URI != uri {
continue
}
if wantCode == "" {
return published.Diagnostics
}
for _, item := range published.Diagnostics {
if item.Code == wantCode {
return published.Diagnostics
}
}
}
t.Fatalf("timed out waiting for %s diagnostic", wantCode)
return nil
}
func TestServerOverlayFeaturesAndNoWrites(t *testing.T) {
root := t.TempDir()
writeTestFile(t, filepath.Join(root, "go.mod"), "module example.test/project\n\ngo 1.25\n")
homePath := filepath.Join(root, "home.sando")
badgePath := filepath.Join(root, "cards", "badge.sando")
writeTestFile(t, homePath, "<?sando go\npackage views\nfunc Home(visitor string)\n?>\n<p><?= visitor ?></p>\n")
writeTestFile(t, badgePath, "<?sando go\npackage cards\nfunc Badge(label string)\n?>\n<strong><?= label ?></strong>\n")
client, done := newProtocolClient(t, root)
homeURI := pathToURI(homePath)
client.waitDiagnostics(t, homeURI, "")
overlay := "<?sando go\npackage views\nimport \"example.test/project/cards\"\nfunc Home(visitor string)\n?>\n<p>😀 <?= visitor ?></p>\n<?~ cards.Badge(\"new\") ?>\n"
client.notify(t, "textDocument/didOpen", map[string]any{"textDocument": map[string]any{"uri": homeURI, "languageId": "sando", "version": 1, "text": overlay}})
if diagnostics := client.waitDiagnostics(t, homeURI, ""); len(diagnostics) != 0 {
t.Fatalf("valid overlay diagnostics = %#v", diagnostics)
}
completionOffset := strings.Index(overlay, "cards.Badge") + len("cards.B")
completion := client.call(t, "textDocument/completion", textDocumentPositionParams{TextDocument: textDocumentIdentifier{URI: homeURI}, Position: offsetToPosition([]byte(overlay), completionOffset)})
assertJSONContains(t, completion.Result, `"label":"cards.Badge"`)
definitionOffset := strings.Index(overlay, "Badge") + 2
definition := client.call(t, "textDocument/definition", textDocumentPositionParams{TextDocument: textDocumentIdentifier{URI: homeURI}, Position: offsetToPosition([]byte(overlay), definitionOffset)})
assertJSONContains(t, definition.Result, pathToURI(badgePath))
hoverOffset := strings.Index(overlay, "visitor ?></p>") + 2
hover := client.call(t, "textDocument/hover", textDocumentPositionParams{TextDocument: textDocumentIdentifier{URI: homeURI}, Position: offsetToPosition([]byte(overlay), hoverOffset)})
assertJSONContains(t, hover.Result, "html-text")
symbols := client.call(t, "textDocument/documentSymbol", map[string]any{"textDocument": map[string]string{"uri": homeURI}})
assertJSONContains(t, symbols.Result, `"name":"Home"`)
if err := os.Remove(badgePath); err != nil {
t.Fatal(err)
}
client.notify(t, "workspace/didChangeWatchedFiles", map[string]any{"changes": []map[string]any{{"uri": pathToURI(badgePath), "type": 3}}})
client.waitDiagnostics(t, pathToURI(badgePath), "")
completion = client.call(t, "textDocument/completion", textDocumentPositionParams{TextDocument: textDocumentIdentifier{URI: homeURI}, Position: offsetToPosition([]byte(overlay), completionOffset)})
if payload, _ := json.Marshal(completion.Result); strings.Contains(string(payload), `"label":"cards.Badge"`) {
t.Fatalf("deleted component remained in completion index: %s", payload)
}
broken := strings.Replace(overlay, "<?~ cards.Badge(\"new\") ?>", "<div>", 1)
client.notify(t, "textDocument/didChange", map[string]any{
"textDocument": map[string]any{"uri": homeURI, "version": 2},
"contentChanges": []map[string]string{{"text": broken}},
})
client.waitDiagnostics(t, homeURI, "HIM1311")
if _, err := os.Stat(homePath + ".go"); !os.IsNotExist(err) {
t.Fatalf("language server wrote generated output: %v", err)
}
shutdown := client.call(t, "shutdown", map[string]any{})
if shutdown.Error != nil {
t.Fatalf("shutdown: %#v", shutdown.Error)
}
if !bytes.Contains(client.lastPayload, []byte(`"result":null`)) {
t.Fatalf("shutdown response omitted JSON-RPC null result: %s", client.lastPayload)
}
client.notify(t, "exit", map[string]any{})
_ = client.input.Close()
select {
case err := <-done:
if err != nil {
t.Fatal(err)
}
case <-time.After(3 * time.Second):
t.Fatal("language server did not exit")
}
}
func TestOverlayHonorsNestedModuleAndSymlinkBoundaries(t *testing.T) {
root := t.TempDir()
writeTestFile(t, filepath.Join(root, "go.mod"), "module example.test/root\n")
nestedPath := filepath.Join(root, "nested", "view.sando")
writeTestFile(t, filepath.Join(root, "nested", "go.mod"), "module example.test/nested\n")
writeTestFile(t, nestedPath, "<?sando go\npackage nested\nfunc View()\n?>\n<p>no</p>\n")
server := &Server{root: root, overlays: make(map[string]document)}
server.updateOverlay(pathToURI(nestedPath), 1, "ignored", true)
if len(server.overlays) != 0 {
t.Fatalf("nested-module overlay was accepted: %#v", server.overlays)
}
if runtime.GOOS != "windows" {
realDirectory := filepath.Join(root, "real")
if err := os.Mkdir(realDirectory, 0o700); err != nil {
t.Fatal(err)
}
linkDirectory := filepath.Join(root, "linked")
if err := os.Symlink(realDirectory, linkDirectory); err != nil {
t.Fatal(err)
}
linkedPath := filepath.Join(linkDirectory, "view.sando")
server.updateOverlay(pathToURI(linkedPath), 1, "ignored", true)
if len(server.overlays) != 0 {
t.Fatalf("symlink overlay was accepted: %#v", server.overlays)
}
}
}
func TestServerRejectsMultipleRootsAndCanceledRequest(t *testing.T) {
root := t.TempDir()
server := &Server{initialized: true, snapshot: workspaceSnapshot{documents: map[string]document{}, analyses: map[string]compiler.DocumentAnalysis{}}}
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, rpcErr := server.handleRequest(ctx, "textDocument/completion", json.RawMessage(`{}`))
if rpcErr == nil || rpcErr.Code != errRequestCancelled {
t.Fatalf("canceled request error = %#v", rpcErr)
}
input, writer := io.Pipe()
reader, serverOutput := io.Pipe()
done := make(chan error, 1)
go func() { done <- Run(context.Background(), Options{Input: input, Output: serverOutput}) }()
client := &protocolClient{input: writer, output: newFrameReader(reader)}
response := client.call(t, "initialize", map[string]any{"workspaceFolders": []map[string]string{{"uri": pathToURI(root)}, {"uri": pathToURI(root)}}})
if response.Error == nil || response.Error.Code != errInvalidParams {
t.Fatalf("multiple-root response = %#v", response)
}
client.notify(t, "exit", map[string]any{})
_ = writer.Close()
<-done
}
func TestReindexCountsOpenOverlaysInWorkspaceLimit(t *testing.T) {
root := t.TempDir()
server := &Server{
root: root,
overlays: make(map[string]document, maxWorkspaceFiles+1),
snapshot: workspaceSnapshot{documents: map[string]document{}, analyses: map[string]compiler.DocumentAnalysis{}},
writer: &frameWriter{output: io.Discard},
logs: io.Discard,
}
for index := range maxWorkspaceFiles + 1 {
path := filepath.Join(root, "overlay-"+itoa(index)+".sando")
server.overlays[path] = document{URI: pathToURI(path), Path: path, Text: []byte("<?sando go\npackage views\nfunc View()\n?>\n"), Open: true}
}
err := server.reindex(context.Background())
if err == nil || !strings.Contains(err.Error(), "more than 10000") {
t.Fatalf("reindex overlay limit error = %v", err)
}
}
func writeTestFile(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
}
func assertJSONContains(t *testing.T, value any, marker string) {
t.Helper()
payload, err := json.Marshal(value)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(payload), marker) {
t.Fatalf("JSON %s does not contain %q", payload, marker)
}
}
func itoa(value int) string {
if value == 0 {
return "0"
}
var digits [20]byte
index := len(digits)
for value > 0 {
index--
digits[index] = byte('0' + value%10)
value /= 10
}
return string(digits[index:])
}
+466
View File
@@ -0,0 +1,466 @@
// SPDX-License-Identifier: AGPL-3.0-only
package lsp
import (
"context"
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"unicode/utf16"
"unicode/utf8"
"gamertan.com/sandwich-hime/internal/compiler"
)
const (
maxDocumentBytes = 16 << 20
maxWorkspaceBytes = 64 << 20
maxWorkspaceFiles = 10000
)
type document struct {
URI string
Path string
Text []byte
Version int
Open bool
}
type workspaceSnapshot struct {
documents map[string]document
analyses map[string]compiler.DocumentAnalysis
moduleRoot string
modulePath string
}
func (server *Server) reindex(ctx context.Context) error {
server.mu.RLock()
root := server.root
overlays := make(map[string]document, len(server.overlays))
for path, item := range server.overlays {
overlays[path] = item
}
server.mu.RUnlock()
if root == "" {
return nil
}
paths, discoveryDiagnostics := compiler.DiscoverSources(ctx, []string{root})
if err := ctx.Err(); err != nil {
return err
}
if len(paths) > maxWorkspaceFiles {
return fmt.Errorf("workspace contains more than %d .sando files", maxWorkspaceFiles)
}
documents := make(map[string]document, len(paths)+len(overlays))
total := 0
for _, path := range paths {
absolute, err := filepath.Abs(path)
if err != nil {
continue
}
absolute = filepath.Clean(absolute)
if overlay, ok := overlays[absolute]; ok {
documents[absolute] = overlay
total += len(overlay.Text)
continue
}
info, err := os.Lstat(absolute)
if err != nil || !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 || info.Size() > maxDocumentBytes {
continue
}
content, err := os.ReadFile(absolute)
if err != nil || len(content) > maxDocumentBytes {
continue
}
documents[absolute] = document{URI: pathToURI(absolute), Path: absolute, Text: content}
total += len(content)
if total > maxWorkspaceBytes {
return fmt.Errorf("workspace .sando sources exceed %d bytes", maxWorkspaceBytes)
}
}
for path, overlay := range overlays {
if _, ok := documents[path]; ok {
continue
}
if overlay.Open && editorPathAllowed(root, path) {
documents[path] = overlay
total += len(overlay.Text)
}
}
if len(documents) > maxWorkspaceFiles {
return fmt.Errorf("workspace contains more than %d .sando files", maxWorkspaceFiles)
}
if total > maxWorkspaceBytes {
return fmt.Errorf("workspace .sando sources exceed %d bytes", maxWorkspaceBytes)
}
inputs := make([]compiler.SourceInput, 0, len(documents))
for _, item := range documents {
inputs = append(inputs, compiler.SourceInput{Path: item.Path, Source: item.Text})
}
analysed := compiler.AnalyzeSources(ctx, inputs)
if err := ctx.Err(); err != nil {
return err
}
analyses := make(map[string]compiler.DocumentAnalysis, len(analysed))
for _, item := range analysed {
analyses[filepath.Clean(item.Path)] = item
}
moduleRoot, modulePath := moduleIdentity(root)
server.mu.Lock()
previous := server.snapshot.documents
server.snapshot = workspaceSnapshot{documents: documents, analyses: analyses, moduleRoot: moduleRoot, modulePath: modulePath}
server.mu.Unlock()
all := make(map[string]bool, len(previous)+len(documents))
for path := range previous {
all[path] = true
}
for path := range documents {
all[path] = true
}
ordered := make([]string, 0, len(all))
for path := range all {
ordered = append(ordered, path)
}
sort.Strings(ordered)
for _, path := range ordered {
item, exists := documents[path]
uri := pathToURI(path)
diagnostics := make([]lspDiagnostic, 0)
if exists {
uri = item.URI
for _, diagnostic := range analyses[path].Diagnostics {
diagnostics = append(diagnostics, diagnosticToLSP(item.Text, diagnostic))
}
}
if err := server.notify("textDocument/publishDiagnostics", struct {
URI string `json:"uri"`
Diagnostics []lspDiagnostic `json:"diagnostics"`
}{URI: uri, Diagnostics: diagnostics}); err != nil {
return err
}
}
if len(discoveryDiagnostics) != 0 {
server.log("workspace discovery reported boundary diagnostics", len(discoveryDiagnostics))
}
server.log("analysis completed", len(documents))
return nil
}
func (server *Server) scheduleReindex(delay bool) {
server.mu.Lock()
if server.analysisCancel != nil {
server.analysisCancel()
}
if server.analysisTimer != nil {
server.analysisTimer.Stop()
}
generation := server.analysisGeneration + 1
server.analysisGeneration = generation
wait := server.debounce
if !delay {
wait = 0
}
server.analysisTimer = server.afterFunc(wait, func() {
ctx, cancel := context.WithCancel(server.context)
server.mu.Lock()
if server.analysisGeneration != generation {
server.mu.Unlock()
cancel()
return
}
server.analysisWait.Add(1)
server.analysisCancel = cancel
server.mu.Unlock()
defer server.analysisWait.Done()
err := server.reindex(ctx)
cancel()
server.mu.Lock()
if server.analysisGeneration == generation {
server.analysisCancel = nil
}
server.mu.Unlock()
if err != nil && !errors.Is(err, context.Canceled) {
server.log("analysis failed", 1)
}
})
server.mu.Unlock()
}
func (server *Server) snapshotDocument(uri string) (document, compiler.DocumentAnalysis, bool) {
path, err := fileURIToPath(uri)
if err != nil {
return document{}, compiler.DocumentAnalysis{}, false
}
server.mu.RLock()
defer server.mu.RUnlock()
item, ok := server.snapshot.documents[path]
if !ok {
return document{}, compiler.DocumentAnalysis{}, false
}
return item, server.snapshot.analyses[path], true
}
func moduleIdentity(root string) (string, string) {
candidate := filepath.Join(root, "go.mod")
content, err := os.ReadFile(candidate)
if err != nil || len(content) > 1<<20 {
return "", ""
}
for _, line := range strings.Split(string(content), "\n") {
fields := strings.Fields(strings.TrimSpace(line))
if len(fields) == 2 && fields[0] == "module" {
return filepath.Clean(root), fields[1]
}
}
return "", ""
}
func (snapshot workspaceSnapshot) packageImportPath(directory string) string {
if snapshot.moduleRoot == "" || snapshot.modulePath == "" || !withinRoot(snapshot.moduleRoot, directory) {
return ""
}
relative, err := filepath.Rel(snapshot.moduleRoot, directory)
if err != nil || relative == "." {
return snapshot.modulePath
}
return strings.TrimSuffix(snapshot.modulePath, "/") + "/" + filepath.ToSlash(relative)
}
func (snapshot workspaceSnapshot) componentsFor(path string, analysis compiler.DocumentAnalysis) []componentTarget {
directory := filepath.Dir(path)
var targets []componentTarget
for targetPath, targetAnalysis := range snapshot.analyses {
if targetAnalysis.Component == "" {
continue
}
targetDirectory := filepath.Dir(targetPath)
if targetDirectory == directory && targetAnalysis.Package == analysis.Package {
targets = append(targets, componentTarget{Name: targetAnalysis.Component, Signature: targetAnalysis.Signature, Path: targetPath, Analysis: targetAnalysis})
continue
}
importPath := snapshot.packageImportPath(targetDirectory)
for _, imported := range analysis.Imports {
if imported.Path != importPath || imported.Alias == "_" || imported.Alias == "." {
continue
}
alias := imported.Alias
if alias == "" {
alias = targetAnalysis.Package
}
targets = append(targets, componentTarget{Qualifier: alias, Name: targetAnalysis.Component, Signature: targetAnalysis.Signature, Path: targetPath, Analysis: targetAnalysis})
}
}
sort.SliceStable(targets, func(i, j int) bool {
return targets[i].Label() < targets[j].Label()
})
return targets
}
type componentTarget struct {
Qualifier string
Name string
Signature string
Path string
Analysis compiler.DocumentAnalysis
}
func (target componentTarget) Label() string {
if target.Qualifier == "" {
return target.Name
}
return target.Qualifier + "." + target.Name
}
func withinRoot(root, path string) bool {
relative, err := filepath.Rel(filepath.Clean(root), filepath.Clean(path))
return err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) && !filepath.IsAbs(relative)
}
func editorPathAllowed(root, path string) bool {
if filepath.Ext(path) != ".sando" || !withinRoot(root, path) {
return false
}
relative, err := filepath.Rel(filepath.Clean(root), filepath.Clean(path))
if err != nil {
return false
}
current := filepath.Clean(root)
parts := strings.Split(relative, string(filepath.Separator))
for index, part := range parts {
if part == "" || part == "." {
continue
}
if index < len(parts)-1 && (part == ".git" || part == ".hg" || part == ".svn" || part == "vendor") {
return false
}
current = filepath.Join(current, part)
info, statErr := os.Lstat(current)
if statErr == nil && info.Mode()&os.ModeSymlink != 0 {
return false
}
if statErr != nil && !os.IsNotExist(statErr) {
return false
}
if index < len(parts)-1 && current != filepath.Clean(root) {
moduleInfo, moduleErr := os.Lstat(filepath.Join(current, "go.mod"))
if moduleErr == nil || moduleInfo != nil {
return false
}
if moduleErr != nil && !os.IsNotExist(moduleErr) {
return false
}
}
}
return true
}
func fileURIToPath(value string) (string, error) {
parsed, err := url.Parse(value)
if err != nil || parsed.Scheme != "file" || (parsed.Host != "" && parsed.Host != "localhost") {
return "", errors.New("only local file URIs are supported")
}
path, err := url.PathUnescape(parsed.EscapedPath())
if err != nil {
return "", err
}
if runtime.GOOS == "windows" && len(path) >= 3 && path[0] == '/' && path[2] == ':' {
path = path[1:]
}
absolute, err := filepath.Abs(filepath.FromSlash(path))
if err != nil {
return "", err
}
return filepath.Clean(absolute), nil
}
func pathToURI(path string) string {
absolute, err := filepath.Abs(path)
if err != nil {
absolute = filepath.Clean(path)
}
slashed := filepath.ToSlash(absolute)
if runtime.GOOS == "windows" && !strings.HasPrefix(slashed, "/") {
slashed = "/" + slashed
}
return (&url.URL{Scheme: "file", Path: slashed}).String()
}
func offsetToPosition(text []byte, offset int) Position {
if offset < 0 {
offset = 0
}
if offset > len(text) {
offset = len(text)
}
line, character := 0, 0
for index := 0; index < offset; {
if text[index] == '\n' {
line++
character = 0
index++
continue
}
r, size := utf8.DecodeRune(text[index:])
if r == utf8.RuneError && size == 1 {
character++
index++
continue
}
character += len(utf16.Encode([]rune{r}))
index += size
}
return Position{Line: line, Character: character}
}
func positionToOffset(text []byte, position Position) (int, bool) {
if position.Line < 0 || position.Character < 0 {
return 0, false
}
line := 0
start := 0
for start < len(text) && line < position.Line {
if text[start] == '\n' {
line++
}
start++
}
if line != position.Line {
return 0, false
}
units := 0
for index := start; index < len(text) && text[index] != '\n'; {
if units == position.Character {
return index, true
}
r, size := utf8.DecodeRune(text[index:])
if r == utf8.RuneError && size == 1 {
units++
index++
} else {
units += len(utf16.Encode([]rune{r}))
index += size
}
if units > position.Character {
return 0, false
}
}
if units == position.Character {
index := start
for index < len(text) && text[index] != '\n' {
index++
}
return index, true
}
return 0, false
}
func compilerPositionOffset(text []byte, line, column int) int {
if line < 1 {
line = 1
}
if column < 1 {
column = 1
}
start := 0
for current := 1; current < line && start < len(text); current++ {
newline := strings.IndexByte(string(text[start:]), '\n')
if newline < 0 {
return len(text)
}
start += newline + 1
}
offset := start + column - 1
if offset > len(text) {
offset = len(text)
}
return offset
}
func diagnosticToLSP(text []byte, diagnostic compiler.Diagnostic) lspDiagnostic {
startOffset := compilerPositionOffset(text, diagnostic.Line, diagnostic.Column)
endOffset := startOffset
if endOffset < len(text) {
_, size := utf8.DecodeRune(text[endOffset:])
if size < 1 {
size = 1
}
endOffset += size
}
severity := 1
if diagnostic.Severity == compiler.SeverityWarning {
severity = 2
}
return lspDiagnostic{
Range: Range{Start: offsetToPosition(text, startOffset), End: offsetToPosition(text, endOffset)},
Severity: severity, Code: diagnostic.Code, Source: "himesan", Message: diagnostic.Message,
}
}
+2
View File
@@ -17,6 +17,7 @@ func TestSelectCompilerVersion(t *testing.T) {
{name: "missing build info", linkerValue: developmentCompilerVersion, moduleVersion: "", want: developmentCompilerVersion}, {name: "missing build info", linkerValue: developmentCompilerVersion, moduleVersion: "", want: developmentCompilerVersion},
{name: "versioned go install", linkerValue: developmentCompilerVersion, moduleVersion: "v1.0.0", want: "v1.0.0"}, {name: "versioned go install", linkerValue: developmentCompilerVersion, moduleVersion: "v1.0.0", want: "v1.0.0"},
{name: "beta launch install", linkerValue: developmentCompilerVersion, moduleVersion: "v1.0.0-beta.1", want: "v1.0.0-beta.1"}, {name: "beta launch install", linkerValue: developmentCompilerVersion, moduleVersion: "v1.0.0-beta.1", want: "v1.0.0-beta.1"},
{name: "beta two install", linkerValue: developmentCompilerVersion, moduleVersion: "v1.0.0-beta.2", want: "v1.0.0-beta.2"},
{name: "versioned prerelease install", linkerValue: developmentCompilerVersion, moduleVersion: "v1.0.0-rc.1", want: "v1.0.0-rc.1"}, {name: "versioned prerelease install", linkerValue: developmentCompilerVersion, moduleVersion: "v1.0.0-rc.1", want: "v1.0.0-rc.1"},
{name: "hyphenated prerelease install", linkerValue: developmentCompilerVersion, moduleVersion: "v1.2.3-beta-2", want: "v1.2.3-beta-2"}, {name: "hyphenated prerelease install", linkerValue: developmentCompilerVersion, moduleVersion: "v1.2.3-beta-2", want: "v1.2.3-beta-2"},
{name: "pseudo version", linkerValue: developmentCompilerVersion, moduleVersion: "v0.0.0-20260811120000-0123456789ab", want: developmentCompilerVersion}, {name: "pseudo version", linkerValue: developmentCompilerVersion, moduleVersion: "v0.0.0-20260811120000-0123456789ab", want: developmentCompilerVersion},
@@ -30,6 +31,7 @@ func TestSelectCompilerVersion(t *testing.T) {
{name: "leading zero numeric beta identifier", linkerValue: developmentCompilerVersion, moduleVersion: "v1.0.0-beta.01", want: developmentCompilerVersion}, {name: "leading zero numeric beta identifier", linkerValue: developmentCompilerVersion, moduleVersion: "v1.0.0-beta.01", want: developmentCompilerVersion},
{name: "empty beta identifier", linkerValue: developmentCompilerVersion, moduleVersion: "v1.0.0-beta..1", want: developmentCompilerVersion}, {name: "empty beta identifier", linkerValue: developmentCompilerVersion, moduleVersion: "v1.0.0-beta..1", want: developmentCompilerVersion},
{name: "beta linker override wins", linkerValue: "v1.0.0-beta.1", moduleVersion: "(devel)", want: "v1.0.0-beta.1"}, {name: "beta linker override wins", linkerValue: "v1.0.0-beta.1", moduleVersion: "(devel)", want: "v1.0.0-beta.1"},
{name: "beta two linker override wins", linkerValue: "v1.0.0-beta.2", moduleVersion: "(devel)", want: "v1.0.0-beta.2"},
{name: "linker override wins", linkerValue: "v1.0.0-rc.1", moduleVersion: "v1.0.0", want: "v1.0.0-rc.1"}, {name: "linker override wins", linkerValue: "v1.0.0-rc.1", moduleVersion: "v1.0.0", want: "v1.0.0-rc.1"},
} }
for _, test := range tests { for _, test := range tests {
+1 -1
View File
@@ -8,7 +8,7 @@ These scripts are intentionally understandable shell and PowerShell rather than
- `verify.ps1` provides the equivalent native Windows lane; pass `-Race` to include the race detector. - `verify.ps1` provides the equivalent native Windows lane; pass `-Race` to include the race detector.
- `check-licenses.sh` enforces the AGPL compiler / Apache runtime boundary and prevents generated application Go from inheriting an AGPL identifier. - `check-licenses.sh` enforces the AGPL compiler / Apache runtime boundary and prevents generated application Go from inheriting an AGPL identifier.
- `release-check.sh --version vX.Y.Z` is a clean-checkout technical preflight, including exact candidate-version and generated-provenance checks. Beta publication follows the narrower prerelease gates in `RELEASE.md`; release candidates and final v1 additionally use `--public` with a human-reviewed `HIMESAN_RELEASE_EVIDENCE_DIR`. The script never tags, pushes, publishes, or deploys. - `release-check.sh --version vX.Y.Z` is a clean-checkout technical preflight, including exact candidate-version and generated-provenance checks. Beta publication follows the narrower prerelease gates in `RELEASE.md`; release candidates and final v1 additionally use `--public` with a human-reviewed `HIMESAN_RELEASE_EVIDENCE_DIR`. The script never tags, pushes, publishes, or deploys.
- `verify-public-install.sh --version vX.Y.Z` is a post-tag/publication check. It verifies exact `go-get=1` package routes and runs the documented compiler install and runtime get from fresh direct-fetch and public-proxy caches without interactive Git credentials. - `verify-public-install.sh --version vX.Y.Z` is a post-tag/publication check. It verifies exact `go-get=1` package routes, adds the nested runtime before installing the parent compiler, and exercises fresh direct-fetch and public-proxy caches without interactive Git credentials.
The canonical Linux CI and release preflight also run bounded fuzz sessions for the parser/context compiler and Go-aware delimiter scanner. Seed-corpus execution remains part of ordinary `go test`; the bounded sessions are extra evidence, not a substitute for longer scheduled fuzzing before v1. The canonical Linux CI and release preflight also run bounded fuzz sessions for the parser/context compiler and Go-aware delimiter scanner. Seed-corpus execution remains part of ordinary `go test`; the bounded sessions are extra evidence, not a substitute for longer scheduled fuzzing before v1.
+69 -7
View File
@@ -8,13 +8,14 @@ cd "$repo_root"
usage() { usage() {
cat <<'EOF' cat <<'EOF'
Usage: scripts/release-check.sh --version vX.Y.Z [--public] Usage: scripts/release-check.sh --version vX.Y.Z [--runtime-tag sando/vX.Y.Z] [--public]
Runs a read-only release preflight. It never creates tags, commits, release Runs a read-only release preflight. It never creates tags, commits, release
artifacts in the repository, pushes, or deploys. artifacts in the repository, pushes, or deploys.
--version Candidate compiler version. The corresponding runtime tag is --version Candidate compiler version.
sando/vX.Y.Z. --runtime-tag Existing runtime tag retained by a compiler-only release.
Omit only when publishing a matching new runtime tag.
--public Require the human-reviewed RC/final launch evidence bundle named --public Require the human-reviewed RC/final launch evidence bundle named
by HIMESAN_RELEASE_EVIDENCE_DIR. Canonical beta prereleases may by HIMESAN_RELEASE_EVIDENCE_DIR. Canonical beta prereleases may
run their narrower publication preflight without this flag. run their narrower publication preflight without this flag.
@@ -22,6 +23,7 @@ EOF
} }
version='' version=''
runtime_tag=''
public_release=0 public_release=0
while (( $# > 0 )); do while (( $# > 0 )); do
case "$1" in case "$1" in
@@ -30,6 +32,11 @@ while (( $# > 0 )); do
version=$2 version=$2
shift 2 shift 2
;; ;;
--runtime-tag)
[[ $# -ge 2 ]] || { usage >&2; exit 2; }
runtime_tag=$2
shift 2
;;
--public) --public)
public_release=1 public_release=1
shift shift
@@ -74,7 +81,32 @@ if (( public_release == 0 && beta_release == 0 )); then
exit 2 exit 2
fi fi
runtime_tag="sando/$version" paired_runtime_tag="sando/$version"
compiler_only=0
if [[ -z "$runtime_tag" ]]; then
runtime_tag=$paired_runtime_tag
elif [[ ! "$runtime_tag" =~ ^sando/v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$ ]]; then
printf 'error: --runtime-tag must be a canonical sando/v semantic version without build metadata\n' >&2
exit 2
else
runtime_prerelease=${BASH_REMATCH[5]:-}
if [[ "$runtime_prerelease" =~ (^|[.-])(0\.)?[0-9]{14}-[0-9a-f]{12,}$ ]]; then
printf 'error: --runtime-tag must identify a signed release, not a Go pseudo-version\n' >&2
exit 2
fi
if [[ -n "$runtime_prerelease" ]]; then
IFS=. read -r -a runtime_prerelease_identifiers <<<"$runtime_prerelease"
for identifier in "${runtime_prerelease_identifiers[@]}"; do
if [[ "$identifier" =~ ^[0-9]+$ && "$identifier" =~ ^0[0-9]+$ ]]; then
printf 'error: runtime numeric prerelease identifiers must not contain leading zeroes: %s\n' "$identifier" >&2
exit 2
fi
done
fi
if [[ "$runtime_tag" != "$paired_runtime_tag" ]]; then
compiler_only=1
fi
fi
if [[ -n "$(git status --porcelain=v1 --untracked-files=all)" ]]; then if [[ -n "$(git status --porcelain=v1 --untracked-files=all)" ]]; then
printf 'error: release preflight requires a clean canonical checkout\n' >&2 printf 'error: release preflight requires a clean canonical checkout\n' >&2
@@ -99,7 +131,11 @@ if [[ "$branch" != main ]]; then
exit 1 exit 1
fi fi
for tag in "$version" "$runtime_tag"; do candidate_tags=("$version")
if (( compiler_only == 0 )); then
candidate_tags+=("$runtime_tag")
fi
for tag in "${candidate_tags[@]}"; do
if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then
printf 'error: candidate tag already exists locally: %s\n' "$tag" >&2 printf 'error: candidate tag already exists locally: %s\n' "$tag" >&2
exit 1 exit 1
@@ -114,6 +150,26 @@ for tag in "$version" "$runtime_tag"; do
fi fi
done done
if (( compiler_only == 1 )); then
if ! runtime_refs=$(git ls-remote --tags origin "refs/tags/$runtime_tag" "refs/tags/$runtime_tag^{}" 2>/dev/null); then
printf 'error: could not verify retained runtime tag against canonical origin: %s\n' "$runtime_tag" >&2
exit 1
fi
runtime_commit=$(printf '%s\n' "$runtime_refs" | awk '$2 ~ /\^\{\}$/ {print $1}')
if [[ -z "$runtime_commit" || ! "$runtime_commit" =~ ^[0-9a-f]{40}$ ]]; then
printf 'error: retained runtime tag is absent or is not annotated: %s\n' "$runtime_tag" >&2
exit 1
fi
if ! git cat-file -e "$runtime_commit^{commit}" 2>/dev/null; then
printf 'error: retained runtime tag commit is not present in canonical history: %s\n' "$runtime_tag" >&2
exit 1
fi
if [[ "$(git rev-parse HEAD:sando)" != "$(git rev-parse "$runtime_commit:sando")" ]]; then
printf 'error: compiler-only release changed the sando subtree retained at %s\n' "$runtime_tag" >&2
exit 1
fi
fi
artifact_dir=$(mktemp -d "${TMPDIR:-/tmp}/himesan-release-check.XXXXXXXX") artifact_dir=$(mktemp -d "${TMPDIR:-/tmp}/himesan-release-check.XXXXXXXX")
cleanup() { cleanup() {
if [[ -n "${artifact_dir:-}" && -d "$artifact_dir" ]]; then if [[ -n "${artifact_dir:-}" && -d "$artifact_dir" ]]; then
@@ -135,7 +191,7 @@ if [[ "$actual_human_version" != "$expected_human_version" ]]; then
"$expected_human_version" "$actual_human_version" >&2 "$expected_human_version" "$actual_human_version" >&2
exit 1 exit 1
fi fi
expected_json_version=$(printf '{"compiler":"%s","runtime_abi":"sando.v1","go":"%s"}' "$version" "$candidate_go_version") expected_json_version=$(printf '{"compiler":"%s","runtime_abi":"sando.v1","go":"%s","features":["lsp-stdio"]}' "$version" "$candidate_go_version")
actual_json_version=$("$candidate_binary" version --json) actual_json_version=$("$candidate_binary" version --json)
if [[ "$actual_json_version" != "$expected_json_version" ]]; then if [[ "$actual_json_version" != "$expected_json_version" ]]; then
printf 'error: candidate JSON version mismatch\nexpected: %s\nactual: %s\n' \ printf 'error: candidate JSON version mismatch\nexpected: %s\nactual: %s\n' \
@@ -187,6 +243,8 @@ HIMESAN_RACE=1 ./scripts/verify.sh
printf '\n==> bounded compiler fuzz gates\n' printf '\n==> bounded compiler fuzz gates\n'
go test ./internal/compiler -run '^$' -fuzz '^FuzzCompileNeverPanics$' -fuzztime=20s go test ./internal/compiler -run '^$' -fuzz '^FuzzCompileNeverPanics$' -fuzztime=20s
go test ./internal/compiler -run '^$' -fuzz '^FuzzGoDelimiterNeverPanics$' -fuzztime=20s go test ./internal/compiler -run '^$' -fuzz '^FuzzGoDelimiterNeverPanics$' -fuzztime=20s
go test ./internal/lsp -run '^$' -fuzz '^FuzzFrameReaderNeverPanics$' -fuzztime=20s
go test ./internal/lsp -run '^$' -fuzz '^FuzzDocumentPositionNeverPanics$' -fuzztime=20s
printf '\n==> vulnerability scan (pinned golang.org/x/vuln v1.6.0)\n' printf '\n==> vulnerability scan (pinned golang.org/x/vuln v1.6.0)\n'
go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./... go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
@@ -256,4 +314,8 @@ else
printf '\nBeta technical publication preflight passed. This does not establish RC/final launch evidence or production stability.\n' printf '\nBeta technical publication preflight passed. This does not establish RC/final launch evidence or production stability.\n'
fi fi
printf 'No tag, push, publication, or deployment was performed for %s / %s.\n' "$version" "$runtime_tag" if (( compiler_only == 1 )); then
printf 'No tag, push, publication, or deployment was performed for %s; runtime remains %s.\n' "$version" "$runtime_tag"
else
printf 'No tag, push, publication, or deployment was performed for %s / %s.\n' "$version" "$runtime_tag"
fi
+24 -23
View File
@@ -103,6 +103,7 @@ fi
scratch_dir=$(mktemp -d "${TMPDIR:-/tmp}/himesan-public-install.XXXXXXXX") scratch_dir=$(mktemp -d "${TMPDIR:-/tmp}/himesan-public-install.XXXXXXXX")
cleanup() { cleanup() {
if [[ -n "${scratch_dir:-}" && -d "$scratch_dir" ]]; then if [[ -n "${scratch_dir:-}" && -d "$scratch_dir" ]]; then
chmod -R u+w -- "$scratch_dir" 2>/dev/null || true
rm -rf -- "$scratch_dir" rm -rf -- "$scratch_dir"
fi fi
} }
@@ -116,7 +117,29 @@ run_install_pair() {
local installed_binary installed_version go_executable_suffix local installed_binary installed_version go_executable_suffix
mkdir -p "$mode_dir/gopath" "$mode_dir/modcache" "$mode_dir/buildcache" "$mode_dir/consumer" mkdir -p "$mode_dir/gopath" "$mode_dir/modcache" "$mode_dir/buildcache" "$mode_dir/consumer"
printf '\n==> %s clean-cache install\n' "$mode" printf '\n==> %s clean-cache runtime then compiler install\n' "$mode"
(
cd "$mode_dir/consumer"
go mod init example.invalid/himesan-public-install >/dev/null
env \
GIT_TERMINAL_PROMPT=0 \
GIT_CONFIG_NOSYSTEM=1 \
GIT_CONFIG_GLOBAL=/dev/null \
GIT_ASKPASS="$false_command" \
SSH_ASKPASS="$false_command" \
GOPATH="$mode_dir/gopath" \
GOMODCACHE="$mode_dir/modcache" \
GOCACHE="$mode_dir/buildcache" \
GOPROXY="$proxy" \
GOPRIVATE= \
GONOPROXY=none \
GONOSUMDB="$no_sum_db" \
GOSUMDB=sum.golang.org \
GOINSECURE= \
GOAUTH=off \
go get "gamertan.com/sandwich-hime/sando@$version"
)
env \ env \
GIT_TERMINAL_PROMPT=0 \ GIT_TERMINAL_PROMPT=0 \
GIT_CONFIG_NOSYSTEM=1 \ GIT_CONFIG_NOSYSTEM=1 \
@@ -154,28 +177,6 @@ EOF
printf 'error: generated provenance did not record installed compiler version %s\n' "$version" >&2 printf 'error: generated provenance did not record installed compiler version %s\n' "$version" >&2
exit 1 exit 1
fi fi
(
cd "$mode_dir/consumer"
go mod init example.invalid/himesan-public-install >/dev/null
env \
GIT_TERMINAL_PROMPT=0 \
GIT_CONFIG_NOSYSTEM=1 \
GIT_CONFIG_GLOBAL=/dev/null \
GIT_ASKPASS="$false_command" \
SSH_ASKPASS="$false_command" \
GOPATH="$mode_dir/gopath" \
GOMODCACHE="$mode_dir/modcache" \
GOCACHE="$mode_dir/buildcache" \
GOPROXY="$proxy" \
GOPRIVATE= \
GONOPROXY=none \
GONOSUMDB="$no_sum_db" \
GOSUMDB=sum.golang.org \
GOINSECURE= \
GOAUTH=off \
go get "gamertan.com/sandwich-hime/sando@$version"
)
} }
run_install_pair direct direct gamertan.com/sandwich-hime run_install_pair direct direct gamertan.com/sandwich-hime