Compare commits
9
Commits
v1.0.0-beta.2
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d978994b27 | ||
|
|
7e89ebfaba | ||
|
|
f5dcb8f10a | ||
|
|
a9d6f0951e | ||
|
|
3fb7fafd02 | ||
|
|
e730dd1b56 | ||
|
|
efd5b997ed | ||
|
|
c11552b87a | ||
|
|
4fef65f9b0 |
+84
-1
@@ -2,10 +2,93 @@
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
Sandwich Hime follows semantic versioning after final v1. Compiler and nested
|
Sandwich Hime follows semantic versioning from v1.0.0. 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 — 2026-09-09
|
||||||
|
|
||||||
|
The coordinated compiler `v1.0.0` and runtime `sando/v1.0.0` release retains
|
||||||
|
runtime ABI `sando.v1`. Canonical Gitea tags and release announcements establish
|
||||||
|
version and artifact availability.
|
||||||
|
|
||||||
|
### Stable release contract
|
||||||
|
|
||||||
|
- Apply the v1 compatibility policy to the source syntax, generated public API,
|
||||||
|
runtime API, CLI, diagnostics, and schemas on the maintained Linux/amd64 and
|
||||||
|
Apple Silicon macOS/arm64 targets.
|
||||||
|
- Promote runtime-first installation instructions to the exact final versions.
|
||||||
|
- Retain best-effort security fixes for the latest stable v1 patch, without an
|
||||||
|
SLA, independent-audit claim, or expanded platform support promise.
|
||||||
|
|
||||||
|
### Publication tooling
|
||||||
|
|
||||||
|
- Reject references to excluded operator documentation in exported Markdown,
|
||||||
|
with positive and negative regression fixtures.
|
||||||
|
- Keep operator/archive continuity out of the public roadmap and clarify
|
||||||
|
stable public publication history, immutable version tags and separate
|
||||||
|
private development storage.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Finalize the version 1.0 prospective individual CLA with recorded-license
|
||||||
|
grants, contributor safeguards, reciprocal stewardship commitments, and
|
||||||
|
explicit paired contributor/Steward acceptance for future contributions.
|
||||||
|
This does not assert that any contributor has signed, apply retroactively,
|
||||||
|
assign copyright, or require signatures to use or learn from the project.
|
||||||
|
Document commercial freedom and the compiler/runtime/application boundary
|
||||||
|
without changing the software licenses.
|
||||||
|
- Clarify Sandwich Hime as the primary identity and Hime-san as its tool name.
|
||||||
|
- Retain DCO sign-off and existing output-permission records alongside the
|
||||||
|
prospective CLA process; preserve prior contributions under their recorded
|
||||||
|
terms.
|
||||||
|
- Record the maintainer's explicit v1.0.0 deferral of offline restoration and
|
||||||
|
independent second-person recovery without representing those drills as done.
|
||||||
|
|
||||||
|
- Give the installable `himesan` command its own Go package overview covering
|
||||||
|
installation, generation, read-only checking, local development, editor
|
||||||
|
integration, and machine-readable operational contracts.
|
||||||
|
|
||||||
|
## v1.0.0-rc.1 — 2026-08-24
|
||||||
|
|
||||||
|
The first release candidate coordinates compiler `v1.0.0-rc.1` and runtime
|
||||||
|
`sando/v1.0.0-rc.1` at runtime ABI `sando.v1`. The intended v1 language,
|
||||||
|
generated API, runtime API, CLI, diagnostics, and schemas are frozen except for
|
||||||
|
release-blocking corrections.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Maintained native Linux/amd64 and Apple Silicon macOS/arm64 verification on
|
||||||
|
pinned Go 1.26.7 and Go 1.27.0 toolchains.
|
||||||
|
- Reproducible Linux and Darwin archives with checksums, SPDX SBOMs, manifests,
|
||||||
|
build provenance, and exact public-source identities.
|
||||||
|
- A Developer ID-signed, notarized, and stapled macOS disk image for the native
|
||||||
|
arm64 compiler.
|
||||||
|
- Exact contract snapshots, contextual-escaping differentials, real-browser
|
||||||
|
compiler and development-supervisor evidence, long fuzz evidence, and
|
||||||
|
repository-owned benchmark evidence.
|
||||||
|
- Standard-library-only LSP diagnostics, hover, document symbols, component
|
||||||
|
completion, and go-to-definition retained from Beta 2 as part of the frozen
|
||||||
|
v1 command surface.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Linux/amd64 and Apple Silicon macOS/arm64 are the maintained execution,
|
||||||
|
verification, artifact, and release targets. WSL, native Windows, Intel
|
||||||
|
macOS, Linux/arm64, and other targets remain useful portability surfaces but
|
||||||
|
are not v1 compatibility promises.
|
||||||
|
- The module language directive remains Go 1.25 for consumer compatibility;
|
||||||
|
maintained release builds use the pinned patched Go 1.26 and Go 1.27 lines.
|
||||||
|
- Release evidence is bound to the exact sanitized public commit and tree.
|
||||||
|
GitHub remains an untagged discovery snapshot; Gitea remains the source,
|
||||||
|
contribution, module, tag, and artifact authority.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- The native Windows PowerShell verifier and private multi-OS release-gate
|
||||||
|
workflow. Historical Windows evidence remains recorded without creating a
|
||||||
|
current support obligation.
|
||||||
|
|
||||||
## v1.0.0-beta.2 — 2026-08-12
|
## v1.0.0-beta.2 — 2026-08-12
|
||||||
|
|
||||||
Compiler-only release; the unchanged Apache runtime remains
|
Compiler-only release; the unchanged Apache runtime remains
|
||||||
|
|||||||
@@ -1,15 +1,212 @@
|
|||||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||||
|
|
||||||
# Individual contribution agreement — counsel-review draft
|
# Individual contribution and stewardship agreement
|
||||||
|
|
||||||
**This draft is not active, is not offered for signature, and creates no rights or obligations. Public pre-1.0 contributions use DCO 1.1 instead.**
|
**Version 1.0 — prospective contribution terms.** This version takes effect for
|
||||||
|
new contribution intake when first published in the canonical public repository.
|
||||||
|
An individual agreement is formed only through the explicit contributor and
|
||||||
|
Steward acceptance in section 9. Existing contributions remain under their
|
||||||
|
recorded licenses, DCO sign-offs and output permissions; publication does not
|
||||||
|
create retroactive acceptance. Nobody must sign this agreement to learn, install,
|
||||||
|
use or build applications with Sandwich Hime.
|
||||||
|
|
||||||
The intended agreement preserves contributor ownership. For each contribution, the contributor represents that they have the right to submit it and grants the project and recipients a perpetual, worldwide, non-exclusive, royalty-free copyright license to use, reproduce, modify, distribute, publicly perform, publicly display, and sublicense the contribution under the license identified by the contributed file.
|
Purpose: keep the shared project available for lawful use, study, improvement
|
||||||
|
and commercial work, without collecting contributors' ownership or giving
|
||||||
|
official stewards a private route around the project's copyleft. The practical
|
||||||
|
scope and practical limits are in
|
||||||
|
[Licensing intent](docs/LICENSING_INTENT.md).
|
||||||
|
|
||||||
The intended patent grant covers patent claims the contributor can license that are necessarily infringed by their contribution alone or in combination with the project as submitted. Patent litigation over the contribution terminates the patent grant to the extent counsel determines lawful and appropriate.
|
## 1. Parties and contribution scope
|
||||||
|
|
||||||
The intended agreement requires no copyright assignment. For AGPL-covered compiler contributions, it is not intended to grant a separate general right to proprietary-relicense contributor code; this does not restrict permissions already granted by a file's license, including Apache-2.0 or 0BSD. Emitted compiler scaffolding would need the narrow additional permission recorded in `OUTPUT_EXCEPTION.md`. The agreement does not promise that a contribution will be accepted or retained.
|
The parties are the individual identified in the acceptance record ("you") and
|
||||||
|
Cole Speelman, acting in his individual capacity as maintainer of Sandwich
|
||||||
|
Hime ("the Steward"). Sandwich Hime is the project name, not a separate legal
|
||||||
|
entity. An organization does not become a party merely because you work for it.
|
||||||
|
|
||||||
The contributor represents that disclosed material AI assistance was reviewed by a responsible human, that the human can make the same provenance and rights representations, and that no AI system is presented as the legal contributor.
|
A "Contribution" is material you intentionally submit for inclusion in this
|
||||||
|
repository after accepting this agreement, identified by a patch, commit or
|
||||||
|
pull request in the contribution record. Ordinary discussion, support requests,
|
||||||
|
confidential security reports, and material marked "not a contribution" are
|
||||||
|
not submissions under this agreement. Earlier work requires a separate,
|
||||||
|
explicit identification and acceptance; nothing applies retroactively by default.
|
||||||
|
|
||||||
If the project later considers activating a contribution agreement, qualified counsel review should address parties, definitions, governing law, notice, entity contributions, employer authorization, moral-rights treatment, patent language, and signature mechanics. Any active agreement would be announced prospectively. Until then, repository DCO sign-off is the operative contribution certification and does not activate this draft.
|
The "Recorded License" is the file license and any explicit additional
|
||||||
|
permission identified for that Contribution at submission, with the applicable
|
||||||
|
repository revision retained. The current map is [LICENSES.md](LICENSES.md):
|
||||||
|
AGPL-3.0-only for compiler/project material and Apache-2.0 for the nested
|
||||||
|
runtime. Existing third-party material keeps its own terms. A later edit to
|
||||||
|
the map does not change a Contribution's Recorded License.
|
||||||
|
|
||||||
|
## 2. Your ownership and the public grant
|
||||||
|
|
||||||
|
You retain ownership of rights you hold. You license your Contribution to the
|
||||||
|
Steward and recipients under its Recorded License, including that license's
|
||||||
|
copyright and patent provisions, conditions, duration, termination and cure
|
||||||
|
rules. No copyright assignment, exclusive license or agency is created.
|
||||||
|
|
||||||
|
There is no additional general grant to sublicense or relicense your work
|
||||||
|
under arbitrary terms. AGPL recipients obtain their rights under the AGPL;
|
||||||
|
this agreement is not an alternate proprietary license. Permissions already
|
||||||
|
granted by Apache-2.0 or another Recorded License are not narrowed here.
|
||||||
|
|
||||||
|
You remain free to use or license rights you actually own elsewhere. You may
|
||||||
|
stop making future contributions at any time. That does not withdraw rights
|
||||||
|
already granted to recipients who comply with the applicable license. No
|
||||||
|
contribution fee, royalty, revenue share or exclusivity is required.
|
||||||
|
|
||||||
|
## 3. Generated-output permission is specific
|
||||||
|
|
||||||
|
Contributor-owned scaffolding intended to be emitted into generated output
|
||||||
|
requires your explicit `Himesan-Output-Permission: v1.0` trailer on the signed-off
|
||||||
|
submission, as described in [OUTPUT_EXCEPTION.md](OUTPUT_EXCEPTION.md). That
|
||||||
|
record identifies the affected material and retains the exact permission text
|
||||||
|
and version. This is an additional copyright permission, not an additional
|
||||||
|
patent grant.
|
||||||
|
|
||||||
|
General CLA acceptance and DCO sign-off do not supply this separate grant.
|
||||||
|
Without it, the Steward must reject or redesign the contribution so that your
|
||||||
|
unpermitted material is not emitted. The grant does not license the compiler
|
||||||
|
as a whole permissively or give rights in material you do not own.
|
||||||
|
|
||||||
|
## 4. Authority, provenance and patents
|
||||||
|
|
||||||
|
You represent that you own, or have sufficient authorization to submit and
|
||||||
|
license, the Contribution under its Recorded License. Where an employer or
|
||||||
|
another rights holder has an interest, obtain the necessary permission before
|
||||||
|
submission. If that authority is uncertain, disclose the issue and withhold
|
||||||
|
the affected material; your signature cannot grant someone else's rights.
|
||||||
|
|
||||||
|
Identify incorporated third-party material, its source and known restrictions.
|
||||||
|
Disclose material AI assistance under the revision of
|
||||||
|
[AI_CONTRIBUTIONS.md](AI_CONTRIBUTIONS.md) identified in your acceptance record;
|
||||||
|
a responsible human must review the submission and make the representations.
|
||||||
|
Later policy edits do not amend these contractual representations without
|
||||||
|
express agreement under section 8.
|
||||||
|
Do not send confidential prompts, personal data or secrets as provenance.
|
||||||
|
Notify the Steward if you later learn a material representation was inaccurate.
|
||||||
|
|
||||||
|
The patent grant is the one in the Recorded License, including AGPL section 11
|
||||||
|
or Apache-2.0 section 3 as applicable. This agreement adds no patent assignment,
|
||||||
|
new retaliation trigger, or guarantee that no third party holds a patent. It
|
||||||
|
does not purport to license claims outside your authority.
|
||||||
|
|
||||||
|
## 5. Steward commitments: preserve the common project
|
||||||
|
|
||||||
|
In accepting Contributions under this agreement, the Steward undertakes to:
|
||||||
|
|
||||||
|
- Publish accepted material included in an official public release under its
|
||||||
|
Recorded License, with source available as that license requires. The
|
||||||
|
agreement does not promise immediate publication of every submitted patch.
|
||||||
|
- Preserve contributor copyright notices, required attribution, license and
|
||||||
|
permission records. Public squashes must not misrepresent authorship; keep
|
||||||
|
the original signed contribution record without publishing private intake data.
|
||||||
|
- Not use this agreement to remove copyleft from contributor-owned AGPL code,
|
||||||
|
issue an exclusive or proprietary license for that code, or sell a paid
|
||||||
|
exception to its source-sharing duties. The expressly recorded output
|
||||||
|
permission remains the narrow exception already described above.
|
||||||
|
- Not seek a private side agreement to evade that official stewardship
|
||||||
|
commitment. This is a promise about the Steward's official use of accepted
|
||||||
|
material, not a restriction on a contributor's independent use of their work.
|
||||||
|
- Give no sponsor, purchaser or voting majority authority through this
|
||||||
|
agreement to acquire contributor ownership or override existing grants.
|
||||||
|
- Before voluntarily transferring official stewardship, obtain the successor's
|
||||||
|
written assumption of these stewardship obligations. Delegating maintenance
|
||||||
|
does not itself release the Steward from contractual obligations or transfer
|
||||||
|
contributor copyrights.
|
||||||
|
|
||||||
|
These are commitments by the contracting Steward, not new downstream license
|
||||||
|
conditions. They do not bind unrelated forks or non-signing third parties,
|
||||||
|
restrict permissive-license freedoms, or confer authority over rights that
|
||||||
|
another person owns. Retained public licenses remain available independently
|
||||||
|
of changes in maintainers, sponsorship or repository ownership.
|
||||||
|
|
||||||
|
## 6. Contributor and maintainer safeguards
|
||||||
|
|
||||||
|
Contributions are voluntary. Neither party promises that a patch will be
|
||||||
|
accepted, merged unchanged, maintained indefinitely or kept in every future
|
||||||
|
version. Removing a feature does not revoke licenses already granted.
|
||||||
|
|
||||||
|
No employment, partnership, governance seat, support obligation, indemnity or
|
||||||
|
obligation to fund litigation is created. Warranty disclaimers and liability
|
||||||
|
limits remain those in the Recorded License, subject to mandatory law. You
|
||||||
|
are not asked to certify worldwide freedom from infringement or waive all
|
||||||
|
claims against the Steward.
|
||||||
|
|
||||||
|
No blanket moral-rights waiver is requested. To the extent law permits, you
|
||||||
|
consent to ordinary licensed editing, combination, compilation and distribution
|
||||||
|
of the Contribution, while retaining protection against false attribution or
|
||||||
|
endorsement. Neither party may use the other's name to imply an endorsement
|
||||||
|
that was not given. This is not a power to forbid lawful criticism or forks.
|
||||||
|
|
||||||
|
The Steward may coordinate compliance reports but receives no assignment,
|
||||||
|
power of attorney or assumed standing to litigate another owner's copyright.
|
||||||
|
Separate authority may be necessary for particular enforcement action.
|
||||||
|
|
||||||
|
## 7. Commercial freedom and license limits
|
||||||
|
|
||||||
|
Users may charge for lawful services, support, distribution and applications
|
||||||
|
subject to the relevant software licenses. A complying commercial competitor
|
||||||
|
or independently branded fork does not breach this agreement merely by being
|
||||||
|
successful, and upstream contribution is not mandatory.
|
||||||
|
|
||||||
|
No customer must sign this CLA to use the software. A blanket SaaS prohibition,
|
||||||
|
competition veto, moral-use test or mandatory payment to the project is not
|
||||||
|
added to the AGPL. Network-source duties come from the applicable license;
|
||||||
|
they do not automatically reach applications built with the compiler.
|
||||||
|
|
||||||
|
## 8. Changes, disputes and succession
|
||||||
|
|
||||||
|
Changing this document or the governance policy does not amend an accepted
|
||||||
|
agreement. Any amendment requires the affected parties' express agreement to
|
||||||
|
identified new terms; continued use of the software is not acceptance.
|
||||||
|
Contract disputes do not retract lawful downstream grants or add license
|
||||||
|
termination grounds beyond the relevant software license.
|
||||||
|
|
||||||
|
The proposed governing law is Ontario law and applicable federal Canadian law,
|
||||||
|
without excluding non-waivable rights or remedies available under applicable
|
||||||
|
law. The parties should first try good-faith written resolution where practical;
|
||||||
|
no mandatory arbitration, class-action waiver or bar to urgent relief is added.
|
||||||
|
This agreement makes no guarantee about a particular court's interpretation.
|
||||||
|
|
||||||
|
## 9. Acceptance and minimal records
|
||||||
|
|
||||||
|
Identify version 1.0 using an immutable canonical commit containing this text
|
||||||
|
and the incorporated AI policy, not a moving branch link. The contributor sends
|
||||||
|
the statement below with their name, date and identified contribution through
|
||||||
|
their canonical Gitea account. A public pull-request comment is sufficient if
|
||||||
|
the contributor chooses to make it public. For private acceptance, request a
|
||||||
|
private contact channel from the maintainer before sending personal details.
|
||||||
|
Do not post employer documents, private contact details or identity documents.
|
||||||
|
|
||||||
|
> I agree to the Sandwich Hime Individual contribution and stewardship agreement
|
||||||
|
> version 1.0 and its incorporated AI policy at canonical commit [full commit],
|
||||||
|
> for contribution [pull request or commit]. I intend this statement, sent from
|
||||||
|
> my account and signed with my name and date, as my electronic acceptance.
|
||||||
|
|
||||||
|
The Steward must reply with explicit matching acceptance of the identified
|
||||||
|
agreement and contribution before merging it. Retain both statements, their
|
||||||
|
dates/account identifiers, the exact agreement and policy texts with SHA-256
|
||||||
|
digests, and the contribution's Recorded License and source identifiers.
|
||||||
|
Neither a DCO sign-off, a merge, nor silence substitutes for this exchange.
|
||||||
|
Later contributions need their own acceptance unless both parties expressly
|
||||||
|
include future intentional submissions under that exact agreement version.
|
||||||
|
Every commit still requires DCO sign-off; affected output scaffolding still
|
||||||
|
requires its separate explicit permission.
|
||||||
|
|
||||||
|
Keep private acceptance and any necessary authority records in restricted
|
||||||
|
maintainer storage, not in the public source snapshot. Do not request a home
|
||||||
|
address, government identification or unrelated employer/customer records.
|
||||||
|
The public attribution and sign-offs the contributor intentionally submits are
|
||||||
|
distinct from private intake records. The Steward uses retained records only
|
||||||
|
to administer contributions, document rights and handle related disputes, and
|
||||||
|
does not sell them or repurpose contacts for marketing.
|
||||||
|
|
||||||
|
A contributor may request access, correction or deletion through the same
|
||||||
|
private channel. Retain only records reasonably necessary to substantiate grants
|
||||||
|
still relied upon or meet applicable legal obligations; delete unnecessary
|
||||||
|
copies and explain any retention needed when responding to a request.
|
||||||
|
Withdrawing from future participation does not revoke valid existing grants.
|
||||||
|
If an individual's authority does not cover employer-owned material, do not
|
||||||
|
accept it until the rights holder's authorization is recorded. A separate
|
||||||
|
entity agreement may be needed; this document does not bind an employer by
|
||||||
|
assumption.
|
||||||
|
|||||||
+15
-3
@@ -8,7 +8,19 @@ Repository maintainers may temporarily disable issue or patch intake during the
|
|||||||
pre-1.0 period; do not route around a closed intake channel by sending
|
pre-1.0 period; do not route around a closed intake channel by sending
|
||||||
unsolicited private patches.
|
unsolicited private patches.
|
||||||
|
|
||||||
Public pre-1.0 contributions use Developer Certificate of Origin 1.1 sign-off. The proposed `CLA.md` is an inactive draft, is not a condition of contribution, and creates no contributor or project obligations. If a contribution agreement is ever activated after legal review, the project will announce its prospective terms rather than silently applying the draft.
|
New public contributions use DCO 1.1 sign-off and the prospective
|
||||||
|
[Individual contribution and stewardship agreement, version 1.0](CLA.md).
|
||||||
|
The agreement starts with this version's first canonical publication and binds
|
||||||
|
only people who expressly accept it; no earlier contribution is retroactively
|
||||||
|
covered. [Section 9](CLA.md#9-acceptance-and-minimal-records) gives the short
|
||||||
|
contributor statement and matching maintainer acceptance. A public Gitea
|
||||||
|
pull-request comment can record it, or contributors can request a private
|
||||||
|
channel first. Never post private identity or employer documents.
|
||||||
|
|
||||||
|
The agreement preserves ownership, the existing public licenses and reciprocal
|
||||||
|
official-steward duties. See [Licensing intent](docs/LICENSING_INTENT.md).
|
||||||
|
Learning, downloading, using Hime, and building private or commercial applications
|
||||||
|
require no contribution agreement. Ask ordinary usage questions without signing.
|
||||||
|
|
||||||
For local work:
|
For local work:
|
||||||
|
|
||||||
@@ -20,7 +32,7 @@ go vet ./...
|
|||||||
./scripts/check-licenses.sh
|
./scripts/check-licenses.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Changes require focused tests, stable diagnostics, formatted generated goldens when applicable, documentation for public behavior, and a signed-off commit (`git commit -s`). The sign-off certifies the [DCO](DCO.txt); it is not a copyright assignment or acceptance of the inactive CLA. Do not commit production data, private application fixtures, secrets, build candidates, or developer cache files.
|
Changes require focused tests, stable diagnostics, formatted generated goldens when applicable, documentation for public behavior, and a signed-off commit (`git commit -s`). The sign-off certifies the [DCO](DCO.txt); it is not a copyright assignment or a substitute for the separate agreement acceptance. Do not commit production data, private application fixtures, secrets, build candidates, or developer cache files.
|
||||||
|
|
||||||
The project requires no copyright assignment. Ownership remains determined by applicable law and any employer or other agreement. Contributors submit each file under the license identified for that repository area, and the DCO records their certification that they have the right to do so. Material AI assistance must follow [AI_CONTRIBUTIONS.md](AI_CONTRIBUTIONS.md). Review considers provenance, safety, maintenance cost, compatibility, and fit—not just whether code passes tests.
|
The project requires no copyright assignment. Ownership remains determined by applicable law and any employer or other agreement. Contributors submit each file under the license identified for that repository area, and the DCO records their certification that they have the right to do so. Material AI assistance must follow [AI_CONTRIBUTIONS.md](AI_CONTRIBUTIONS.md). Review considers provenance, safety, maintenance cost, compatibility, and fit—not just whether code passes tests.
|
||||||
|
|
||||||
@@ -30,6 +42,6 @@ If a compiler contribution adds or changes contributor-owned scaffolding that Hi
|
|||||||
Himesan-Output-Permission: v1.0
|
Himesan-Output-Permission: v1.0
|
||||||
```
|
```
|
||||||
|
|
||||||
That trailer records the contributor's grant of the additional permission in [OUTPUT_EXCEPTION.md](OUTPUT_EXCEPTION.md) for the affected contribution. DCO sign-off does not supply that separate grant. Maintainers must preserve the signed grant in the private contribution record even when a sanitized public snapshot uses fresh history. A patch without it must not cause contributor-owned text to be emitted; maintainers must reject or redesign such a patch rather than assume permission.
|
That trailer records the contributor's grant of the additional permission in [OUTPUT_EXCEPTION.md](OUTPUT_EXCEPTION.md) for the affected contribution. DCO sign-off does not supply that separate grant. Maintainers must preserve the signed grant in the private contribution record even when development commits are squashed into a public publication commit. Public attribution must remain accurate; squashing does not transfer ownership. A patch without it must not cause contributor-owned text to be emitted; maintainers must reject or redesign such a patch rather than assume permission.
|
||||||
|
|
||||||
Potential vulnerabilities follow [SECURITY.md](SECURITY.md), not the ordinary contribution channel. Do not place confidential vulnerability details in an issue or patch description.
|
Potential vulnerabilities follow [SECURITY.md](SECURITY.md), not the ordinary contribution channel. Do not place confidential vulnerability details in an issue or patch description.
|
||||||
|
|||||||
@@ -11,4 +11,8 @@ Output must be deterministic for identical source, compiler version, runtime ABI
|
|||||||
|
|
||||||
The template/application author chooses the generated file's license to the extent they hold the necessary rights. A project-wide license may cover generated files because inline headers would be overwritten. Sandwich Hime adds provenance metadata, not an AGPL license identifier or a compiler copyright claim.
|
The template/application author chooses the generated file's license to the extent they hold the necessary rights. A project-wide license may cover generated files because inline headers would be overwritten. Sandwich Hime adds provenance metadata, not an AGPL license identifier or a compiler copyright claim.
|
||||||
|
|
||||||
|
The ordered v1 marker fields, compile-time ABI assertion, and source-map form
|
||||||
|
are machine-checked against
|
||||||
|
[`contracts/generated-provenance-v1.txt`](contracts/generated-provenance-v1.txt).
|
||||||
|
|
||||||
[OUTPUT_EXCEPTION.md](OUTPUT_EXCEPTION.md) is an additional permission for Cole Speelman-owned generator scaffolding copied into output. It is intended to remove licensing ambiguity without claiming that every generated file is or is not a derivative work. It does not cover third-party inputs, code copied manually from the compiler, other contributors' additions unless they grant the same permission, or the Apache-licensed runtime.
|
[OUTPUT_EXCEPTION.md](OUTPUT_EXCEPTION.md) is an additional permission for Cole Speelman-owned generator scaffolding copied into output. It is intended to remove licensing ambiguity without claiming that every generated file is or is not a derivative work. It does not cover third-party inputs, code copied manually from the compiler, other contributors' additions unless they grant the same permission, or the Apache-licensed runtime.
|
||||||
|
|||||||
+28
-2
@@ -4,10 +4,36 @@
|
|||||||
|
|
||||||
Sandwich Hime is founder-led. Cole Speelman is lead maintainer and has final authority over product direction, merges, releases, project identity, and security coordination. Authority is exercised through reviewable public releases, written rationale for material public decisions, and the published compatibility and release policies. Publishing a prerelease creates only the support commitment stated for that version; it does not transfer stewardship.
|
Sandwich Hime is founder-led. Cole Speelman is lead maintainer and has final authority over product direction, merges, releases, project identity, and security coordination. Authority is exercised through reviewable public releases, written rationale for material public decisions, and the published compatibility and release policies. Publishing a prerelease creates only the support commitment stated for that version; it does not transfer stewardship.
|
||||||
|
|
||||||
Pre-1.0 contributions use the DCO process described in `CONTRIBUTING.md`; the CLA draft is inactive. Acceptance is never guaranteed. Maintainers may close work that expands the product beyond an HTML-first, ahead-of-time Go template engine, weakens fail-closed safety, couples the runtime to a framework, or imposes unsustainable maintenance.
|
New contributions use the DCO and prospective versioned agreement process in
|
||||||
|
`CONTRIBUTING.md`. Acceptance is never guaranteed. Maintainers may close work
|
||||||
|
that expands the product beyond an HTML-first, ahead-of-time Go template engine,
|
||||||
|
weakens fail-closed safety, couples the runtime to a framework, or imposes
|
||||||
|
unsustainable maintenance.
|
||||||
|
|
||||||
Maintainers disclose relevant employment or financial conflicts and recuse when a conflict would compromise review. If the project reaches three maintainers, no single employer may control a majority of seats.
|
Maintainers disclose relevant employment or financial conflicts and recuse when a conflict would compromise review. If the project reaches three maintainers, no single employer may control a majority of seats.
|
||||||
|
|
||||||
Repository ownership, domain control, package namespace, release signing, and security recovery must not depend on a sponsor account. Before a supported v1.0.0 release, at least two independent humans must hold documented recovery capability. Succession may transfer official stewardship to a person or mission-aligned organization committed to the license map, public source releases, and trademark policy.
|
Repository ownership, domain control, package namespace, release signing, and security recovery must not depend on a sponsor account. Independent recovery capability remains a governance aim; the maintainer explicitly deferred offline restoration and second-person recovery/verification for v1.0.0. They remain unproven assurance follow-ups in `ROADMAP.md`, not a claim of resilient stewardship. Succession may transfer official stewardship to a person or mission-aligned organization committed to the license map, public source releases, and trademark policy.
|
||||||
|
|
||||||
The project does not accept copyright assignment as a condition of contribution. It will not represent that it can proprietary-relicense contributor-owned code unless it has permission from every rights holder whose permission is required. This governance statement does not alter rights already granted by an applicable file license.
|
The project does not accept copyright assignment as a condition of contribution. It will not represent that it can proprietary-relicense contributor-owned code unless it has permission from every rights holder whose permission is required. This governance statement does not alter rights already granted by an applicable file license.
|
||||||
|
|
||||||
|
## Shared value and independent stewardship
|
||||||
|
|
||||||
|
Official stewardship preserves the compiler's open, reciprocal licensing.
|
||||||
|
Commercial users, paid services and compliant independent forks are welcome;
|
||||||
|
funding does not purchase ownership, a governance seat, a veto, or authority to
|
||||||
|
erase attribution. Maintainers must not use a contribution agreement or private
|
||||||
|
side deal to remove copyleft from accepted contributor-owned AGPL work. The
|
||||||
|
existing runtime and output permissions remain deliberate separate boundaries.
|
||||||
|
|
||||||
|
Material licensing, governance and stewardship transfers require a public
|
||||||
|
rationale and respect for recorded rights; a vote or repository transfer cannot
|
||||||
|
supply someone else's copyright permission. A successor to a contribution
|
||||||
|
agreement must expressly assume its steward obligations. Preserve accessible
|
||||||
|
released source and provenance so lawful community continuation remains possible
|
||||||
|
without ownership of official domains, release accounts or marks. This is not a
|
||||||
|
promise of perpetual free hosting or a restriction on independent fork rights.
|
||||||
|
|
||||||
|
The reciprocal agreement requires explicit contributor and Steward acceptance.
|
||||||
|
Publication alone does not bind a contributor, an earlier submission or an
|
||||||
|
unrelated third party. No agreement is required to learn or use the software.
|
||||||
|
See [Licensing intent](docs/LICENSING_INTENT.md) for the distinctions and limits.
|
||||||
|
|||||||
+9
-2
@@ -7,9 +7,12 @@ Sandwich Hime deliberately separates the development tool from application runti
|
|||||||
|
|
||||||
| Path or material | License |
|
| Path or material | License |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Project-authored files in the repository root, `cmd/**`, `internal/**`, `docs/**`, and `scripts/**`, except the legal texts listed below | AGPL-3.0-only |
|
| Project-authored files in the repository root, `cmd/**`, `contracts/**`, `internal/**`, `docs/**`, and `scripts/**`, except the legal texts listed below | AGPL-3.0-only |
|
||||||
| Nested `sando/**` runtime module, except its verbatim license text | Apache-2.0 |
|
| Nested `sando/**` runtime module, except its verbatim license text | Apache-2.0 |
|
||||||
| `LICENSE`, `sando/LICENSE`, and `DCO.txt` | Their own stated copying terms and notices |
|
| `LICENSE`, `sando/LICENSE`, and `DCO.txt` | Their own stated copying terms and notices |
|
||||||
|
| `contracts/himesan-config-v1.schema.json` | AGPL-3.0-only |
|
||||||
|
| `contracts/himesan-operation-output-v1.schema.json` | AGPL-3.0-only |
|
||||||
|
| `contracts/himesan-version-output-v1.schema.json` | AGPL-3.0-only |
|
||||||
| User-authored `.sando` templates | Chosen by their author, subject to rights in their inputs |
|
| User-authored `.sando` templates | Chosen by their author, subject to rights in their inputs |
|
||||||
| Generated application `.sando.go` files | Chosen by the template/application author, subject to rights in their inputs and dependencies |
|
| Generated application `.sando.go` files | Chosen by the template/application author, subject to rights in their inputs and dependencies |
|
||||||
|
|
||||||
@@ -33,4 +36,8 @@ SPDX identifiers state the applicable license for comment-capable source and doc
|
|||||||
|
|
||||||
The snapshot exporter's `PUBLIC-SNAPSHOT.json` and `PUBLIC-SNAPSHOT.sha256` are generated factual provenance records and intentionally carry no inline SPDX comment. They do not change the license of any listed file.
|
The snapshot exporter's `PUBLIC-SNAPSHOT.json` and `PUBLIC-SNAPSHOT.sha256` are generated factual provenance records and intentionally carry no inline SPDX comment. They do not change the license of any listed file.
|
||||||
|
|
||||||
These are practical project licensing terms, not legal advice or a prediction of how every jurisdiction will classify a particular work. The inactive CLA draft and pre-registration trademark policy say so explicitly. Qualified legal review remains prudent before changing these terms, activating a CLA, registering marks, or making a fact-specific licensing decision; it is not represented as a prerequisite to publishing the current narrowly scoped public beta.
|
These are practical project licensing terms, not a prediction of how every
|
||||||
|
jurisdiction will classify a particular work. The prospective contribution
|
||||||
|
agreement and pre-registration trademark policy preserve that distinction.
|
||||||
|
Professional advice may help with a specific dispute or change in circumstances;
|
||||||
|
it is not a prerequisite to this release and is not claimed to have occurred.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"schema_version":2,"project":"sandwich-hime","export_policy":"exact-allowlist-v1","export_mode":"release","file_count":90,"allowlist_sha256":"393ee598dc7e12cdbb603887bf06599e46b40d7c19e4ff693a818cc32afb01ec","manifest_sha256":"655bfadbddf5967288147f5b1762debd3f4b106d86c044706c41cf6aabca5e00"}
|
{"schema_version":2,"project":"sandwich-hime","export_policy":"exact-allowlist-v1","export_mode":"release","file_count":128,"allowlist_sha256":"4f067a37de73082f3534ebfeb5a73b53b37ace9e4c6b21956405e2fefacbeb38","manifest_sha256":"90192f06c4ae7182834e5c1bc6890568698635b71530cb9593fb946ea2e6b3f4"}
|
||||||
|
|||||||
+72
-34
@@ -2,35 +2,46 @@
|
|||||||
658ba4b4645426f8c3249337f47669074ae9249a31703dcd9ea4c1afec45e20b ./.gitattributes
|
658ba4b4645426f8c3249337f47669074ae9249a31703dcd9ea4c1afec45e20b ./.gitattributes
|
||||||
d5ae411fb422b2388cac220f9655900eecbc49ece961b2bb2a6610347733b756 ./.gitignore
|
d5ae411fb422b2388cac220f9655900eecbc49ece961b2bb2a6610347733b756 ./.gitignore
|
||||||
98f663ab0f376b4550094465ec2e06192d1e0b0707604ec6794f20b0d10952c1 ./AI_CONTRIBUTIONS.md
|
98f663ab0f376b4550094465ec2e06192d1e0b0707604ec6794f20b0d10952c1 ./AI_CONTRIBUTIONS.md
|
||||||
0828545d3aa440e1ec5dce4b934de6800413f55a925988b60923c5ee9b700a4e ./CHANGELOG.md
|
1b8da729e4311d8839683177ae917927d6c9e28b5e82f5d9778031b737bb7d88 ./CHANGELOG.md
|
||||||
b696cab3cf482ff5737501371cca749369b119351383e698ced42bcdbcbfc8ae ./CLA.md
|
9f0d4fd40cb2e789a6007d29fe31e00391c687b6cfd4941f3b69cbb2c5e14b03 ./CLA.md
|
||||||
797e884105738fc931b585b695424f43ec5f296d8ab9bba5191b096e87a9e2c2 ./CONTRIBUTING.md
|
018d21c429ef5a94b2bc5b5535086bba0b2b3849d45c5cfff3e758622a76c722 ./CONTRIBUTING.md
|
||||||
86d7e49d5d90e0f98a4ad0f14b5d8b9f11ed09a1e29ecdf27388316b28e195e8 ./COPYRIGHT
|
86d7e49d5d90e0f98a4ad0f14b5d8b9f11ed09a1e29ecdf27388316b28e195e8 ./COPYRIGHT
|
||||||
f7ac75b443f4ca16b503241344b41aeff9503b0c30bedc2b119551d83cb0fa90 ./DCO.txt
|
f7ac75b443f4ca16b503241344b41aeff9503b0c30bedc2b119551d83cb0fa90 ./DCO.txt
|
||||||
73cb1b092a40c56e522c5a0ebddf2b44f347cdb57bf6994cdb305d0e5697b55e ./GENERATED_CODE.md
|
8ac46ef4e559f0bb43a8acb8cf6013052e09d41aa591a25c5e6fd462179eb067 ./GENERATED_CODE.md
|
||||||
a4570d054f072d33b8f17b0c8b162a6ee0ca37d7df2b1aee7e4b728ab350a892 ./GOVERNANCE.md
|
2c206b42bf864ff6b2e52fa83d536f209601601c272753f4c09d1e8f44b9c8f9 ./GOVERNANCE.md
|
||||||
0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0 ./LICENSE
|
0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0 ./LICENSE
|
||||||
47d857e49f89596bac9b09fc8ca57a668a33d01e2b51508acfc92ed321cdc27f ./LICENSES.md
|
4db1cfa504d3c80e06a3b3fde2854b3d01fbd2e5498d5f28d257297cd79b88c2 ./LICENSES.md
|
||||||
b6aa08e5ccaec3c5dccdc19d7cd7f54a70adae4d57966263c7aa353c7ba70e08 ./MAINTAINERS.md
|
b6aa08e5ccaec3c5dccdc19d7cd7f54a70adae4d57966263c7aa353c7ba70e08 ./MAINTAINERS.md
|
||||||
6638db2f1fba831c79de835ce95c847a5b36c5b5c693b99a28655b2d096cc440 ./OUTPUT_EXCEPTION.md
|
6638db2f1fba831c79de835ce95c847a5b36c5b5c693b99a28655b2d096cc440 ./OUTPUT_EXCEPTION.md
|
||||||
c3ac873ae2e6248e1d86dd542a11557b24b8dba80e4785f3bc1018152020235c ./README.md
|
007ffcafd32b50ed6ba4241ced90ef2e22a79cc7ad10f768e9e1f9560453b41f ./README.md
|
||||||
2751674c180f15a42c1d2b40cf149be4138aa6cf247d7be176f1f0c468103c24 ./RELEASE.md
|
fd2f45bb683d804afc127414e88e6b8ab75754af0596efe58fcb4ca3f4a55b9f ./RELEASE.md
|
||||||
209decb6769646eb2f58e312fbcd9c497c26234f3d3115bae3f20493b8178584 ./ROADMAP.md
|
953e226cb5179c17b1b2ce091653cbdaf83e92dd4913cc3b34a3cc893843496f ./ROADMAP.md
|
||||||
0fef473ac46b71215d1eb7922da4594210ffbbb8bb2dedd5e531fb3bd09396e1 ./SECURITY.md
|
44a895f8a738d21121379a5a417445ecc953f2ebfb64a7e575543769f642d1b6 ./SECURITY.md
|
||||||
53bd6eda804d6b782bdb07115ec197c890813cf2d5d0125dfe8f47f5f92f75b0 ./SPEC.md
|
d566b8b27777cb33209602df81d654a94072ad875440c089ac41764a349b6c17 ./SPEC.md
|
||||||
3d9e680cdfe147df7cc9ff29ecf1d3e566e9cd559ae84db4880e559b9c7c7205 ./TRADEMARKS.md
|
8906258cc7aaadf03bd7e84b69efc3be6a01d1c392b0711fbcee9abf4863b25b ./TRADEMARKS.md
|
||||||
|
e2ce8cbc43e4575984839b61332207f7c90e2848fa270d4c68dbc5fc394f28e7 ./cmd/himesan-release/main.go
|
||||||
|
9dda7402f76b38d417981f9e7658880b592c282c00163a3fdbfb0ab55e1edcba ./cmd/himesan/contract_test.go
|
||||||
|
d53548ffa05a386bd869cf6b70d1ebe3397eb49bf0c05d9dc48ed94ed85a052c ./cmd/himesan/doc.go
|
||||||
8cd8db68e1300f9b78cc7235855853cbc7aeb499a921419e23a22e4d22826fcb ./cmd/himesan/main.go
|
8cd8db68e1300f9b78cc7235855853cbc7aeb499a921419e23a22e4d22826fcb ./cmd/himesan/main.go
|
||||||
0709990e28d58d50e49219690e435338b269284efb6c2d2e2c95ed6b242580df ./cmd/himesan/main_test.go
|
8c53127437cd8a77676fdbcdf62f6dd9346e533665bf7d5ac32f103839a24974 ./cmd/himesan/main_test.go
|
||||||
|
de004164324a640555256a91ca21a02be163bd3d231b38f08f8539c9a77ef665 ./contracts/diagnostic-codes-v1.txt
|
||||||
|
60de7d2cb9e8626136a0d197cbf55d2eed545ebab184cca66d83d5593a45b8d7 ./contracts/generated-provenance-v1.txt
|
||||||
|
e6fb36b274bf44143497b2af454e9ee0fabe6122f935cbf97e785a003277e211 ./contracts/himesan-cli-help-v1.txt
|
||||||
|
6cad9bb55238c268b25937d02ac1708bb2302c8614556648fb6c63b4479d053b ./contracts/himesan-config-v1.schema.json
|
||||||
|
09e574ed55e3a48fa117936465d3de887686cf3f2a5c4231cf598ff30e4b1a75 ./contracts/himesan-operation-output-v1.schema.json
|
||||||
|
7679c9908266811a4336be9698982e58ffedecedf2c340b082bf5dcdef1554dd ./contracts/himesan-version-output-v1.schema.json
|
||||||
1ecbba46f8b1b2d548a01d7e98afae17b2dd17a814338ff1f88db885655d1c07 ./docs/ARCHITECTURE.md
|
1ecbba46f8b1b2d548a01d7e98afae17b2dd17a814338ff1f88db885655d1c07 ./docs/ARCHITECTURE.md
|
||||||
9c598559a89fa4a9bdd2311bd1ed8330992d0a0f74ec8b29ac151fc0ff8fef16 ./docs/BENCHMARKS.md
|
c26f1f2030f0661aaf3bab963efc132aa63e8fae61be05ff18b4c9c8bc35cb0d ./docs/BENCHMARKS.md
|
||||||
5c3a62fed80ca28d56558b8c75e8b5be8ba7d2554127adf4609d96da314e85b0 ./docs/BRAND.md
|
015e15c7b00edd0f5d3d4906745f0a6def526cb66422aa27eb8b5c6a935d9e3d ./docs/BENCHMARK_RESULTS.md
|
||||||
2b815d3b815b8d338560183c6f0af46f761c2465309783c93870b8ac8d022d03 ./docs/COMPATIBILITY.md
|
ae6043d4687708ce583e15e064c3baec2d9735b499a0f7c56a166d4bac5b18da ./docs/BRAND.md
|
||||||
5f4ac209a16ab110baeaa64a40c19d9239c903e17550c3f05e1e1473ddcc33a3 ./docs/DEVELOPMENT_SERVER.md
|
4e9085e89db89427849509ab36a59e0e06243e65c65cb948da8f577a9d043411 ./docs/COMPATIBILITY.md
|
||||||
51aa57a81131b64f76c45552122de842f22be92d81c8bba8f6fd38a18a7670d6 ./docs/DIAGNOSTICS.md
|
d1b4ef192e02b59a9d8d0eb3b1072392e633116433fa308f6ca9bbe17b75e213 ./docs/DEVELOPMENT_SERVER.md
|
||||||
|
4d92dc88ef33d4f7d952de039f842e54a3f3bfb4dbe56debf7ddb6e6c4a428f6 ./docs/DIAGNOSTICS.md
|
||||||
a62cc7174f3c92d8ef77e4bd9607fbf5d4b80bc514ff05bd433c02a9b0578f18 ./docs/LANGUAGE_SERVER.md
|
a62cc7174f3c92d8ef77e4bd9607fbf5d4b80bc514ff05bd433c02a9b0578f18 ./docs/LANGUAGE_SERVER.md
|
||||||
965a6ae57a8162c3af81f4987771e88617247f903088d210720ea2afab152cc9 ./docs/SECURITY_EVIDENCE.md
|
f66134e7a45a378e09fda4098d4fc706a280bb4ffdafe42a38a875f6ff82b3bb ./docs/LICENSING_INTENT.md
|
||||||
d969c7b5486ee93e54232fd69d9db06f3b4dc1bba63001596ec48545073c2680 ./docs/THREAT_MODEL.md
|
c80d0ee5d2b467c696f45bac26fedc5cce04cc69148d42a3d5a103f8760da515 ./docs/SECURITY_EVIDENCE.md
|
||||||
738258ba8f7e5ffea67d3f00eb70839590171971a9946a55b013ca95baf7aafb ./docs/V1_RELEASE_PLAN.md
|
e600caf17a87997de330ca787bfc5f16c1b20e07f467f8d1ad450e96e2421910 ./docs/THREAT_MODEL.md
|
||||||
|
96c5be9c958c6005c7d1fcea3843a71f501b10b5d12b545906ae1cfdd9a771db ./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
|
d891b9b075617050471b2ca34de73d926aaebde4ec638a5039b0d5001d3172f4 ./internal/compiler/analysis.go
|
||||||
@@ -38,10 +49,11 @@ c123bf8c8fd847962e147041172b71fef7e852819ccd7b27d4648748e02d522f ./internal/com
|
|||||||
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
|
||||||
|
1e0f83c3e9eea34a23e8d62f8da36bb697268531e8edd9d76a1f3752abfeb563 ./internal/compiler/contract_test.go
|
||||||
b2a96ef1ad572ad9cd0e9247328ca261de6f9f3689da41e3f3e111d405a6dee6 ./internal/compiler/diagnostic.go
|
b2a96ef1ad572ad9cd0e9247328ca261de6f9f3689da41e3f3e111d405a6dee6 ./internal/compiler/diagnostic.go
|
||||||
6a3dccd0c7caf92fe2690b350f1db6f34037609b1fff6d3e68e0e229241950e5 ./internal/compiler/discover.go
|
6a3dccd0c7caf92fe2690b350f1db6f34037609b1fff6d3e68e0e229241950e5 ./internal/compiler/discover.go
|
||||||
50a8bad11deea99faf0c7c9de1266bc086b7a1247ff9dd275ffedfc69a3be81d ./internal/compiler/e2e_test.go
|
e2843949f626c15dd6116af357588e2b471e6b03300f14f8ea76f31c1a91a2e8 ./internal/compiler/e2e_test.go
|
||||||
eefb05a35bd07660a293c8af97949cd6f69a22709728f3fe2cc9132b863b7d5a ./internal/compiler/fuzz_test.go
|
3cf1b6c26b2551ad2e8f1e06e45adfdf8b72e6f9ce7f9baf6b73209b7d788022 ./internal/compiler/fuzz_test.go
|
||||||
440580fcd844b2c80b1f4dddc4d83db82974e203cca298387a190e53e6a524f3 ./internal/compiler/model.go
|
440580fcd844b2c80b1f4dddc4d83db82974e203cca298387a190e53e6a524f3 ./internal/compiler/model.go
|
||||||
01516c0330b14a1970019690d8efcc9004d2dcfa59e82fef1b67f98005b67532 ./internal/compiler/operation.go
|
01516c0330b14a1970019690d8efcc9004d2dcfa59e82fef1b67f98005b67532 ./internal/compiler/operation.go
|
||||||
1f633caa019d8de9ae5bbad27e29679fcc01b3cb9a33d7d616be07140e06f38b ./internal/compiler/parse.go
|
1f633caa019d8de9ae5bbad27e29679fcc01b3cb9a33d7d616be07140e06f38b ./internal/compiler/parse.go
|
||||||
@@ -49,11 +61,13 @@ eefb05a35bd07660a293c8af97949cd6f69a22709728f3fe2cc9132b863b7d5a ./internal/com
|
|||||||
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
|
||||||
63fa75a3049a3a8a12d769d7f9b6b510dfe763baacf706775b75cef2c57a984f ./internal/compiler/testdata/golden/basic.sando.go
|
63fa75a3049a3a8a12d769d7f9b6b510dfe763baacf706775b75cef2c57a984f ./internal/compiler/testdata/golden/basic.sando.go
|
||||||
|
3b9755803bb66a08ce74a973d275234a593b6a944e7fe325de35bdbe52e62c6b ./internal/devserver/browser_evidence_test.go
|
||||||
eafbe9f7d8abb8fa792ec9e01f56655f9ec9d67279ffaac66d6035f9b2bfc404 ./internal/devserver/config.go
|
eafbe9f7d8abb8fa792ec9e01f56655f9ec9d67279ffaac66d6035f9b2bfc404 ./internal/devserver/config.go
|
||||||
99807040a870dd38ad1e04ae179243316778f94a41feb5d2c3076d463f52f9fe ./internal/devserver/config_test.go
|
99807040a870dd38ad1e04ae179243316778f94a41feb5d2c3076d463f52f9fe ./internal/devserver/config_test.go
|
||||||
|
53cdeb4131976a4191b43ae3537b7883c18b69be9468aaae6b2cfed86c13bed2 ./internal/devserver/contract_test.go
|
||||||
eddac51aecaac99bd11cfcf98f8a47cec5d51672efedad75d6f2a862c5d57fc1 ./internal/devserver/events.go
|
eddac51aecaac99bd11cfcf98f8a47cec5d51672efedad75d6f2a862c5d57fc1 ./internal/devserver/events.go
|
||||||
36c4a415ee8201064b2365647f24b3cc7497bf74fefbc30db8e3d4dadcc2997b ./internal/devserver/process.go
|
1449c00e9f25a0a2cd2ecda99ee5b11d6b30a48ff7ba0eb2f00572f917ac332a ./internal/devserver/process.go
|
||||||
72074ddb02d2eae3cabe5a79c648fe0debbae3ffaa1b0800b897259eca74f209 ./internal/devserver/process_test.go
|
c1a487deaca6ab22b4d45b9fa7cd1918834443f258945a9472967852ee55daff ./internal/devserver/process_test.go
|
||||||
5286e7ba5f59114e37265ab614d9dfc14bfb3003eccc93278e819e52d12f5e7a ./internal/devserver/process_unix.go
|
5286e7ba5f59114e37265ab614d9dfc14bfb3003eccc93278e819e52d12f5e7a ./internal/devserver/process_unix.go
|
||||||
c0f76ef5c14b0a28ed1e68d8d518102ffcbf067285b087eed4d13cd3c87b0e00 ./internal/devserver/process_windows.go
|
c0f76ef5c14b0a28ed1e68d8d518102ffcbf067285b087eed4d13cd3c87b0e00 ./internal/devserver/process_windows.go
|
||||||
6fbbe08813385ed43a9377b3772260e577dbd9f742f7a9ed5140a02f4c7991a4 ./internal/devserver/process_windows_32.go
|
6fbbe08813385ed43a9377b3772260e577dbd9f742f7a9ed5140a02f4c7991a4 ./internal/devserver/process_windows_32.go
|
||||||
@@ -61,7 +75,7 @@ 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
|
||||||
5c0ce1eedb08c0381dd048526e9f781a2ac8501cc1142648c43d4aa07ca62299 ./internal/devserver/supervisor_test.go
|
40872123cec152daad73eaaabdba465033a4de57fade62790c29a8655444e67c ./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
|
76609ee5c57b1c174076689d4953e6ec5ef914ca114c27f1ca1d6e7fcabbddcf ./internal/lsp/features.go
|
||||||
@@ -69,22 +83,46 @@ edd377bca36a04ae76620a146f986e25c954fd137bab0b0e49084dbcde216ce7 ./internal/lsp
|
|||||||
73b2ff8d6fd8fe44abec287384f0051520fe9564a50b4764538474f336624eee ./internal/lsp/protocol_test.go
|
73b2ff8d6fd8fe44abec287384f0051520fe9564a50b4764538474f336624eee ./internal/lsp/protocol_test.go
|
||||||
2a2151a0211a5048c3bc89f9a2901a3c208609f483e99144f3837f87941efff9 ./internal/lsp/security_test.go
|
2a2151a0211a5048c3bc89f9a2901a3c208609f483e99144f3837f87941efff9 ./internal/lsp/security_test.go
|
||||||
3be838b4bb0b618f46c0eab40788a1e03121da25644dedfa7542bd0fa64571e1 ./internal/lsp/server.go
|
3be838b4bb0b618f46c0eab40788a1e03121da25644dedfa7542bd0fa64571e1 ./internal/lsp/server.go
|
||||||
c919b132373d9ab715cdaf057789a3d23f27b1c9f0eded14c06d04d2a4c86ee3 ./internal/lsp/server_test.go
|
99f7ccc8768bd92efe6aec9039d4a1f8ba5a2d072da50c65ab3ed887e8ad9cf5 ./internal/lsp/server_test.go
|
||||||
4c5ade5f649dffdd0a1b0a350c15488fc601b7eba582386f39e64412387b5c91 ./internal/lsp/workspace.go
|
4c5ade5f649dffdd0a1b0a350c15488fc601b7eba582386f39e64412387b5c91 ./internal/lsp/workspace.go
|
||||||
|
21ec36dd353b237cd6e6705d09b2ad8a1b7dade8ad18024976e4c6fc140294d2 ./internal/releaseartifact/artifact.go
|
||||||
|
0c482e0c0ff2659ee6c650c56a7a267239cf8149283dfe5b3e680a5e3ab47121 ./internal/releaseartifact/artifact_test.go
|
||||||
|
83dd9a9d683fc443e69449bc0ac5cdc5c764f4404718c41b3a05676f7abef6d0 ./internal/releaseartifact/evidence.go
|
||||||
|
40faefc3a8ab0a6e1e28972fc515e118989da068186307133ae83c676604d741 ./internal/releaseartifact/evidence_test.go
|
||||||
|
77e7dc75b48765bb4ce926c634b0eaaded08b4d59d0951ee814001d23aebcf4c ./internal/releaseartifact/macos.go
|
||||||
|
8e31a8c4654e8a1bc8a025b350494f509b5979b61e500cc937dcbc916ee87d39 ./internal/releaseartifact/macos_test.go
|
||||||
|
f2e1873c4beb131266d6b4b8ee27acc12b7cb1baca734cc0d9586fb14484b7b0 ./internal/releaseartifact/native.go
|
||||||
|
c07cd08a7875f851981a11a4d570a7784cf453faafe0c991aa43881cd4f2e39a ./internal/releaseartifact/native_test.go
|
||||||
|
fd1babf093a791d7e50a1faa3d4b00e42a1e487ad64eece7841a3f4d81cb4072 ./internal/testpath/tempdir.go
|
||||||
|
5a8ffb091328464b84403ea54a8911e41173881a4909a334ba2e4c44dd1c0de4 ./internal/testpath/tempdir_test.go
|
||||||
d8c6f37c94ef426fc2d95c82331265f7d700d2e2a23100ad78c92849280ff6d8 ./internal/version/version.go
|
d8c6f37c94ef426fc2d95c82331265f7d700d2e2a23100ad78c92849280ff6d8 ./internal/version/version.go
|
||||||
a1ee0ec44dc45f01a3357cf93e13a81587af7f061497c25df374e2a88231f3b7 ./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
|
e66858232f401e112bd5f072943bc286e9a8564ad77558cdf115c023940ef318 ./sando/README.md
|
||||||
|
acfbbc860417aeabba486ca4fea530e711c3e488bc3f5f6588db102b8a58bec2 ./sando/api_contract_test.go
|
||||||
|
7e862661f19fc9fcd694ef64e65fda898186c51836335c27c21397e8dcf09c05 ./sando/benchmark_test.go
|
||||||
7ec3fe73755a385e0950b9fbf833dd4b6a753a769ab743e97b9d94e77370a32c ./sando/component.go
|
7ec3fe73755a385e0950b9fbf833dd4b6a753a769ab743e97b9d94e77370a32c ./sando/component.go
|
||||||
a242fd3bebb9cb8786c92651950999c6a2575d0be9602bac562e0b63c9ded015 ./sando/component_test.go
|
a242fd3bebb9cb8786c92651950999c6a2575d0be9602bac562e0b63c9ded015 ./sando/component_test.go
|
||||||
|
e12e2b27396ba4447d27cb1f80ef44100f7295135c26a1bdad5e13e1312a3a5f ./sando/differential_test.go
|
||||||
ff76daee5b642ad84af31701833246d68b54d09580192312d750a7a2e893a692 ./sando/go.mod
|
ff76daee5b642ad84af31701833246d68b54d09580192312d750a7a2e893a692 ./sando/go.mod
|
||||||
|
ed2b8d8040689c608623d30085a55678ae3c9d70a8a40a42435e04bff47a31ab ./sando/testdata/public-api-v1.txt
|
||||||
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
|
||||||
c4a161faba46ce5b508c0788078256a520277a573a3ace0e85ae0c26b16d298b ./scripts/README.md
|
6ea995e9d7d22e09a13065a6e340df4efa0f1860b0b42409f84835c93f29962c ./scripts/PUBLIC_SNAPSHOT.md
|
||||||
0bc796f71c863aa898674a26c56f055e3d81cf20629ca7b32fbae87d8841e0a8 ./scripts/check-licenses.sh
|
0482438a902e1f5368af3707c1348007dc0c2efeb854be85bf4f410e6d2f4a1f ./scripts/README.md
|
||||||
1b003062799b99bfe271b47438397a8cce5875c60c982a0117eb11c3babcadf0 ./scripts/release-check.sh
|
81ab4c10a436bc9b345828a55c2fe446f297fa6cfd1debfe95e1082b6e28c969 ./scripts/check-licenses.sh
|
||||||
78a64c7fb3a039b15a1d08b4c0b873952852287a07f670247b081e59dbb09a30 ./scripts/verify-public-install.sh
|
3adba1428df83d6180d4fa3c683f0604dc7cdc0c4358f3dae7595189cc40f949 ./scripts/export-public-snapshot.sh
|
||||||
24ed3c9a1d37e46a856cbbd68e5c58ae04c6c9852902b99ed675e1f428339a9f ./scripts/verify.ps1
|
f391ac912cdeb6b139d4feaec29a992cb53faea0fc4bbe805fd380450b238f7c ./scripts/native-gate.sh
|
||||||
f0cbd86759fa729064cb1c69991db2ac291792dadb6b1e1ba83794f2e390404d ./scripts/verify.sh
|
a038a002f0c895fef38a7265bcb7df01efdda57de5c4d36d0dc7c8e377798a95 ./scripts/package-macos.sh
|
||||||
|
7d1e3606f3159e0d5d222bb5b02171ad90bb2c42b0e041042dcf8e6b7692c40a ./scripts/package-native.sh
|
||||||
|
ed17f6225971af8ca9ba88dc53f307f46dcc6cb5d9f3b08d42c466ac4d0e4214 ./scripts/public-snapshot-lib.sh
|
||||||
|
4f067a37de73082f3534ebfeb5a73b53b37ace9e4c6b21956405e2fefacbeb38 ./scripts/public-snapshot.allow
|
||||||
|
8fa4ee6c6ae80c261a8f5c1ff18a89185b4ce6472ccb577e1daf61afaa146e14 ./scripts/release-check.sh
|
||||||
|
494cf5573cc6a891d4243ec01a98c43263de75090cb3b55c9c398e545b1ec1a8 ./scripts/sign-notarize-macos.sh
|
||||||
|
485f2eb4bba187e22dae820e75b4fa4ee807a1e526f21de08108c3e987390bcb ./scripts/test-public-snapshot.sh
|
||||||
|
39a80708b1c1a64c837032c6570e77dea28b2c6e6058eea40efac63ad5827431 ./scripts/verify-consumer.sh
|
||||||
|
ddcb5602a51a515ee6649e26f27d919addb0590abdcc095c374c90f5b0c2f8a3 ./scripts/verify-public-install.sh
|
||||||
|
1673257efa50b906363ccab45f4ebcf7cb377185861a1c37221c98292c2c7cf6 ./scripts/verify-real-browser.sh
|
||||||
|
d67b73b716fa795bf166df12ad91eea74225b6c76e659d0e8b088dad221865aa ./scripts/verify.sh
|
||||||
|
|||||||
@@ -42,30 +42,38 @@ request object, or production server.
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
`v1.0.0-beta.2` is the current compiler beta. It is supported for
|
This is the source and documentation for compiler `v1.0.0` and independently
|
||||||
learning, classroom projects, evaluation, and compatibility feedback. It is a
|
tagged runtime `sando/v1.0.0`. The v1 source syntax, generated API, runtime
|
||||||
semantic-version prerelease: source syntax, generated output, the runtime API,
|
API, CLI, diagnostics, and schemas follow the
|
||||||
and CLI behavior may change before final v1, and this beta is not recommended
|
[v1 compatibility policy](docs/COMPATIBILITY.md). Corrections receive new
|
||||||
for production deployment.
|
immutable versions rather than moving a published tag.
|
||||||
|
|
||||||
The exact Beta 1 source passed maintainer-run native Windows and executed Linux
|
The commands below target that final pair. Canonical Gitea tags and release
|
||||||
matrices with Go 1.25.12 and Go 1.26.5. Native macOS validation is still
|
announcements establish version and artifact availability; check them before
|
||||||
pending, so macOS support is provisional in this beta. Mac learners and Go
|
installing a newly announced version.
|
||||||
developers are warmly invited to try it and share their macOS version,
|
|
||||||
architecture, Go version, command, and smallest useful reproduction. Community
|
Linux/amd64 and Apple Silicon macOS/arm64 are the maintained v1 execution and
|
||||||
reports broaden the evidence; maintainers remain responsible for security
|
release targets. Native release evidence runs with pinned Go 1.26.7 and Go
|
||||||
review, triage, fixes, and release decisions.
|
1.27.0 toolchains on both platforms; the module language directive remains Go
|
||||||
|
1.25 for consumer compatibility. WSL, native Windows, Intel macOS, and other
|
||||||
|
targets may be useful development or portability environments but are not v1
|
||||||
|
compatibility promises.
|
||||||
|
|
||||||
|
The evidence ledger retains the exact Beta 1 Windows and Linux observations as
|
||||||
|
historical facts. Those past results do not expand the current support policy.
|
||||||
|
Maintainers remain responsible for security review, triage, fixes, and release
|
||||||
|
decisions on both supported native targets.
|
||||||
|
|
||||||
Inside an application module, add the small runtime first:
|
Inside an application module, add the small runtime first:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go get gamertan.com/sandwich-hime/sando@v1.0.0-beta.1
|
go get gamertan.com/sandwich-hime/sando@v1.0.0
|
||||||
```
|
```
|
||||||
|
|
||||||
Then install the current beta compiler:
|
Then install the matching compiler:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go install gamertan.com/sandwich-hime/cmd/himesan@v1.0.0-beta.2
|
go install gamertan.com/sandwich-hime/cmd/himesan@v1.0.0
|
||||||
```
|
```
|
||||||
|
|
||||||
Keep that runtime-first order. It avoids path-selection ambiguity between the
|
Keep that runtime-first order. It avoids path-selection ambiguity between the
|
||||||
@@ -76,22 +84,23 @@ does not contain `sando`, seed the exact nested module without clearing the
|
|||||||
global cache, then retry:
|
global cache, then retry:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go mod download gamertan.com/sandwich-hime/sando@v1.0.0-beta.1
|
go mod download gamertan.com/sandwich-hime/sando@v1.0.0
|
||||||
go get gamertan.com/sandwich-hime/sando@v1.0.0-beta.1
|
go get gamertan.com/sandwich-hime/sando@v1.0.0
|
||||||
```
|
```
|
||||||
|
|
||||||
For a reproducible one-off or classroom invocation that does not depend on the
|
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.2 --help
|
go run gamertan.com/sandwich-hime/cmd/himesan@v1.0.0 --help
|
||||||
```
|
```
|
||||||
|
|
||||||
The unchanged runtime remains `sando/v1.0.0-beta.1`; compiler versions are
|
The runtime implementation retains ABI `sando.v1` and zero third-party module
|
||||||
independent. Beta 2 adds `himesan lsp --stdio` without changing the runtime ABI.
|
requirements. The coordinated v1 tags make the release pair explicit even
|
||||||
Signed compiler tags, direct fetching, the public Go proxy, and the checksum
|
though compiler and runtime versions remain independently addressable. Signed
|
||||||
database are verified after publication. A newly announced version may still
|
tags, direct fetching, the public Go proxy, and the checksum database are
|
||||||
need a short propagation interval before every proxy sees its immutable tag.
|
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:
|
||||||
|
|
||||||
@@ -109,7 +118,7 @@ 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
|
Hime-san also provides a standard, editor-neutral language server. It analyzes
|
||||||
unsaved overlays with the compiler's real parser and context model, but never
|
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,
|
generates, runs Go, executes a project, fetches a module, accesses the network,
|
||||||
or starts the dev supervisor. See
|
or starts the dev supervisor. See
|
||||||
@@ -117,9 +126,8 @@ or starts the dev supervisor. See
|
|||||||
Skill and VS Code preview live in the separate
|
Skill and VS Code preview live in the separate
|
||||||
[tooling repository](https://gitea.speelman.ca/gamertan/sandwich-hime-tooling).
|
[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
|
A version is advertised as available only after its immutable tags,
|
||||||
advertised as available only after its immutable tags, `gamertan.com`
|
`gamertan.com` metadata, and clean direct-fetch installation have been verified.
|
||||||
metadata, and clean direct-fetch installation have been verified.
|
|
||||||
|
|
||||||
## The contract
|
## The contract
|
||||||
|
|
||||||
|
|||||||
+70
-19
@@ -21,25 +21,26 @@ while it is the current prerelease, but it is not recommended or supported as a
|
|||||||
production-stable dependency. Syntax, generated output, runtime APIs, CLI
|
production-stable dependency. Syntax, generated output, runtime APIs, CLI
|
||||||
behavior, and diagnostics may change in a later prerelease.
|
behavior, and diagnostics may change in a later prerelease.
|
||||||
|
|
||||||
Beta 1 may publish with native macOS validation pending when Windows and Linux
|
Beta 1 and Beta 2 retain their historical platform evidence. The v1 RC line
|
||||||
have passed the exact-candidate matrix and macOS is clearly marked provisional.
|
raises the maintained release gate to native Linux/amd64 and Apple Silicon
|
||||||
Community Mac results are valuable compatibility input; they do not transfer
|
macOS/arm64. WSL, native Windows, Intel macOS, and other targets remain useful
|
||||||
security review, triage, remediation, or release responsibility away from the
|
portability input but are not v1 release blockers or maintained promises.
|
||||||
maintainers.
|
|
||||||
|
|
||||||
Beta tags are signed, annotated, and immutable. Beta 1 is a source/module
|
Beta tags are signed, annotated, and immutable. Beta 1 is a source/module
|
||||||
release installed through the Go toolchain; it does not promise downloadable
|
release installed through the Go toolchain; it does not promise downloadable
|
||||||
prebuilt binaries. Signed binary artifacts, checksums, SBOMs, reproducible
|
prebuilt binaries. Signed binary artifacts, checksums, SBOMs, reproducible
|
||||||
archives, and key-recovery rehearsal are mandatory for release candidates and
|
archives are mandatory for release candidates and final v1. Offline signing-key
|
||||||
final v1.
|
restoration and independent second-person recovery/verification were explicitly
|
||||||
|
deferred by the maintainer for v1.0.0. Record that assurance gap in the release
|
||||||
|
review; retain the drills as follow-up work rather than claiming they passed.
|
||||||
|
|
||||||
### Release candidate
|
### Release candidate
|
||||||
|
|
||||||
An RC means the intended v1 source, runtime, CLI, diagnostics, schemas, and
|
An RC means the intended v1 source, runtime, CLI, diagnostics, schemas, and
|
||||||
generated contract are frozen except for release-blocking fixes. An RC requires
|
generated contract are frozen except for release-blocking fixes. An RC requires
|
||||||
maintainer-run native Linux, macOS, and Windows evidence, complete release
|
maintainer-run Linux/amd64 and native macOS/arm64 evidence, complete release
|
||||||
artifacts and provenance, signed tags, clean direct/proxy installs, and every RC
|
artifacts and provenance, signed tags, clean direct/proxy installs, and every
|
||||||
gate in this repository. Findings produce a new RC rather than a moved tag.
|
RC gate in this repository. Findings produce a new RC rather than a moved tag.
|
||||||
|
|
||||||
### Final v1
|
### Final v1
|
||||||
|
|
||||||
@@ -49,7 +50,17 @@ published assurance gap, and the documented RC observation period. A
|
|||||||
deployment, example, classroom project, or case study in another repository is
|
deployment, example, classroom project, or case study in another repository is
|
||||||
neither imported nor required as release evidence.
|
neither imported nor required as release evidence.
|
||||||
|
|
||||||
## Beta 1 publication gates
|
For v1.0.0 the maintainer explicitly accepted the missing timed dogfooding-note
|
||||||
|
gap and deferred independent Mac installation to available alternate hardware.
|
||||||
|
Keep those dispositions visible in `ROADMAP.md`; do not recreate historical
|
||||||
|
reviews, call the install tested, or require another local profile as a
|
||||||
|
substitute. A reproduced product defect remains distinct from missing notes.
|
||||||
|
|
||||||
|
## Beta 1 publication gates (historical)
|
||||||
|
|
||||||
|
The first beta used a broader one-time platform campaign. The completed items
|
||||||
|
below are retained as publication history; they do not define future platform
|
||||||
|
support.
|
||||||
|
|
||||||
Before `sando/v1.0.0-beta.1` and `v1.0.0-beta.1` are created:
|
Before `sando/v1.0.0-beta.1` and `v1.0.0-beta.1` are created:
|
||||||
|
|
||||||
@@ -99,8 +110,7 @@ In addition to every Beta 1 compiler/security/determinism gate:
|
|||||||
nested-module boundaries, completion scope, and component definitions;
|
nested-module boundaries, completion scope, and component definitions;
|
||||||
2. prove the language-server package does not write, execute project code,
|
2. prove the language-server package does not write, execute project code,
|
||||||
invoke Go, fetch, access the network, or start the development supervisor;
|
invoke Go, fetch, access the network, or start the development supervisor;
|
||||||
3. run the exact candidate on supported Go lines under executed Linux and
|
3. run the exact candidate on supported Go lines under executed Linux/amd64;
|
||||||
native Windows, with native macOS status stated explicitly;
|
|
||||||
4. build an exact version-stamped candidate and assert the additive
|
4. build an exact version-stamped candidate and assert the additive
|
||||||
`features: ["lsp-stdio"]` JSON identity;
|
`features: ["lsp-stdio"]` JSON identity;
|
||||||
5. publish a signed annotated compiler tag only after the reviewed sanitized
|
5. publish a signed annotated compiler tag only after the reviewed sanitized
|
||||||
@@ -111,21 +121,62 @@ In addition to every Beta 1 compiler/security/determinism gate:
|
|||||||
## RC and final gates
|
## RC and final gates
|
||||||
|
|
||||||
No release candidate or v1.0.0 release occurs until every applicable gate in
|
No release candidate or v1.0.0 release occurs until every applicable gate in
|
||||||
this repository is evidenced, including cross-platform deterministic
|
this repository is evidenced, including deterministic generation on supported
|
||||||
generation, temporary-module compilation, fuzz/adversarial suites,
|
Linux and Go lanes, temporary-module compilation, fuzz/adversarial suites,
|
||||||
race/vet/vulnerability/license checks on the latest two supported Go lines,
|
race/vet/vulnerability/license checks on the latest two supported Go lines,
|
||||||
development-supervisor failure tests, and reproducible repository-owned
|
development-supervisor failure tests, and reproducible repository-owned
|
||||||
benchmark and security results.
|
benchmark and security results.
|
||||||
|
|
||||||
Release candidates require a clean canonical checkout, reviewed changelog,
|
Release candidates require a clean canonical checkout, reviewed changelog,
|
||||||
compatible vanity-import metadata, reproducible binaries, signed annotated
|
compatible vanity-import metadata, reproducible Linux/amd64 and Darwin/arm64
|
||||||
tags, checksums, SBOMs, vulnerability results, and verification on Linux,
|
binaries, signed annotated tags, checksums, SBOMs, vulnerability results, and
|
||||||
macOS, and Windows.
|
verification on both maintained native targets. Darwin artifacts additionally
|
||||||
|
require manual Developer ID signing, notarization, stapling, and Gatekeeper
|
||||||
|
validation outside runner authority.
|
||||||
|
|
||||||
|
Human-reviewed RC evidence stays outside the source tree. After completing the
|
||||||
|
fixed document set reported by `himesan-release verify-evidence`, the release
|
||||||
|
operator seals its exact bytes and source identity once:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go run ./cmd/himesan-release evidence-manifest \
|
||||||
|
--directory "$HIMESAN_RELEASE_EVIDENCE_DIR" \
|
||||||
|
--repository gamertan/sandwich-hime \
|
||||||
|
--version v1.0.0-rc.1 \
|
||||||
|
--commit "$(git rev-parse HEAD)" \
|
||||||
|
--tree "$(git rev-parse 'HEAD^{tree}')" \
|
||||||
|
--reviewed-by "REVIEWER" \
|
||||||
|
--reviewed-at "YYYY-MM-DDTHH:MM:SSZ"
|
||||||
|
```
|
||||||
|
|
||||||
|
The manifest is created without overwrite. Changing any document or candidate
|
||||||
|
identity requires a fresh review directory and manifest; deleting a manifest
|
||||||
|
is not an approval shortcut. `release-check.sh --public` verifies the sealed
|
||||||
|
digests and identities but never substitutes for the human review itself.
|
||||||
|
|
||||||
|
Native receipts use an equally strict, extraction-scratch-free layout. Download
|
||||||
|
runner ZIPs outside this directory, then copy only each checksummed receipt pair
|
||||||
|
into the exact four lanes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$HIMESAN_NATIVE_EVIDENCE_DIR/
|
||||||
|
├── darwin-arm64-go1.26.7/TEND-CI-VERIFICATION.json{,.sha256}
|
||||||
|
├── darwin-arm64-go1.27.0/TEND-CI-VERIFICATION.json{,.sha256}
|
||||||
|
├── linux-amd64-go1.26.7/TEND-CI-VERIFICATION.json{,.sha256}
|
||||||
|
└── linux-amd64-go1.27.0/TEND-CI-VERIFICATION.json{,.sha256}
|
||||||
|
```
|
||||||
|
|
||||||
|
ZIPs, additional files, renamed lanes, development-repository identities, and
|
||||||
|
receipts for a public commit other than current canonical `main` are rejected.
|
||||||
|
The strict layout prevents extraction debris or a nearby historical run from
|
||||||
|
being mistaken for the reviewed native receipt set.
|
||||||
|
|
||||||
## Public source and artifacts
|
## Public source and artifacts
|
||||||
|
|
||||||
Gitea is the only canonical public forge. Public source is exported into a
|
Gitea is the only canonical public forge. Public source is exported into a
|
||||||
separate, sanitized Gitea repository with fresh reviewed history. A sanitized
|
separate, sanitized Gitea repository with its own reviewed history. Append
|
||||||
|
reviewed publication commits to the existing public main; do not create a new
|
||||||
|
orphan history per version or push private development branches into it. A sanitized
|
||||||
GitHub discovery snapshot may copy reviewed public source, but it is not an
|
GitHub discovery snapshot may copy reviewed public source, but it is not an
|
||||||
issue, contribution, release, or module origin and must never receive private
|
issue, contribution, release, or module origin and must never receive private
|
||||||
development refs or an indiscriminate Git mirror. Release binaries and
|
development refs or an indiscriminate Git mirror. Release binaries and
|
||||||
|
|||||||
+69
-32
@@ -7,73 +7,110 @@ necessarily blockers for an earlier prerelease. The ordered initiative,
|
|||||||
repository topology, release-candidate sequence, and definition of confidence
|
repository topology, release-candidate sequence, and definition of confidence
|
||||||
are maintained in [docs/V1_RELEASE_PLAN.md](docs/V1_RELEASE_PLAN.md).
|
are maintained in [docs/V1_RELEASE_PLAN.md](docs/V1_RELEASE_PLAN.md).
|
||||||
|
|
||||||
## Beta 1: public learning and evaluation
|
## Beta 1: public learning and evaluation (historical)
|
||||||
|
|
||||||
Beta 1 deliberately ships before the final-v1 compatibility and artifact gates.
|
Beta 1 deliberately ships before the final-v1 compatibility and artifact gates.
|
||||||
Its scope is classroom use, learning, prototypes, and compatibility feedback;
|
Its scope is classroom use, learning, prototypes, and compatibility feedback;
|
||||||
it is not a production-stability promise.
|
it is not a production-stability promise.
|
||||||
|
|
||||||
- [x] Define beta versus RC/final support and compatibility policy.
|
- [x] Define beta versus RC/final support and compatibility policy.
|
||||||
- [x] Establish a public pre-beta Linux/Windows matrix on Go 1.25 and Go 1.26.
|
- [x] Establish a one-time public pre-beta Linux/Windows evidence matrix on Go
|
||||||
- [x] Document macOS as provisional and invite useful community reports while
|
1.25 and Go 1.26.
|
||||||
retaining maintainer responsibility for security and releases.
|
- [x] Record the untested macOS boundary without presenting it as evidence.
|
||||||
- [x] Rerun all required Windows/Linux checks and deterministic generation on
|
- [x] Rerun the historical Windows/Linux campaign and deterministic generation
|
||||||
the exact Beta 1 candidate.
|
on the exact Beta 1 candidate.
|
||||||
- [x] 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.
|
||||||
- [x] Verify clean runtime-first direct and public-proxy installs after
|
- [x] Verify clean runtime-first direct and public-proxy installs after
|
||||||
publication.
|
publication.
|
||||||
- [ ] Complete native macOS maintainer validation. This is an RC/final gate,
|
|
||||||
not a Beta 1 gate.
|
|
||||||
|
|
||||||
## Compiler and runtime for RC/final
|
## Compiler and runtime for RC/final
|
||||||
|
|
||||||
- [ ] Freeze and machine-check the compiler, CLI, diagnostic, schema, generated,
|
- [x] Freeze and machine-check the compiler, CLI, diagnostic, schema, generated,
|
||||||
and runtime compatibility contracts.
|
and runtime compatibility contracts.
|
||||||
- [ ] Repeat compiler-owned deterministic golden output across Linux, macOS,
|
- [x] Repeat compiler-owned deterministic golden output across the supported
|
||||||
and Windows on the exact candidate.
|
Linux and macOS Go lanes on the exact candidate.
|
||||||
- [ ] Compile temporary consumer modules using committed Go and only the Apache
|
- [x] Compile temporary consumer modules using committed Go and only the Apache
|
||||||
runtime.
|
runtime.
|
||||||
- [ ] Run the parser, delimiter, context, path, and source-map release fuzz
|
- [x] Run the parser, delimiter, context, path, and source-map release fuzz
|
||||||
campaign.
|
campaign.
|
||||||
- [ ] Evidence adversarial escaping and filesystem cases.
|
- [x] Evidence adversarial escaping and filesystem cases.
|
||||||
- [ ] Pass test, race, vet, vulnerability, and license gates on the latest two
|
- [x] Pass test, race, vet, vulnerability, and license gates on the latest two
|
||||||
supported Go lines.
|
supported Go lines.
|
||||||
- [ ] Reproduce signed compiler/runtime release artifacts, checksums, and SBOMs.
|
- [x] Reproduce compiler/runtime release artifacts, checksums, and SBOMs; sign
|
||||||
|
and notarize the macOS distribution outside runner authority.
|
||||||
|
|
||||||
## Development supervisor for RC/final
|
## Development supervisor for RC/final
|
||||||
|
|
||||||
- [ ] Generation/build/start/health failures keep the previous healthy server
|
- [x] Generation/build/start/health failures keep the previous healthy server
|
||||||
live.
|
live.
|
||||||
- [ ] SSE reconnect/reload and mapped overlay diagnostics pass browser-level
|
- [x] SSE reconnect/reload and mapped overlay diagnostics pass browser-level
|
||||||
tests.
|
tests.
|
||||||
- [ ] CSP hash injection, fragment/API/download exclusion, and cache disabling
|
- [x] CSP hash injection, fragment/API/download exclusion, and cache disabling
|
||||||
pass.
|
pass.
|
||||||
- [ ] Replaced and interrupted child processes leave no descendants on
|
- [x] Replaced and interrupted child processes leave no descendants on
|
||||||
supported systems.
|
supported systems.
|
||||||
|
|
||||||
## Repository-owned release evidence
|
## Repository-owned release evidence
|
||||||
|
|
||||||
- [ ] Differentially test contextual escaping against Go's documented
|
- [x] Differentially test contextual escaping against Go's documented
|
||||||
`html/template` safety baseline.
|
`html/template` safety baseline.
|
||||||
- [ ] Reproduce repository-owned synthetic benchmark cases and methodology from
|
- [x] Reproduce repository-owned synthetic benchmark cases and methodology from
|
||||||
a clean checkout.
|
a clean checkout.
|
||||||
- [ ] Review generated output for stable provenance, source mappings, and
|
- [x] Review generated output for stable provenance, source mappings, and
|
||||||
absence of compiler-license headers.
|
absence of compiler-license headers.
|
||||||
- [ ] Document the production boundary: committed generated Go plus the Apache
|
- [x] Document the production boundary: committed generated Go plus the Apache
|
||||||
runtime, with no compiler or development supervisor in the deployed binary.
|
runtime, with no compiler or development supervisor in the deployed binary.
|
||||||
- [ ] Keep unsupported or unmeasured performance and production claims out of
|
- [x] Keep unsupported or unmeasured performance and production claims out of
|
||||||
release materials.
|
release materials.
|
||||||
|
|
||||||
## Final public launch
|
## Final public launch
|
||||||
|
|
||||||
- [ ] Complete final human review of ownership notices, output permission, DCO
|
Final-release preparation is active. Preserve the existing reviewed public
|
||||||
contribution process, and pre-registration trademark terms.
|
history and append allowlisted updates; keep operator and archival records
|
||||||
- [ ] Complete name clearance, security-mailbox recovery, release signing, and
|
outside the public snapshot. Completed native and signing evidence remains
|
||||||
two-person credential recovery.
|
bound to its exact candidate, not automatically to a later documentation or
|
||||||
- [ ] Verify `gamertan.com` vanity metadata and documented installs from clean
|
export-tool update. Final tags and artifacts have not been published.
|
||||||
machines.
|
|
||||||
|
Use [RELEASE.md](RELEASE.md) for the remaining final-publication requirements.
|
||||||
|
Passing CI is evidence for that review, not an automatic publication decision.
|
||||||
|
|
||||||
|
- [x] Finalize the maintainer-approved individual [contribution agreement](CLA.md)
|
||||||
|
version 1.0 and explicit prospective acceptance process. Preserve the AGPL
|
||||||
|
compiler, Apache runtime, chosen application license and existing DCO/output
|
||||||
|
grants. Publication is not contributor acceptance or retroactive assent.
|
||||||
|
The maintainer chose to proceed without an outside legal-review prerequisite;
|
||||||
|
see [the rationale](docs/LICENSING_INTENT.md).
|
||||||
|
- [x] Record the maintainer's approval to proceed with ownership notices, output
|
||||||
|
permission, reciprocal contribution terms and pre-registration naming scope.
|
||||||
|
This is not a statement of external legal review or guaranteed enforceability.
|
||||||
|
- [x] Record the maintainer's decision to proceed with Sandwich Hime as the
|
||||||
|
primary project identity and Hime-san as its tool name. This is acceptance of
|
||||||
|
the documented name-review limitations, not formal trademark clearance,
|
||||||
|
completed similarity analysis or a registration requirement.
|
||||||
|
- [ ] Complete final security/signing readiness review. Distinguish successful
|
||||||
|
signing from the explicitly deferred recovery drills below.
|
||||||
|
- [x] Verify `gamertan.com` vanity metadata and documented RC installs using clean
|
||||||
|
direct-fetch and public-proxy caches. Final-version installs remain post-tag
|
||||||
|
checks; an independent Mac installation is an accepted follow-up below.
|
||||||
- [ ] Confirm the sanitized public Gitea source contains no private paths,
|
- [ ] Confirm the sanitized public Gitea source contains no private paths,
|
||||||
identifiers, history, or unsupported claims.
|
identifiers, history, or unsupported claims.
|
||||||
- [ ] Publish and observe a signed RC on every supported native platform.
|
- [x] Publish signed RC.1 artifacts on Linux/amd64 and Darwin/arm64.
|
||||||
|
- [x] Record the maintainer's v1 acceptance of existing live dogfooding despite
|
||||||
|
missing timed checkpoint notes. This is an explicit assurance-gap acceptance,
|
||||||
|
not reconstructed reviews or a claim of measured error-free operation. Missing
|
||||||
|
notes alone do not restart the observation period or block release.
|
||||||
- [ ] Publish `sando/v1.0.0`, then `v1.0.0`, without moving either tag.
|
- [ ] Publish `sando/v1.0.0`, then `v1.0.0`, without moving either tag.
|
||||||
|
|
||||||
|
## Accepted assurance follow-ups
|
||||||
|
|
||||||
|
- [ ] Exercise the published Mac download/install and CLI on independent Apple
|
||||||
|
Silicon hardware when available. The maintainer accepted this gap for v1 and
|
||||||
|
prefers an independent machine over another profile on the development Mac.
|
||||||
|
Record the result in documentation; fix any reproduced defect in an appropriate
|
||||||
|
patch release. This is not a current launch blocker or completed install test.
|
||||||
|
- [ ] Exercise offline signing-key restoration and independent second-person
|
||||||
|
credential recovery/verification. The maintainer explicitly deferred these
|
||||||
|
for v1.0.0; they are not launch blockers or completed recovery evidence. Loss
|
||||||
|
of the current credentials or operator access remains an incident-response
|
||||||
|
risk until these drills and independent access are proven.
|
||||||
|
|||||||
+46
-19
@@ -2,14 +2,16 @@
|
|||||||
|
|
||||||
# Security policy
|
# Security policy
|
||||||
|
|
||||||
Sandwich Hime compiler `v1.0.0-beta.2` and runtime
|
This policy covers Sandwich Hime compiler `v1.0.0` and runtime `sando/v1.0.0`
|
||||||
`sando/v1.0.0-beta.1` are supported within a deliberately narrow scope:
|
under the [v1 compatibility policy](docs/COMPATIBILITY.md). The latest published
|
||||||
learning, classroom projects, evaluation, and compatibility testing.
|
stable v1 patch receives best-effort maintainer security assessment and fixes.
|
||||||
Security reports are welcome and receive best-effort maintainer assessment and
|
Canonical Gitea tags and release announcements establish availability. The
|
||||||
fixes while this pair is current. This is not production support,
|
published RC.1 pair remains covered until final v1 supersedes it. This policy is
|
||||||
an SLA, a fitness guarantee, or a promise that a fix will preserve beta APIs.
|
not an SLA, certification, or fitness guarantee.
|
||||||
|
|
||||||
The community is invited to help find compatibility gaps, especially on macOS.
|
The community is invited to help find portability gaps outside the maintained
|
||||||
|
Linux/amd64 and Darwin/arm64 targets, but those reports do not create a support
|
||||||
|
or release commitment.
|
||||||
That invitation does not outsource security assurance. Maintainers retain
|
That invitation does not outsource security assurance. Maintainers retain
|
||||||
responsibility for vulnerability review, triage, remediation decisions,
|
responsibility for vulnerability review, triage, remediation decisions,
|
||||||
advisories, and release decisions.
|
advisories, and release decisions.
|
||||||
@@ -18,11 +20,14 @@ advisories, and release decisions.
|
|||||||
|
|
||||||
| Version | Security status |
|
| Version | Security status |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `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 |
|
| `v1.0.0` compiler and `sando/v1.0.0` runtime | Stable v1 line; latest published v1 patch receives best-effort security assessment and fixes |
|
||||||
|
| `v1.0.0-rc.1` compiler and `sando/v1.0.0-rc.1` runtime | Published release-candidate pair; covered until final v1 supersedes it |
|
||||||
|
| `v1.0.0-beta.2` compiler and `sando/v1.0.0-beta.1` runtime | Superseded evaluation/classroom pair after RC publication |
|
||||||
| 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 |
|
||||||
|
|
||||||
Beta support lasts until it is superseded or explicitly withdrawn in the
|
The latest stable v1 patch and current prerelease receive fixes. Prerelease
|
||||||
|
support lasts until it is superseded or explicitly withdrawn in the
|
||||||
supported-version table and release notes. Published tags are immutable. A
|
supported-version table and release notes. Published tags are immutable. A
|
||||||
security fix is issued as a new version, never by silently replacing a tag.
|
security fix is issued as a new version, never by silently replacing a tag.
|
||||||
|
|
||||||
@@ -35,7 +40,7 @@ public issue.
|
|||||||
If that new mailbox rejects or bounces a message, retain the report and open a
|
If that new mailbox rejects or bounces a message, retain the report and open a
|
||||||
canonical Gitea issue containing only the fact that the private security contact
|
canonical Gitea issue containing only the fact that the private security contact
|
||||||
failed. Do not include technical details or sensitive data. The maintainer will
|
failed. Do not include technical details or sensitive data. The maintainer will
|
||||||
publish a corrected private route. Ordinary usage, classroom, and macOS
|
publish a corrected private route. Ordinary usage, classroom, and portability
|
||||||
compatibility reports that do not reveal a vulnerability may use a public issue.
|
compatibility reports that do not reveal a vulnerability may use a public issue.
|
||||||
|
|
||||||
Helpful reports include:
|
Helpful reports include:
|
||||||
@@ -55,8 +60,8 @@ ownership, backup, and recovery procedure have been tested.
|
|||||||
## What to expect
|
## What to expect
|
||||||
|
|
||||||
These are best-effort targets for a founder-maintained project, not an SLA.
|
These are best-effort targets for a founder-maintained project, not an SLA.
|
||||||
They describe maintainer responsibilities; asking the community to test a beta
|
They describe maintainer responsibilities; inviting community testing
|
||||||
does not ask reporters to investigate or remediate it:
|
does not make reporters responsible for investigation or remediation:
|
||||||
|
|
||||||
- acknowledge a report within 7 calendar days;
|
- acknowledge a report within 7 calendar days;
|
||||||
- provide an initial severity/scope assessment within 14 calendar days when a
|
- provide an initial severity/scope assessment within 14 calendar days when a
|
||||||
@@ -75,6 +80,28 @@ may be delivered through a new immutable version, a retraction, an advisory, or
|
|||||||
documentation that narrows an incorrect guarantee. Published tags will not be
|
documentation that narrows an incorrect guarantee. Published tags will not be
|
||||||
moved or silently replaced.
|
moved or silently replaced.
|
||||||
|
|
||||||
|
## Severity, advisories, retractions, and CVEs
|
||||||
|
|
||||||
|
Triage considers attacker prerequisites, affected trust boundary, data or code
|
||||||
|
impact, availability impact, default reachability, and whether exploitation
|
||||||
|
crosses the documented application-owned capability boundary. The project uses
|
||||||
|
plain-language critical, high, moderate, and low labels; it does not publish a
|
||||||
|
CVSS score unless the vector and calculation have been reviewed.
|
||||||
|
|
||||||
|
An accepted vulnerability affecting a published version receives a canonical
|
||||||
|
advisory or release security note identifying affected and fixed versions,
|
||||||
|
mitigations, credit, and disclosure chronology as safely available. The
|
||||||
|
maintainer requests a CVE when a disclosed vulnerability materially affects a
|
||||||
|
published release and a stable public advisory exists. A CVE is an identifier,
|
||||||
|
not evidence of severity or independent validation.
|
||||||
|
|
||||||
|
Published source and tags are never deleted or moved to hide a faulty release.
|
||||||
|
When Go tooling supports the boundary, a later immutable module version may
|
||||||
|
use a `retract` directive with a concise rationale. Otherwise the project
|
||||||
|
publishes a superseding version and marks the affected release in canonical
|
||||||
|
release notes. Retraction does not erase source, evidence, or the disclosure
|
||||||
|
record.
|
||||||
|
|
||||||
## Scope and trust boundary
|
## Scope and trust boundary
|
||||||
|
|
||||||
The most useful reports concern:
|
The most useful reports concern:
|
||||||
@@ -134,17 +161,17 @@ 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.
|
||||||
|
|
||||||
The Beta 2 language server is read-only and standard-library-only. Its tests
|
The language server is read-only and standard-library-only. Its tests
|
||||||
exercise bounded framing, malformed messages, cancellation, UTF-16 positions,
|
exercise bounded framing, malformed messages, cancellation, UTF-16 positions,
|
||||||
unsaved overlays, workspace re-indexing, and no generated writes. Source review
|
unsaved overlays, workspace re-indexing, and no generated writes. Source review
|
||||||
forbids process execution and network-client imports inside the server. Editor
|
forbids process execution and network-client imports inside the server. Editor
|
||||||
workspace trust and executable resolution remain editor-client responsibilities.
|
workspace trust and executable resolution remain editor-client responsibilities.
|
||||||
|
|
||||||
Beta 1 uses signed annotated source tags, but precedes the
|
Beta 1 used signed annotated source tags but predates the complete artifact
|
||||||
complete prebuilt-artifact and key-recovery system. Signed binaries, checksums,
|
system. RC.1 introduced signed or checksummed binaries, SPDX SBOMs, reproducible
|
||||||
an SBOM, reproducible archives, and complete source/build provenance are
|
archives, and source/build provenance; final-v1 artifacts retain that release
|
||||||
release-candidate and final-v1 gates. Their absence from a source-only beta must
|
boundary and require their own exact-version verification. Those controls
|
||||||
not be read as evidence of artifact authenticity beyond the verified tag and
|
establish artifact identity and provenance; they do not constitute an
|
||||||
canonical source.
|
independent security audit or prove the absence of vulnerabilities.
|
||||||
|
|
||||||
This policy is practical project guidance, not legal advice.
|
This policy is practical project guidance, not legal advice.
|
||||||
|
|||||||
@@ -21,7 +21,23 @@ func Card(card model.Card)
|
|||||||
|
|
||||||
The v1 target is `go`. Other target names are rejected. The explicit target is an architectural seam for a possible future San backend; it is not a promise that such a backend exists.
|
The v1 target is `go`. Other target names are rejected. The explicit target is an architectural seam for a possible future San backend; it is not a promise that such a backend exists.
|
||||||
|
|
||||||
The header permits one package clause, ordinary Go imports, and one bodyless, receiverless function declaration. The component name is the function name and its parameters form the generated typed API. Multiple components, methods, global declarations, and executable initialization in the header are errors.
|
The header permits one package clause, ordinary Go imports, and one bodyless,
|
||||||
|
receiverless function declaration. The component name is the function name and
|
||||||
|
its parameters form the generated typed API. A Go type-parameter list is part
|
||||||
|
of the v1 grammar and is preserved after `go/format` normalization:
|
||||||
|
|
||||||
|
```sando
|
||||||
|
<?sando go
|
||||||
|
package views
|
||||||
|
|
||||||
|
func List[T ~string](values []T)
|
||||||
|
?>
|
||||||
|
<ul><? for _, value := range values { ?><li><?= value ?></li><? } ?></ul>
|
||||||
|
```
|
||||||
|
|
||||||
|
Constraints, inference, and instantiation use ordinary Go rules; Sandwich Hime
|
||||||
|
does not add a second generic type system. Multiple components, methods, global
|
||||||
|
declarations, and executable initialization in the header are errors.
|
||||||
|
|
||||||
## Template tags
|
## Template tags
|
||||||
|
|
||||||
@@ -42,6 +58,12 @@ For `func Card(card model.Card)`, generation emits:
|
|||||||
func Card(card model.Card) sando.Component
|
func Card(card model.Card) sando.Component
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For the generic example above, generation emits:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func List[T ~string](values []T) sando.Component
|
||||||
|
```
|
||||||
|
|
||||||
The component captures its typed parameters and renders later with a context and writer. All static writes, escaping operations, nested component renders, and application-provided writers propagate errors.
|
The component captures its typed parameters and renders later with a context and writer. All static writes, escaping operations, nested component renders, and application-provided writers propagate errors.
|
||||||
|
|
||||||
Generated files are adjacent to their source (`card.sando.go`), formatted with `go/format`, and contain the compiler version, runtime ABI, source digest, and source mappings. Hime-san does not inject the compiler's AGPL license identifier or copyright claim. An application rightsholder remains free to select AGPL intentionally through the application's own license policy.
|
Generated files are adjacent to their source (`card.sando.go`), formatted with `go/format`, and contain the compiler version, runtime ABI, source digest, and source mappings. Hime-san does not inject the compiler's AGPL license identifier or copyright claim. An application rightsholder remains free to select AGPL intentionally through the application's own license policy.
|
||||||
@@ -62,6 +84,35 @@ V1 recognizes:
|
|||||||
|
|
||||||
Ordinary URL values are attribute-escaped and rejected at render time when their normalized scheme is dangerous. Only `sando.TrustedURL`, made by an explicit `sando.TrustURL` call in trusted Go code, may bypass that scheme policy. The analogous trusted HTML, JavaScript, and CSS types are opaque and have conspicuous constructors.
|
Ordinary URL values are attribute-escaped and rejected at render time when their normalized scheme is dangerous. Only `sando.TrustedURL`, made by an explicit `sando.TrustURL` call in trusted Go code, may bypass that scheme policy. The analogous trusted HTML, JavaScript, and CSS types are opaque and have conspicuous constructors.
|
||||||
|
|
||||||
|
### V1 output matrix
|
||||||
|
|
||||||
|
| Template position | Ordinary value | Explicit trusted value | Unsupported or rejected |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| HTML text | HTML-escaped by `WriteText` | `TrustedHTML` is written verbatim | Dynamic markup structure remains the caller's capability boundary |
|
||||||
|
| `title`/`textarea` RCDATA | HTML-escaped by `WriteRCDATA` | Trusted wrappers are still escaped | Closing the element through a value |
|
||||||
|
| Quoted ordinary attribute | HTML-escaped by `WriteAttr` | Trusted wrappers stringify, then escape | Unquoted values, dynamic names, and event-handler attributes |
|
||||||
|
| Quoted URL attribute | Scheme-checked, normalized, then attribute-escaped by `WriteURL` | `TrustedURL` bypasses only the scheme check | Ambiguous schemes, controls, and non-allowlisted schemes |
|
||||||
|
| `script` data | Not accepted | `TrustedJS` is written verbatim | Plain strings and ambiguous escaped-script parser states |
|
||||||
|
| `style` data | Not accepted | `TrustedCSS` is written verbatim | Plain strings and dynamic style attributes |
|
||||||
|
| Ordinary HTML content | `<?~` renders a `Component` | Handwritten components are explicit trusted-output capabilities | Component rendering in attributes, tags, comments, RCDATA, script, or style |
|
||||||
|
|
||||||
|
Ordinary values use `fmt.Sprint` semantics before contextual normalization.
|
||||||
|
Invalid UTF-8 and NUL bytes become U+FFFD in text, RCDATA, attribute, and URL
|
||||||
|
helpers. A nil render context, writer, component, typed-nil component, or
|
||||||
|
typed-nil writer produces the corresponding stable sentinel error rather than
|
||||||
|
a panic. All writer errors and short writes propagate.
|
||||||
|
|
||||||
|
Relative URLs and the `http`, `https`, `mailto`, and `tel` schemes are accepted.
|
||||||
|
Leading and trailing Unicode whitespace is removed before classification;
|
||||||
|
ASCII controls, ambiguous scheme syntax, and every other ordinary scheme are
|
||||||
|
rejected before bytes are written. `TrustedURL` does not bypass quoted-attribute
|
||||||
|
escaping.
|
||||||
|
|
||||||
|
Handwritten Go statements, handwritten components, and every `Trust*` call are
|
||||||
|
application-owned capabilities. Sandwich Hime does not sanitize or sandbox
|
||||||
|
trusted source, prevent panics or blocking inside application code, provide
|
||||||
|
HTTP routing, or infer that a string became safe elsewhere in the program.
|
||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
V1 is a clean break from the 2025 prototype. `.go.hime`, injected `himesan` helper directories, `SandoName(io.Writer)` functions, Go plugins, and nested demonstration modules are not accepted or generated. `.san` is not and will never be a Sandwich Hime extension.
|
V1 is a clean break from the 2025 prototype. `.go.hime`, injected `himesan` helper directories, `SandoName(io.Writer)` functions, Go plugins, and nested demonstration modules are not accepted or generated. `.san` is not and will never be a Sandwich Hime extension.
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
|
|
||||||
“Sandwich Hime,” “Hime-san,” the Sandwich Hime flag, mascot, and official project artwork identify the project stewarded by Cole Speelman. Code licenses do not grant trademark rights, and this policy does not limit anyone's rights to use the code under those licenses.
|
“Sandwich Hime,” “Hime-san,” the Sandwich Hime flag, mascot, and official project artwork identify the project stewarded by Cole Speelman. Code licenses do not grant trademark rights, and this policy does not limit anyone's rights to use the code under those licenses.
|
||||||
|
|
||||||
|
Sandwich Hime is the primary project identity. Hime-san names the tool within
|
||||||
|
that project, not a separate framework brand. The project does not claim the
|
||||||
|
word “Hime” in general. This naming scope is not a statement of registration
|
||||||
|
or a conclusion about confusing similarity in any jurisdiction.
|
||||||
|
|
||||||
Truthful, non-confusing statements such as “Built with Sandwich Hime,” “compatible with Sandwich Hime,” and unmodified community badges are welcome. They must not imply sponsorship, certification, or official status.
|
Truthful, non-confusing statements such as “Built with Sandwich Hime,” “compatible with Sandwich Hime,” and unmodified community badges are welcome. They must not imply sponsorship, certification, or official status.
|
||||||
|
|
||||||
If a modified distribution uses these project marks, it should use a distinct primary product name, clearly identify its changes, and avoid official artwork or presentation that could confuse users about origin. A fork that does not use the project marks is not restricted by this policy. Either kind of fork may truthfully describe compatibility.
|
If a modified distribution uses these project marks, it should use a distinct primary product name, clearly identify its changes, and avoid official artwork or presentation that could confuse users about origin. A fork that does not use the project marks is not restricted by this policy. Either kind of fork may truthfully describe compatibility.
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// Command himesan-release creates deterministic unsigned artifacts and native
|
||||||
|
// verification receipts. Signing and notarization intentionally remain outside
|
||||||
|
// this command and outside unattended runner authority.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gamertan.com/sandwich-hime/internal/releaseartifact"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(os.Args[1:]); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "himesan-release: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(arguments []string) error {
|
||||||
|
if len(arguments) == 0 {
|
||||||
|
return errors.New("usage: himesan-release <package|receipt|evidence-manifest|verify-evidence|verify-native|extract-macos|finalize-macos> [options]")
|
||||||
|
}
|
||||||
|
switch arguments[0] {
|
||||||
|
case "package":
|
||||||
|
return runPackage(arguments[1:])
|
||||||
|
case "receipt":
|
||||||
|
return runReceipt(arguments[1:])
|
||||||
|
case "evidence-manifest":
|
||||||
|
return runEvidenceManifest(arguments[1:])
|
||||||
|
case "verify-evidence":
|
||||||
|
return runVerifyEvidence(arguments[1:])
|
||||||
|
case "verify-native":
|
||||||
|
return runVerifyNative(arguments[1:])
|
||||||
|
case "extract-macos":
|
||||||
|
return runExtractMacOS(arguments[1:])
|
||||||
|
case "finalize-macos":
|
||||||
|
return runFinalizeMacOS(arguments[1:])
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown command %q", arguments[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runVerifyNative(arguments []string) error {
|
||||||
|
flags := flag.NewFlagSet("verify-native", flag.ContinueOnError)
|
||||||
|
var directory string
|
||||||
|
var expected releaseartifact.NativeReceiptExpectation
|
||||||
|
flags.StringVar(&directory, "directory", "", "four-lane native receipt directory")
|
||||||
|
flags.StringVar(&expected.Repository, "repository", "", "repository identity")
|
||||||
|
flags.StringVar(&expected.Commit, "commit", "", "source commit")
|
||||||
|
flags.StringVar(&expected.Tree, "tree", "", "source tree")
|
||||||
|
if err := flags.Parse(arguments); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
summary, err := releaseartifact.VerifyNativeReceiptSet(directory, expected)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.NewEncoder(os.Stdout).Encode(summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runExtractMacOS(arguments []string) error {
|
||||||
|
flags := flag.NewFlagSet("extract-macos", flag.ContinueOnError)
|
||||||
|
var archive, checksum, output string
|
||||||
|
flags.StringVar(&archive, "archive", "", "unsigned Darwin/arm64 archive")
|
||||||
|
flags.StringVar(&checksum, "sha256", "", "approved archive SHA-256")
|
||||||
|
flags.StringVar(&output, "output", "", "empty extraction parent directory")
|
||||||
|
if err := flags.Parse(arguments); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
root, err := releaseartifact.ExtractVerifiedMacOSPackage(archive, checksum, output)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.NewEncoder(os.Stdout).Encode(map[string]string{"root": root, "unsigned_archive_sha256": checksum})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runFinalizeMacOS(arguments []string) error {
|
||||||
|
flags := flag.NewFlagSet("finalize-macos", flag.ContinueOnError)
|
||||||
|
var options releaseartifact.MacOSSigningOptions
|
||||||
|
flags.StringVar(&options.Directory, "directory", "", "extracted signed distribution directory")
|
||||||
|
flags.StringVar(&options.UnsignedArchiveSHA256, "unsigned-archive-sha256", "", "approved unsigned archive SHA-256")
|
||||||
|
flags.StringVar(&options.Identity, "identity", "", "Developer ID identity")
|
||||||
|
flags.StringVar(&options.Identifier, "identifier", "", "signed binary identifier")
|
||||||
|
flags.StringVar(&options.FinalizedAt, "finalized-at", "", "RFC3339 finalization time")
|
||||||
|
if err := flags.Parse(arguments); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := releaseartifact.FinalizeSignedMacOSDistribution(options); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.NewEncoder(os.Stdout).Encode(map[string]any{"valid": true, "directory": options.Directory})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runEvidenceManifest(arguments []string) error {
|
||||||
|
flags := flag.NewFlagSet("evidence-manifest", flag.ContinueOnError)
|
||||||
|
var directory string
|
||||||
|
var identity releaseartifact.EvidenceIdentity
|
||||||
|
flags.StringVar(&directory, "directory", "", "reviewed evidence directory")
|
||||||
|
bindEvidenceIdentityFlags(flags, &identity, true)
|
||||||
|
if err := flags.Parse(arguments); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
checksum, err := releaseartifact.WriteEvidenceManifest(directory, identity)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.NewEncoder(os.Stdout).Encode(map[string]string{"manifest": filepath.Join(directory, "RELEASE-EVIDENCE.json"), "sha256": checksum})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runVerifyEvidence(arguments []string) error {
|
||||||
|
flags := flag.NewFlagSet("verify-evidence", flag.ContinueOnError)
|
||||||
|
var directory string
|
||||||
|
var identity releaseartifact.EvidenceIdentity
|
||||||
|
flags.StringVar(&directory, "directory", "", "sealed evidence directory")
|
||||||
|
bindEvidenceIdentityFlags(flags, &identity, false)
|
||||||
|
if err := flags.Parse(arguments); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := releaseartifact.VerifyEvidenceManifest(directory, identity); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.NewEncoder(os.Stdout).Encode(map[string]any{"valid": true, "files": releaseartifact.RequiredEvidenceFiles()})
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindEvidenceIdentityFlags(flags *flag.FlagSet, identity *releaseartifact.EvidenceIdentity, review bool) {
|
||||||
|
flags.StringVar(&identity.Repository, "repository", "", "canonical repository identity")
|
||||||
|
flags.StringVar(&identity.Version, "version", "", "candidate semantic version")
|
||||||
|
flags.StringVar(&identity.Commit, "commit", "", "source commit")
|
||||||
|
flags.StringVar(&identity.Tree, "tree", "", "source tree")
|
||||||
|
if review {
|
||||||
|
flags.StringVar(&identity.ReviewedBy, "reviewed-by", "", "human reviewer identity")
|
||||||
|
flags.StringVar(&identity.ReviewedAt, "reviewed-at", "", "RFC3339 review time")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPackage(arguments []string) error {
|
||||||
|
flags := flag.NewFlagSet("package", flag.ContinueOnError)
|
||||||
|
var options releaseartifact.PackageOptions
|
||||||
|
flags.StringVar(&options.Version, "version", "", "candidate semantic version")
|
||||||
|
flags.StringVar(&options.Commit, "commit", "", "source commit")
|
||||||
|
flags.StringVar(&options.Tree, "tree", "", "source tree")
|
||||||
|
flags.StringVar(&options.GoVersion, "go-version", "", "Go toolchain identity")
|
||||||
|
flags.StringVar(&options.GOOS, "goos", "", "target operating system")
|
||||||
|
flags.StringVar(&options.GOARCH, "goarch", "", "target architecture")
|
||||||
|
flags.StringVar(&options.BinaryPath, "binary", "", "unsigned native binary")
|
||||||
|
flags.StringVar(&options.LicensePath, "license", "LICENSE", "license text")
|
||||||
|
flags.StringVar(&options.ReleaseNotes, "release-notes", "RELEASE.md", "release notes")
|
||||||
|
flags.StringVar(&options.OutputDirectory, "output", "", "output directory")
|
||||||
|
flags.Int64Var(&options.SourceDateEpoch, "source-date-epoch", 0, "fixed Unix timestamp")
|
||||||
|
if err := flags.Parse(arguments); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result, err := releaseartifact.Package(options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.NewEncoder(os.Stdout).Encode(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runReceipt(arguments []string) error {
|
||||||
|
flags := flag.NewFlagSet("receipt", flag.ContinueOnError)
|
||||||
|
var receipt releaseartifact.Receipt
|
||||||
|
var output, gates, generatedFiles string
|
||||||
|
flags.StringVar(&output, "output", "", "receipt output path")
|
||||||
|
flags.StringVar(&receipt.Repository, "repository", "", "repository identity")
|
||||||
|
flags.StringVar(&receipt.Commit, "commit", "", "source commit")
|
||||||
|
flags.StringVar(&receipt.Tree, "tree", "", "source tree")
|
||||||
|
flags.StringVar(&receipt.GOOS, "goos", "", "native operating system")
|
||||||
|
flags.StringVar(&receipt.GOARCH, "goarch", "", "native architecture")
|
||||||
|
flags.StringVar(&receipt.GoVersion, "go-version", "", "Go toolchain identity")
|
||||||
|
flags.StringVar(&receipt.RunnerVersion, "runner-version", "", "Gitea Runner version")
|
||||||
|
flags.StringVar(&receipt.RunnerName, "runner-name", "", "runner identity")
|
||||||
|
flags.StringVar(&receipt.UnsignedArtifactSHA, "artifact-sha256", "", "optional unsigned artifact digest")
|
||||||
|
flags.StringVar(&receipt.CompletedAt, "completed-at", "", "RFC3339 completion time")
|
||||||
|
flags.StringVar(&gates, "gates", "", "comma-separated successful gates")
|
||||||
|
flags.StringVar(&generatedFiles, "generated-files", "", "comma-separated generated output paths")
|
||||||
|
if err := flags.Parse(arguments); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if output == "" {
|
||||||
|
return errors.New("output is required")
|
||||||
|
}
|
||||||
|
receipt.SuccessfulGates = splitList(gates)
|
||||||
|
digest, err := releaseartifact.DigestFiles(splitList(generatedFiles))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
receipt.GeneratedDigest = digest
|
||||||
|
checksum, err := releaseartifact.WriteReceipt(output, receipt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.NewEncoder(os.Stdout).Encode(map[string]string{"receipt": output, "sha256": checksum})
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitList(value string) []string {
|
||||||
|
var values []string
|
||||||
|
for _, item := range strings.Split(value, ",") {
|
||||||
|
item = strings.TrimSpace(item)
|
||||||
|
if item != "" {
|
||||||
|
values = append(values, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gamertan.com/sandwich-hime/internal/testpath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contractSchema struct {
|
||||||
|
AdditionalProperties bool `json:"additionalProperties"`
|
||||||
|
Required []string `json:"required"`
|
||||||
|
Properties map[string]json.RawMessage `json:"properties"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV1CLIHelpContract(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
want, err := os.ReadFile(filepath.Join("..", "..", "contracts", "himesan-cli-help-v1.txt"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want = bytes.TrimPrefix(want, []byte("# SPDX-License-Identifier: AGPL-3.0-only\n\n"))
|
||||||
|
var output bytes.Buffer
|
||||||
|
printHelp(&output)
|
||||||
|
if !bytes.Equal(output.Bytes(), want) {
|
||||||
|
t.Fatalf("CLI help contract drifted\n--- want ---\n%s--- got ---\n%s", want, output.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV1VersionJSONSchemaMatchesOutput(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
schema := readContractSchema(t, "himesan-version-output-v1.schema.json")
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
if code := run(context.Background(), []string{"version", "--json"}, &stdout, &stderr); code != 0 {
|
||||||
|
t.Fatalf("version exit code = %d: %s", code, stderr.String())
|
||||||
|
}
|
||||||
|
var output map[string]any
|
||||||
|
if err := json.Unmarshal(stdout.Bytes(), &output); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assertObjectShape(t, output, schema, "version output")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV1OperationJSONSchemaMatchesSuccessAndDiagnosticOutput(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
schema := readContractSchema(t, "himesan-operation-output-v1.schema.json")
|
||||||
|
directory := testpath.TempDir(t)
|
||||||
|
source := filepath.Join(directory, "page.sando")
|
||||||
|
if err := os.WriteFile(source, []byte("<?sando go\npackage views\nfunc Page()\n?>\n<p>page</p>\n"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
if code := run(context.Background(), []string{"check", "--json", source}, &stdout, &stderr); code != 1 {
|
||||||
|
t.Fatalf("missing-output check exit code = %d, want 1: %s", code, stderr.String())
|
||||||
|
}
|
||||||
|
var output map[string]any
|
||||||
|
if err := json.Unmarshal(stdout.Bytes(), &output); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assertObjectShape(t, output, schema, "operation output")
|
||||||
|
|
||||||
|
resultSchema := nestedSchema(t, schema.Properties["result"])
|
||||||
|
result, ok := output["result"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("result = %T, want object", output["result"])
|
||||||
|
}
|
||||||
|
assertObjectShape(t, result, resultSchema, "operation result")
|
||||||
|
files, ok := result["files"].([]any)
|
||||||
|
if !ok || len(files) != 1 {
|
||||||
|
t.Fatalf("files = %#v, want one item", result["files"])
|
||||||
|
}
|
||||||
|
filesProperty := rawObject(t, resultSchema.Properties["files"])
|
||||||
|
fileSchema := nestedSchema(t, filesProperty["items"])
|
||||||
|
assertObjectShape(t, files[0].(map[string]any), fileSchema, "file result")
|
||||||
|
|
||||||
|
diagnostics, ok := result["diagnostics"].([]any)
|
||||||
|
if !ok || len(diagnostics) == 0 {
|
||||||
|
t.Fatalf("diagnostics = %#v, want at least one item", result["diagnostics"])
|
||||||
|
}
|
||||||
|
diagnosticsProperty := rawObject(t, resultSchema.Properties["diagnostics"])
|
||||||
|
diagnosticSchema := nestedSchema(t, diagnosticsProperty["items"])
|
||||||
|
assertObjectShape(t, diagnostics[0].(map[string]any), diagnosticSchema, "diagnostic")
|
||||||
|
}
|
||||||
|
|
||||||
|
func readContractSchema(t *testing.T, name string) contractSchema {
|
||||||
|
t.Helper()
|
||||||
|
contents, err := os.ReadFile(filepath.Join("..", "..", "contracts", name))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var schema contractSchema
|
||||||
|
if err := json.Unmarshal(contents, &schema); err != nil {
|
||||||
|
t.Fatalf("decode %s: %v", name, err)
|
||||||
|
}
|
||||||
|
if schema.AdditionalProperties || len(schema.Properties) == 0 {
|
||||||
|
t.Fatalf("%s is not a closed object schema", name)
|
||||||
|
}
|
||||||
|
return schema
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedSchema(t *testing.T, raw json.RawMessage) contractSchema {
|
||||||
|
t.Helper()
|
||||||
|
var schema contractSchema
|
||||||
|
if err := json.Unmarshal(raw, &schema); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return schema
|
||||||
|
}
|
||||||
|
|
||||||
|
func rawObject(t *testing.T, raw json.RawMessage) map[string]json.RawMessage {
|
||||||
|
t.Helper()
|
||||||
|
var object map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(raw, &object); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return object
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertObjectShape(t *testing.T, actual map[string]any, schema contractSchema, label string) {
|
||||||
|
t.Helper()
|
||||||
|
actualKeys := make([]string, 0, len(actual))
|
||||||
|
for key := range actual {
|
||||||
|
actualKeys = append(actualKeys, key)
|
||||||
|
if _, declared := schema.Properties[key]; !declared {
|
||||||
|
t.Fatalf("%s emitted undeclared property %q", label, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(actualKeys)
|
||||||
|
for _, required := range schema.Required {
|
||||||
|
if _, present := actual[required]; !present {
|
||||||
|
t.Fatalf("%s omitted required property %q (got %v)", label, required, actualKeys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(actual) == 0 || reflect.ValueOf(actual).IsNil() {
|
||||||
|
t.Fatalf("%s is empty", label)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// Command himesan is the development-time compiler and tooling entry point for
|
||||||
|
// Sandwich Hime's HTML-first, ahead-of-time .sando templates.
|
||||||
|
//
|
||||||
|
// Hime-san generates deterministic, formatted .sando.go files beside their
|
||||||
|
// sources. Applications commit those generated files and deploy their ordinary
|
||||||
|
// Go program with the small Apache-2.0 sando runtime. The compiler, language
|
||||||
|
// server, and development supervisor are not production dependencies.
|
||||||
|
//
|
||||||
|
// # Install
|
||||||
|
//
|
||||||
|
// Install the current compiler release with the Go toolchain:
|
||||||
|
//
|
||||||
|
// go install gamertan.com/sandwich-hime/cmd/himesan@latest
|
||||||
|
//
|
||||||
|
// The compiler and runtime have independent tags. For a consuming module, add
|
||||||
|
// the exact sando runtime release first, then install and pin the exact compiler
|
||||||
|
// version selected by that project's documentation. The module README records
|
||||||
|
// current release versions and the runtime-first installation sequence.
|
||||||
|
//
|
||||||
|
// # Generate and check
|
||||||
|
//
|
||||||
|
// Generate adjacent Go files for templates below the current module:
|
||||||
|
//
|
||||||
|
// himesan generate ./...
|
||||||
|
//
|
||||||
|
// Check committed output without writing files:
|
||||||
|
//
|
||||||
|
// himesan check ./...
|
||||||
|
//
|
||||||
|
// The shorter "gen" command aliases generate. The friendly "bless" command is
|
||||||
|
// a read-only alias for check. Both generate and check accept --json for one
|
||||||
|
// bounded machine-readable result.
|
||||||
|
//
|
||||||
|
// Generation is deterministic and replaces only compiler-owned output
|
||||||
|
// atomically. It does not edit handwritten Go files or go.mod. Check reports
|
||||||
|
// stale, missing, invalid, and orphaned generated output through the same
|
||||||
|
// compiler diagnostics used by generation.
|
||||||
|
//
|
||||||
|
// # Local development
|
||||||
|
//
|
||||||
|
// The optional development supervisor regenerates templates, builds the
|
||||||
|
// application's own net/http program, health-checks a new loopback candidate,
|
||||||
|
// and preserves the last healthy process when a candidate fails:
|
||||||
|
//
|
||||||
|
// himesan dev [flags] [package] [-- app-args...]
|
||||||
|
//
|
||||||
|
// Its stable local proxy and browser diagnostics are development conveniences,
|
||||||
|
// not an application framework or production server. Projects may configure
|
||||||
|
// the supervisor with himesan.json or use generate, check, and ordinary Go
|
||||||
|
// tooling directly.
|
||||||
|
//
|
||||||
|
// # Editor integration
|
||||||
|
//
|
||||||
|
// The editor-neutral language server runs over standard input and output:
|
||||||
|
//
|
||||||
|
// himesan lsp --stdio
|
||||||
|
//
|
||||||
|
// It analyzes saved sources and unsaved document overlays with the compiler's
|
||||||
|
// parser and context model. It does not generate files, execute project code,
|
||||||
|
// invoke Go, fetch modules, access the network, or start the development
|
||||||
|
// supervisor.
|
||||||
|
//
|
||||||
|
// # Operational contracts
|
||||||
|
//
|
||||||
|
// Use "himesan version --json" for compiler, runtime ABI, Go, and feature
|
||||||
|
// identity. The repository freezes the command surface, exit meanings,
|
||||||
|
// diagnostic codes, JSON schemas, and configuration schema in its contracts
|
||||||
|
// directory. Language syntax and contextual-safety rules are specified in
|
||||||
|
// SPEC.md and docs/THREAT_MODEL.md at the canonical repository.
|
||||||
|
package main
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gamertan.com/sandwich-hime/internal/compiler"
|
"gamertan.com/sandwich-hime/internal/compiler"
|
||||||
|
"gamertan.com/sandwich-hime/internal/testpath"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRunHelpVersionAndUnknownCommand(t *testing.T) {
|
func TestRunHelpVersionAndUnknownCommand(t *testing.T) {
|
||||||
@@ -67,7 +68,7 @@ func TestRunHelpVersionAndUnknownCommand(t *testing.T) {
|
|||||||
func TestGenerateCheckBlessAndJSONDiagnostics(t *testing.T) {
|
func TestGenerateCheckBlessAndJSONDiagnostics(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
directory := t.TempDir()
|
directory := testpath.TempDir(t)
|
||||||
sourcePath := filepath.Join(directory, "hello.sando")
|
sourcePath := filepath.Join(directory, "hello.sando")
|
||||||
source := "<?sando go\npackage views\nfunc Hello(name string)\n?>\n<p><?= name ?></p>\n"
|
source := "<?sando go\npackage views\nfunc Hello(name string)\n?>\n<p><?= name ?></p>\n"
|
||||||
if err := os.WriteFile(sourcePath, []byte(source), 0o600); err != nil {
|
if err := os.WriteFile(sourcePath, []byte(source), 0o600); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
HIM1001
|
||||||
|
HIM1002
|
||||||
|
HIM1101
|
||||||
|
HIM1102
|
||||||
|
HIM1103
|
||||||
|
HIM1104
|
||||||
|
HIM1105
|
||||||
|
HIM1110
|
||||||
|
HIM1111
|
||||||
|
HIM1112
|
||||||
|
HIM1113
|
||||||
|
HIM1114
|
||||||
|
HIM1115
|
||||||
|
HIM1116
|
||||||
|
HIM1117
|
||||||
|
HIM1118
|
||||||
|
HIM1119
|
||||||
|
HIM1120
|
||||||
|
HIM1121
|
||||||
|
HIM1122
|
||||||
|
HIM1123
|
||||||
|
HIM1201
|
||||||
|
HIM1202
|
||||||
|
HIM1203
|
||||||
|
HIM1210
|
||||||
|
HIM1301
|
||||||
|
HIM1302
|
||||||
|
HIM1303
|
||||||
|
HIM1310
|
||||||
|
HIM1311
|
||||||
|
HIM1320
|
||||||
|
HIM1321
|
||||||
|
HIM1322
|
||||||
|
HIM1323
|
||||||
|
HIM1324
|
||||||
|
HIM1325
|
||||||
|
HIM1326
|
||||||
|
HIM1327
|
||||||
|
HIM1328
|
||||||
|
HIM1329
|
||||||
|
HIM1330
|
||||||
|
HIM1331
|
||||||
|
HIM1332
|
||||||
|
HIM1333
|
||||||
|
HIM1340
|
||||||
|
HIM1341
|
||||||
|
HIM1342
|
||||||
|
HIM1343
|
||||||
|
HIM1344
|
||||||
|
HIM1345
|
||||||
|
HIM1346
|
||||||
|
HIM1347
|
||||||
|
HIM1350
|
||||||
|
HIM1351
|
||||||
|
HIM1352
|
||||||
|
HIM1353
|
||||||
|
HIM1354
|
||||||
|
HIM1355
|
||||||
|
HIM1356
|
||||||
|
HIM1357
|
||||||
|
HIM1401
|
||||||
|
HIM1410
|
||||||
|
HIM1500
|
||||||
|
HIM1501
|
||||||
|
HIM1901
|
||||||
|
HIM1902
|
||||||
|
HIM1903
|
||||||
|
HIM2001
|
||||||
|
HIM2002
|
||||||
|
HIM2003
|
||||||
|
HIM2004
|
||||||
|
HIM2005
|
||||||
|
HIM2006
|
||||||
|
HIM2007
|
||||||
|
HIM2008
|
||||||
|
HIM2009
|
||||||
|
HIM2010
|
||||||
|
HIM2011
|
||||||
|
HIM2012
|
||||||
|
HIM2013
|
||||||
|
HIM2014
|
||||||
|
HIM2101
|
||||||
|
HIM2102
|
||||||
|
HIM2103
|
||||||
|
HIM2104
|
||||||
|
HIM2110
|
||||||
|
HIM2201
|
||||||
|
HIM2202
|
||||||
|
HIM2203
|
||||||
|
HIM2204
|
||||||
|
HIM2205
|
||||||
|
HIM2901
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// Code generated by himesan; DO NOT EDIT.
|
||||||
|
// himesan:compiler <compiler-version>
|
||||||
|
// himesan:runtime-abi <runtime-abi>
|
||||||
|
// himesan:source-sha256 <lowercase-sha256>
|
||||||
|
var _ = <sando-import>.ABISandoV1
|
||||||
|
//line <source-path>:<line>:<column>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
Sandwich Hime / Hime-san — HTML-first typed components for Go
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
himesan generate [--json] [paths...] generate adjacent .sando.go files
|
||||||
|
himesan gen [--json] [paths...] alias for generate
|
||||||
|
himesan check [--json] [paths...] validate sources and committed output without writes
|
||||||
|
himesan bless [--json] [paths...] friendly read-only alias for check
|
||||||
|
himesan dev [flags] [package] [-- app-args...] run the loopback last-good supervisor
|
||||||
|
himesan lsp --stdio run the read-only language server
|
||||||
|
himesan version [--json] print compiler and runtime ABI versions
|
||||||
|
|
||||||
|
Templates use .sando; .san remains exclusively San language source.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://sandwichhime.com/schema/himesan-config-v1.schema.json",
|
||||||
|
"title": "Hime-san local development configuration v1",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["version"],
|
||||||
|
"properties": {
|
||||||
|
"version": {"const": 1},
|
||||||
|
"sourceRoots": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string", "minLength": 1, "pattern": "^[^\\u0000]+$"},
|
||||||
|
"minItems": 1,
|
||||||
|
"default": ["."]
|
||||||
|
},
|
||||||
|
"goPackage": {"type": "string", "minLength": 1, "pattern": "^[^\\u0000\\r\\n]+$", "default": "."},
|
||||||
|
"appArgs": {"type": "array", "items": {"type": "string", "pattern": "^[^\\u0000]*$"}, "default": []},
|
||||||
|
"listenAddressEnv": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$",
|
||||||
|
"default": "HIMESAN_LISTEN_ADDR"
|
||||||
|
},
|
||||||
|
"healthPath": {"type": "string", "pattern": "^/(?!/)[^?#\\u0000\\r\\n]*$", "default": "/"},
|
||||||
|
"proxyAddress": {
|
||||||
|
"type": "string",
|
||||||
|
"anyOf": [
|
||||||
|
{"pattern": "^127(?:\\.[0-9]{1,3}){3}:[0-9]{1,5}$"},
|
||||||
|
{"pattern": "^\\[::1\\]:[0-9]{1,5}$"}
|
||||||
|
],
|
||||||
|
"default": "127.0.0.1:7331"
|
||||||
|
},
|
||||||
|
"additionalWatchRoots": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string", "minLength": 1, "pattern": "^[^\\u0000]+$"},
|
||||||
|
"default": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://sandwichhime.com/schema/himesan-operation-output-v1.schema.json",
|
||||||
|
"title": "Hime-san generate/check JSON result v1",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["command", "ok", "result"],
|
||||||
|
"properties": {
|
||||||
|
"command": {"enum": ["generate", "check", "bless"]},
|
||||||
|
"ok": {"type": "boolean"},
|
||||||
|
"result": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["files", "discovered", "changed", "unchanged", "stale", "missing"],
|
||||||
|
"properties": {
|
||||||
|
"files": {
|
||||||
|
"type": ["array", "null"],
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["source_path", "output_path", "changed", "stale", "missing"],
|
||||||
|
"properties": {
|
||||||
|
"source_path": {"type": "string"},
|
||||||
|
"output_path": {"type": "string"},
|
||||||
|
"changed": {"type": "boolean"},
|
||||||
|
"stale": {"type": "boolean"},
|
||||||
|
"missing": {"type": "boolean"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"diagnostics": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["path", "line", "column", "code", "severity", "message"],
|
||||||
|
"properties": {
|
||||||
|
"path": {"type": "string"},
|
||||||
|
"line": {"type": "integer", "minimum": 1},
|
||||||
|
"column": {"type": "integer", "minimum": 1},
|
||||||
|
"code": {"type": "string", "pattern": "^HIM[0-9]{4}$"},
|
||||||
|
"severity": {"enum": ["error", "warning"]},
|
||||||
|
"message": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"discovered": {"type": "integer", "minimum": 0},
|
||||||
|
"changed": {"type": "integer", "minimum": 0},
|
||||||
|
"unchanged": {"type": "integer", "minimum": 0},
|
||||||
|
"stale": {"type": "integer", "minimum": 0},
|
||||||
|
"missing": {"type": "integer", "minimum": 0}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://sandwichhime.com/schema/himesan-version-output-v1.schema.json",
|
||||||
|
"title": "Hime-san version JSON result v1",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["compiler", "runtime_abi", "go", "features"],
|
||||||
|
"properties": {
|
||||||
|
"compiler": {"type": "string", "minLength": 1},
|
||||||
|
"runtime_abi": {"const": "sando.v1"},
|
||||||
|
"go": {"type": "string", "minLength": 1},
|
||||||
|
"features": {
|
||||||
|
"type": "array",
|
||||||
|
"prefixItems": [{"const": "lsp-stdio"}],
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
# Benchmark policy
|
# Benchmark policy
|
||||||
|
|
||||||
|
The [RC1 measurement report](BENCHMARK_RESULTS.md) records the observed values,
|
||||||
|
hardware, scope and original samples. Historical RC results must not be relabelled
|
||||||
|
as measurements of a later release candidate.
|
||||||
|
|
||||||
Benchmarks compare equivalent typed views and output against Go's `html/template` baseline. Reports include hardware, operating system, Go version, repository commit, dataset identity, exact commands, warmup/run counts, `ns/op`, bytes and allocations per operation, end-to-end response latency where relevant, output size, and statistical method.
|
Benchmarks compare equivalent typed views and output against Go's `html/template` baseline. Reports include hardware, operating system, Go version, repository commit, dataset identity, exact commands, warmup/run counts, `ns/op`, bytes and allocations per operation, end-to-end response latency where relevant, output size, and statistical method.
|
||||||
|
|
||||||
The v1 gate is no material regression against equivalent repository-owned
|
The v1 gate is no material regression against equivalent repository-owned
|
||||||
@@ -9,6 +13,28 @@ synthetic cases under the published method. Only reproduced improvements become
|
|||||||
marketing claims. Microbenchmarks do not justify claims about request
|
marketing claims. Microbenchmarks do not justify claims about request
|
||||||
throughput, database-heavy pages, or whole-application latency.
|
throughput, database-heavy pages, or whole-application latency.
|
||||||
|
|
||||||
|
The threshold was fixed before measuring the RC. On each maintained native
|
||||||
|
platform and toolchain, ten benchmark samples use the exact output-equivalent
|
||||||
|
`BenchmarkV1Corpus*` pair. Sandwich Hime passes when its median `ns/op` and
|
||||||
|
`B/op` are each no more than 125% of `html/template`, and its median
|
||||||
|
allocations/op are no more than two allocations above `html/template`.
|
||||||
|
Any failed platform/toolchain pair is a material regression. Timing is reviewed
|
||||||
|
from raw samples rather than enforced in ordinary CI, where host contention
|
||||||
|
would turn a performance policy into a flaky correctness gate.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd sando
|
||||||
|
go test -run '^TestBenchmarkCorpusEquivalent$' \
|
||||||
|
-bench '^BenchmarkV1Corpus' -benchmem -benchtime=2s -count=10
|
||||||
|
```
|
||||||
|
|
||||||
|
`benchmarkSandoComponent` intentionally mirrors generated writer calls and
|
||||||
|
captures the same typed view used by the parsed standard template. This is a
|
||||||
|
runtime renderer microbenchmark; it excludes parsing, compiler execution,
|
||||||
|
HTTP, routing, logging, databases, and deployment.
|
||||||
|
|
||||||
Benchmark fixtures must be self-contained, synthetic, reviewable, and committed
|
Benchmark fixtures must be self-contained, synthetic, reviewable, and committed
|
||||||
to this repository. Application-specific datasets and deployment measurements
|
to this repository. Application-specific datasets and deployment measurements
|
||||||
belong with their applications and are neither copied here nor treated as core
|
belong with their applications and are neither copied here nor treated as core
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||||
|
|
||||||
|
# RC1 renderer measurements
|
||||||
|
|
||||||
|
These are historical, maintainer-run measurements of the **RC1 runtime**, not
|
||||||
|
new measurements of a final v1 artifact and not an independent performance audit.
|
||||||
|
They were collected on August 24, 2026 from public commit
|
||||||
|
`e730dd1b56061501881818c7000364a55cd49e35`, tree
|
||||||
|
`931023c7d42591b5280ff81d64ce86d2ebba85b7`.
|
||||||
|
|
||||||
|
## What was measured
|
||||||
|
|
||||||
|
The committed `sando/benchmark_test.go` corpus renders one synthetic typed view
|
||||||
|
to `io.Discard` during timing. `TestBenchmarkCorpusEquivalent` first requires
|
||||||
|
identical buffered output.
|
||||||
|
`BenchmarkV1CorpusSandwichHime` mirrors generated runtime writer calls;
|
||||||
|
`BenchmarkV1CorpusHTMLTemplate` executes a pre-parsed Go standard-library template.
|
||||||
|
Both include the measured render allocations. Template parsing is not charged to
|
||||||
|
the baseline. Compiler execution, HTTP, routing, databases and deployment are
|
||||||
|
outside this measurement.
|
||||||
|
|
||||||
|
Each renderer has ten samples, with two seconds requested per sample. The result
|
||||||
|
is the median of the ten `ns/op` values, not the fastest sample. There was no
|
||||||
|
separately recorded application warmup phase; Go's benchmark harness performs
|
||||||
|
its normal iteration calibration. See the [predeclared policy](BENCHMARKS.md).
|
||||||
|
|
||||||
|
| Host | Go | Hime median ns/op | html/template median ns/op | Hime/baseline time |
|
||||||
|
| --- | --- | ---: | ---: | ---: |
|
||||||
|
| Apple M1, Darwin/arm64, 8 logical CPUs | 1.26.7 | 1,161.0 | 4,958.0 | 23.4% |
|
||||||
|
| Apple M1, Darwin/arm64, 8 logical CPUs | 1.27.0 | 1,149.5 | 4,800.5 | 23.9% |
|
||||||
|
| Xeon E5-2620 v2, Linux/amd64, 4-CPU container limit | 1.26.7 | 4,362.5 | 22,837.5 | 19.1% |
|
||||||
|
| Xeon E5-2620 v2, Linux/amd64, 4-CPU container limit | 1.27.0 | 3,877.5 | 22,003.5 | 17.6% |
|
||||||
|
|
||||||
|
All samples recorded **688 B/op and 21 allocations/op** for Sandwich Hime,
|
||||||
|
versus **1,600 B/op and 58 allocations/op** for `html/template`. Linux ran
|
||||||
|
natively in a read-only, network-disabled,
|
||||||
|
capability-dropped container limited to 4 GiB and 512 processes. macOS ran
|
||||||
|
natively. Exact OS patch/kernel versions were not captured in these benchmark
|
||||||
|
logs; do not infer them from the toolchain versions.
|
||||||
|
|
||||||
|
The baseline took approximately **4.2–5.7 times as long** on these lanes.
|
||||||
|
This does not establish whole-site speed, request throughput, a comparison
|
||||||
|
against every Go template engine, or a universal result on other workloads.
|
||||||
|
The samples are sequential, not a statistical study of concurrent application
|
||||||
|
load. No confidence interval or causal claim about the difference between the
|
||||||
|
two machines is implied. We do not claim to be the fastest engine.
|
||||||
|
|
||||||
|
### September 9 repeat check
|
||||||
|
|
||||||
|
A local repeat on Apple M1, Darwin/arm64, Go 1.27.0 used the same command and
|
||||||
|
runtime source unchanged from the public RC1 commit above. Output equivalence
|
||||||
|
passed. Median times were **1,034.5 ns/op**
|
||||||
|
for Hime and **4,044.0 ns/op** for the baseline (25.6%, approximately 3.9 times
|
||||||
|
as long for the baseline). Allocation counts and bytes remained 21/688 and
|
||||||
|
58/1,600 respectively. This is a local repeat, not a final-release artifact gate.
|
||||||
|
|
||||||
|
- Hime: `1028, 1021, 1028, 1031, 1033, 1097, 1063, 1065, 1041, 1036`
|
||||||
|
- Baseline: `4024, 4218, 4053, 4035, 4156, 4098, 4022, 3993, 4133, 3980`
|
||||||
|
|
||||||
|
The changed timing ratio is a useful reminder to rerun the workload on the
|
||||||
|
deployment hardware rather than treating a historical speedup as a guarantee.
|
||||||
|
|
||||||
|
## Reproduce
|
||||||
|
|
||||||
|
Check out the public commit above, select one of the recorded Go versions and,
|
||||||
|
from its `sando` directory, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
GOWORK=off go test -run '^TestBenchmarkCorpusEquivalent$' \
|
||||||
|
-bench '^BenchmarkV1Corpus' -benchmem -benchtime=2s -count=10
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `GOMAXPROCS=8` for the recorded Darwin lane or `GOMAXPROCS=4` for the Linux
|
||||||
|
lane, and document the actual hardware/resource limits. Compare within a host;
|
||||||
|
do not treat absolute times from different hardware as a controlled comparison.
|
||||||
|
|
||||||
|
## Recorded samples
|
||||||
|
|
||||||
|
Values below are `ns/op` in original sample order. Each vector contains ten
|
||||||
|
measurements. Raw-log digests identify the retained original evidence; these
|
||||||
|
vectors expose the measurements without publishing private runner metadata.
|
||||||
|
|
||||||
|
### Darwin/arm64, Go 1.26.7
|
||||||
|
|
||||||
|
- Hime: `1168, 1163, 1159, 1159, 1164, 1171, 1159, 1160, 1159, 1162`
|
||||||
|
- Baseline: `4771, 4802, 5117, 4846, 4885, 4928, 5017, 4988, 5036, 5105`
|
||||||
|
- Raw-log SHA-256: `7b68a643d685168dbd77cf3df617e8de59aa8063f6728de6c2d664173b0b05bd`
|
||||||
|
|
||||||
|
### Darwin/arm64, Go 1.27.0
|
||||||
|
|
||||||
|
- Hime: `1102, 1107, 1120, 1125, 1144, 1155, 1167, 1169, 1167, 1180`
|
||||||
|
- Baseline: `4686, 4924, 5132, 4750, 4822, 4741, 4791, 4787, 4810, 4822`
|
||||||
|
- Raw-log SHA-256: `b85481317da41bc63ad8afc317da2fd1075184405e7704a69714f42453d7dd4c`
|
||||||
|
|
||||||
|
### Linux/amd64, Go 1.26.7
|
||||||
|
|
||||||
|
- Hime: `4291, 4496, 4112, 4357, 4400, 3984, 4368, 4511, 4145, 4375`
|
||||||
|
- Baseline: `23663, 22300, 22364, 22946, 22863, 22812, 22078, 24476, 23041, 20900`
|
||||||
|
- Raw-log SHA-256: `277eee8f2488b0aa0b752584a46d3737df87defd311fd0bcf722aea81612a960`
|
||||||
|
|
||||||
|
### Linux/amd64, Go 1.27.0
|
||||||
|
|
||||||
|
- Hime: `4314, 3939, 3950, 3861, 3797, 3821, 3756, 3651, 3894, 4170`
|
||||||
|
- Baseline: `21346, 21015, 22272, 22066, 22198, 21059, 22473, 21318, 21941, 22447`
|
||||||
|
- Raw-log SHA-256: `a5d760c0fb76012b5923376dfcd80a1f1f44bf0043145f4352e4ec820aa475b3`
|
||||||
+4
-1
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
# Brand vocabulary
|
# Brand vocabulary
|
||||||
|
|
||||||
Use **Sandwich Hime** for the project and **Hime-san** as its friendly short form. Use `himesan` for the command, `.sando` for source, `.sando.go` for generated Go, and `sando` for the runtime.
|
Use **Sandwich Hime** as the primary project identity and **Hime-san** for its
|
||||||
|
compiler/tool personality within that project, not as a separate framework
|
||||||
|
brand. Use `himesan` for the command, `.sando` for source, `.sando.go` for
|
||||||
|
generated Go, and `sando` for the runtime.
|
||||||
|
|
||||||
Never call the project bare “Hime,” which can be confused with an existing Go framework. Never use `.san`; it belongs exclusively to the separate San language project. “Sando” evokes a sandwich and keeps both projects unmistakable in editors, tooling, and search results.
|
Never call the project bare “Hime,” which can be confused with an existing Go framework. Never use `.san`; it belongs exclusively to the separate San language project. “Sando” evokes a sandwich and keeps both projects unmistakable in editors, tooling, and search results.
|
||||||
|
|
||||||
|
|||||||
+48
-29
@@ -2,23 +2,20 @@
|
|||||||
|
|
||||||
# Compatibility policy
|
# Compatibility policy
|
||||||
|
|
||||||
## Beta contract
|
## v1 release contract
|
||||||
|
|
||||||
Compiler `v1.0.0-beta.2` and runtime `sando/v1.0.0-beta.1` are semantic-version
|
This document defines the compatibility contract for compiler `v1.0.0` and
|
||||||
prereleases. They are supported for learning, classroom projects, evaluation,
|
runtime `sando/v1.0.0`. Canonical Gitea tags and release announcements establish
|
||||||
and compatibility testing. Before final v1, source syntax, generated output,
|
version and artifact availability. Every correction receives a new immutable
|
||||||
the runtime API, CLI behavior, diagnostics, and configuration may change
|
version, documentation, and deterministic generation evidence.
|
||||||
without compatibility shims. Every public change must still be documented and
|
|
||||||
generation must remain deterministic.
|
|
||||||
|
|
||||||
The beta is not a production-stability commitment. Maintainers accept and
|
Maintainers accept and triage security reports within the boundary described in
|
||||||
triage security reports within the boundary described in
|
[SECURITY.md](../SECURITY.md). A security correction may intentionally fail
|
||||||
[SECURITY.md](../SECURITY.md), but cannot promise that a beta fix preserves its
|
closed when retaining behavior would contradict a published safety guarantee.
|
||||||
public API.
|
|
||||||
|
|
||||||
## Final-v1 contract
|
## v1 compatibility contract
|
||||||
|
|
||||||
At final v1, semantic versions apply independently to the compiler and
|
For v1, semantic versions apply independently to the compiler and
|
||||||
`sando` runtime. Generated files record the exact compiler version and
|
`sando` runtime. Generated files record the exact compiler version and
|
||||||
required runtime ABI. Patch releases do not intentionally change accepted
|
required runtime ABI. Patch releases do not intentionally change accepted
|
||||||
source semantics or generated public signatures. Minor releases may add
|
source semantics or generated public signatures. Minor releases may add
|
||||||
@@ -30,13 +27,36 @@ compiler versions; `himesan check` defines whether they are current. The
|
|||||||
project makes no compatibility promise for internal packages, development SSE
|
project makes no compatibility promise for internal packages, development SSE
|
||||||
payloads before final v1, or hand-edited generated files.
|
payloads before final v1, or hand-edited generated files.
|
||||||
|
|
||||||
|
The v1 compatibility snapshots cover the exported `sando` API and values, CLI
|
||||||
|
help and exit-code classes, structured operation/version output, diagnostic
|
||||||
|
codes, `himesan.json`, and generated provenance. English diagnostic wording,
|
||||||
|
internal packages, temporary paths, and compiler implementation details are not
|
||||||
|
stable API.
|
||||||
|
|
||||||
|
An API deprecated during v1 remains available for the rest of the v1
|
||||||
|
major line and may be removed in v2. A security correction may fail closed in
|
||||||
|
a patch release when retaining old behavior would contradict a published safety
|
||||||
|
guarantee; that exception receives an advisory and migration note rather than a
|
||||||
|
silent compatibility claim. Until a broader maintenance policy is announced,
|
||||||
|
only the latest stable v1 patch and the current prerelease receive fixes.
|
||||||
|
|
||||||
## Go and platform support
|
## Go and platform support
|
||||||
|
|
||||||
The current beta targets Go 1.25 and Go 1.26. Support is based on point-in-time,
|
The modules retain a `go 1.25` language directive for consumer compatibility.
|
||||||
maintainer-run release matrices, not an implication of continuous CI coverage.
|
The maintained v1 build and verification targets are Linux/amd64 and Apple
|
||||||
A Go support change is announced in release notes before it takes effect.
|
Silicon macOS/arm64 using the pinned patched Go 1.26.7 and Go 1.27.0 toolchains.
|
||||||
|
Both native targets are release blockers. A sleeping or unavailable Mac delays
|
||||||
|
the release gate rather than silently converting it into Linux or
|
||||||
|
cross-compilation evidence. Native Windows, Intel macOS, Linux/arm64, and other
|
||||||
|
targets may work but are not v1 compatibility promises. A Go or platform
|
||||||
|
support change is announced in release notes before it takes effect.
|
||||||
|
|
||||||
The current public evidence is the exact Beta 1 source at commit
|
### Historical Beta 1 observations
|
||||||
|
|
||||||
|
The following table is retained because the tests genuinely ran. It records a
|
||||||
|
point-in-time Beta 1 campaign and does not define the current support matrix.
|
||||||
|
|
||||||
|
The historical Beta 1 evidence is bound to source commit
|
||||||
`b7a84054d755e42285e50298e41e47f06a8325a5` (tree
|
`b7a84054d755e42285e50298e41e47f06a8325a5` (tree
|
||||||
`be9e118e38dfebed19f60403ededdadabe07d2aa`):
|
`be9e118e38dfebed19f60403ededdadabe07d2aa`):
|
||||||
|
|
||||||
@@ -45,26 +65,25 @@ The current public evidence is the exact Beta 1 source at commit
|
|||||||
| 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 | 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 |
|
| 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 | — | Not executed during the Beta 1 campaign |
|
||||||
|
|
||||||
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 in that historical campaign.
|
||||||
|
|
||||||
The signed Beta tags and fresh direct/public-proxy installation were verified
|
The signed Beta tags and fresh direct/public-proxy installation were verified
|
||||||
after publication. For Beta 1, add the nested runtime to an application module
|
after publication. For Beta 1, add the nested runtime to an application module
|
||||||
before installing the parent compiler at the same version; this avoids a Go
|
before installing the parent compiler at the same version; this avoids a Go
|
||||||
module-cache path-selection ambiguity observed in the reverse order.
|
module-cache path-selection ambiguity observed in the reverse order.
|
||||||
|
|
||||||
## macOS feedback
|
## Portability feedback
|
||||||
|
|
||||||
Mac learners, teachers, and Go developers are warmly invited to try the beta.
|
Developers may try Hime-san on an unsupported target and report useful gaps. A
|
||||||
A useful compatibility report includes the macOS version, Intel or Apple
|
good report includes the operating system and architecture, `go version`, the
|
||||||
Silicon architecture, `go version`, the exact command, and a minimal
|
exact command, and a minimal reproduction or diagnostic output. Ordinary
|
||||||
reproduction or diagnostic output. Ordinary compatibility reports belong on
|
portability reports belong on the canonical Gitea project. Suspected
|
||||||
the canonical Gitea project. Suspected vulnerabilities must use the private
|
vulnerabilities must use the private route in [SECURITY.md](../SECURITY.md).
|
||||||
route in [SECURITY.md](../SECURITY.md).
|
|
||||||
|
|
||||||
Community reports can reveal gaps and help prioritize maintainer testing. They
|
Community reports can reveal gaps and help prioritize future work. They do not
|
||||||
do not constitute an independent audit or shift responsibility for security
|
constitute an independent audit, create a support promise, or shift
|
||||||
review, triage, fixes, and release decisions to the community.
|
responsibility for security review, triage, fixes, and release decisions.
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ The application must read its listen address from the configured environment var
|
|||||||
|
|
||||||
## `himesan.json` schema version 1
|
## `himesan.json` schema version 1
|
||||||
|
|
||||||
|
The machine-readable contract is
|
||||||
|
[`contracts/himesan-config-v1.schema.json`](../contracts/himesan-config-v1.schema.json).
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
|
|||||||
@@ -21,3 +21,27 @@ Code families are intentionally coarse compatibility surfaces:
|
|||||||
| `HIM29xx` | Boundary warnings |
|
| `HIM29xx` | Boundary warnings |
|
||||||
|
|
||||||
Scripts should consume the JSON `code`, `severity`, and location fields, not parse English messages. Message wording may improve within a compatible release.
|
Scripts should consume the JSON `code`, `severity`, and location fields, not parse English messages. Message wording may improve within a compatible release.
|
||||||
|
|
||||||
|
The exact v1 code inventory is machine-checked against
|
||||||
|
[`contracts/diagnostic-codes-v1.txt`](../contracts/diagnostic-codes-v1.txt).
|
||||||
|
Adding, removing, or renumbering a code requires an explicit compatibility
|
||||||
|
review and snapshot update.
|
||||||
|
|
||||||
|
## CLI and structured-output contract
|
||||||
|
|
||||||
|
Command exit codes use three classes: `0` for success (including help and
|
||||||
|
warning-only results), `1` for a completed operation that failed validation or
|
||||||
|
runtime service, and `2` for invalid command usage or failure to encode the
|
||||||
|
requested CLI result. `gen` normalizes to `generate`; `bless` remains a named
|
||||||
|
read-only alias of `check` in structured output.
|
||||||
|
|
||||||
|
The v1 JSON shapes are published as closed schemas:
|
||||||
|
|
||||||
|
- [`himesan-operation-output-v1.schema.json`](../contracts/himesan-operation-output-v1.schema.json)
|
||||||
|
for `generate`, `check`, and `bless`;
|
||||||
|
- [`himesan-version-output-v1.schema.json`](../contracts/himesan-version-output-v1.schema.json)
|
||||||
|
for `version --json`.
|
||||||
|
|
||||||
|
Unknown output fields are not introduced in a compatible v1 patch without an
|
||||||
|
explicit schema/version decision. Consumers should still ignore English
|
||||||
|
message wording.
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||||
|
|
||||||
|
# Shared tools, commercial freedom
|
||||||
|
|
||||||
|
The design principle is **earn from useful work; preserve the shared freedoms
|
||||||
|
that made the work possible**. Commercial users are welcome. Sponsorship is
|
||||||
|
appreciated, not a license fee or a claim to project control. The intended users
|
||||||
|
are software developers and maintainers; v1 is a maintained compatibility
|
||||||
|
commitment, not a declaration that development has ended.
|
||||||
|
|
||||||
|
This document explains the [contribution agreement](../CLA.md) and official governance.
|
||||||
|
It is not a new software license, a downstream contract or a claim that every
|
||||||
|
future use or derivative of every component must be open source.
|
||||||
|
|
||||||
|
## Four different jobs
|
||||||
|
|
||||||
|
- The software license supplies recipients' permissions and obligations.
|
||||||
|
- The CLA records the rights for submitted contributions and reciprocal
|
||||||
|
promises by the named official steward; it is not a contract with every user.
|
||||||
|
- Governance determines official decisions, repository access and succession.
|
||||||
|
- The trademark policy addresses origin and endorsement, not ownership of
|
||||||
|
ideas or a veto on honest independent development.
|
||||||
|
|
||||||
|
Open-source commercial freedoms include competition. A subjective ban on
|
||||||
|
"greed," commercial hosting or businesses the maintainer dislikes would not
|
||||||
|
preserve those freedoms. We instead use concrete source-sharing, attribution,
|
||||||
|
no-hidden-relicensing and stewardship commitments. See the
|
||||||
|
[Open Source Definition](https://opensource.org/osd).
|
||||||
|
|
||||||
|
## The deliberate compiler/application boundary
|
||||||
|
|
||||||
|
The current [license map](../LICENSES.md) remains unchanged:
|
||||||
|
|
||||||
|
- Compiler/project material is AGPL-3.0-only. Distribution and covered modified
|
||||||
|
network use have source obligations under that license.
|
||||||
|
- The `sando` runtime is Apache-2.0. That is permissive, not network copyleft;
|
||||||
|
compliant closed-source derivatives are possible.
|
||||||
|
- User templates and generated applications may use their authors' chosen
|
||||||
|
terms, subject to input/dependency rights and the explicit output permission.
|
||||||
|
A proprietary paid website built with Hime is deliberately possible.
|
||||||
|
|
||||||
|
This protects the compiler's shared code without requiring its developers'
|
||||||
|
applications to become AGPL. Private production adoption is intentional: the
|
||||||
|
maintainer has confirmed this split for the release. The runtime's rendering,
|
||||||
|
escaping and URL-safety helpers are valuable implementation work; permissive
|
||||||
|
embedding makes them useful without imposing compiler licensing on applications.
|
||||||
|
It does not deliver "every part always open." Neither a later CLA nor a changed
|
||||||
|
README retracts existing recipients' licenses. No license conversion is proposed.
|
||||||
|
|
||||||
|
## Concrete cases
|
||||||
|
|
||||||
|
- A consultant sells a website built with Hime: allowed under the existing
|
||||||
|
application boundary; no mandatory payment, public badge or CLA for the client.
|
||||||
|
- A business distributes a modified compiler: it must follow the AGPL's
|
||||||
|
applicable source, licensing and notice requirements.
|
||||||
|
- A business operates a modified AGPL compiler with remote user interaction:
|
||||||
|
section 13 requires an offer of that version's Corresponding Source to those
|
||||||
|
interacting users. This is not necessarily every unrelated part of its service.
|
||||||
|
- A business merely hosts an unmodified program: hosting alone does not meet
|
||||||
|
section 13's modification condition. Wrappers and combined works require
|
||||||
|
fact-specific analysis, not an assumed universal SaaS prohibition.
|
||||||
|
- A company publishes a complying, independently branded fork: permitted,
|
||||||
|
even if it competes successfully and submits no changes upstream.
|
||||||
|
- A sponsor wants official control or private permission to close contributor
|
||||||
|
AGPL work: sponsorship grants neither; the steward covenant rejects
|
||||||
|
that official relicensing route. A fork cannot claim official endorsement.
|
||||||
|
|
||||||
|
These examples summarize boundaries, not legal opinions on a specific service.
|
||||||
|
The [AGPL text](https://opensource.org/license/agpl-3.0), including sections 2,
|
||||||
|
7, 10 and 13, controls actual covered uses. The
|
||||||
|
[Apache license](https://www.apache.org/licenses/LICENSE-2.0) controls the runtime.
|
||||||
|
The AGPL permits removing additional permissions from a redistributed copy;
|
||||||
|
do not promise that every independent fork must keep our output exception.
|
||||||
|
|
||||||
|
## What the contribution agreement adds
|
||||||
|
|
||||||
|
An identified contributor keeps ownership and grants only the recorded public
|
||||||
|
license plus an expressly authorized output permission. No alternate broad
|
||||||
|
sublicensing grant, copyright assignment, forced upstream labor, contributor
|
||||||
|
indemnity or proprietary buyout permission is collected. Patent rights and
|
||||||
|
termination follow the relevant established license.
|
||||||
|
|
||||||
|
The named Steward promises accurate attribution, preservation of contribution
|
||||||
|
records, public licensing of accepted material in official releases, no official
|
||||||
|
side deal to remove contributor copyleft, and written assumption of those duties
|
||||||
|
before a voluntary transfer of official stewardship. Those contractual promises
|
||||||
|
are not appended to downstream AGPL licenses. They cannot guarantee that a
|
||||||
|
project will never be abandoned, that a competitor will never outperform it,
|
||||||
|
or that every later actor is bound without agreement. Copyright enforcement
|
||||||
|
for contributor-owned work may require separate cooperation or authority.
|
||||||
|
|
||||||
|
Contributors remain free to license their own work elsewhere. This is reciprocal
|
||||||
|
stewardship rather than acquiring all rights from contributors. Existing public
|
||||||
|
licenses, usable source and the ability to fork preserve continuity if official
|
||||||
|
stewardship fails; governance and operational recovery still require real people.
|
||||||
|
|
||||||
|
## Deliberate adoption, without a barrier to learning
|
||||||
|
|
||||||
|
The AGPL compiler, Apache runtime and chosen application-license boundary is
|
||||||
|
settled. Version 1.0 of the contribution agreement is prospective: explicit
|
||||||
|
contributor and Steward acceptance is necessary, and no earlier contribution
|
||||||
|
is silently covered. The small recordkeeping process is in its section 9.
|
||||||
|
Nobody signs it merely to learn, download or build a private paid application.
|
||||||
|
|
||||||
|
The maintainer chose to proceed with this reviewed project-specific wording
|
||||||
|
without making outside legal review a release prerequisite. It is not an
|
||||||
|
ASF-approved agreement, a claim that counsel reviewed it, or a guaranteed
|
||||||
|
takeover shield. The [ASF ICLA](https://www.apache.org/licenses/icla.pdf) is a
|
||||||
|
useful comparison for contribution scope, authority and explicit acceptance;
|
||||||
|
its broader licensing grant is not imported into this agreement. Specific
|
||||||
|
future disputes or changes in jurisdiction may warrant professional advice.
|
||||||
|
|
||||||
|
The principle may inform other Gamertan projects, but each license, dependency
|
||||||
|
and contributor history needs its own decision. This document does not change
|
||||||
|
other repositories or turn "Canadian license" into a new license family.
|
||||||
+99
-22
@@ -23,8 +23,46 @@ The named Windows and WSL2 platform runs used the exact public commit and tree
|
|||||||
above. The isolated server-container matrix preceded the final candidate and
|
above. The isolated server-container matrix preceded the final candidate and
|
||||||
is retained only as supplementary Linux evidence. Hostnames, network addresses,
|
is retained only as supplementary Linux evidence. Hostnames, network addresses,
|
||||||
account names, private paths, private repository identities, and private commit
|
account names, private paths, private repository identities, and private commit
|
||||||
mappings are intentionally absent from this public ledger. Native macOS
|
mappings are intentionally absent from this public ledger. These platform
|
||||||
execution remains pending and is provisional for the beta.
|
observations are historical evidence, not the current support matrix.
|
||||||
|
Linux/amd64 and Darwin/arm64 are now the maintained v1 release targets. This
|
||||||
|
section retains historical Beta 1 evidence; the exact RC must supply new native
|
||||||
|
evidence on both targets. WSL and native Windows are not v1 release blockers.
|
||||||
|
|
||||||
|
## Beta 2 compiler publication addendum
|
||||||
|
|
||||||
|
This addendum records the additive language-server release without replacing
|
||||||
|
the Beta 1 assessment identity above. Compiler tag `v1.0.0-beta.2` is a signed
|
||||||
|
annotated tag whose peeled public commit is
|
||||||
|
`1082d9d61eb84e67ca4012ff9ee3898ee37ac6fd` and whose public tree is
|
||||||
|
`01d5702928f3d9c9fb0e3d2213530add7ff94745`. The tag object is
|
||||||
|
`f091cd67f688ba5ee784b18f5a407a9326df7ab2`.
|
||||||
|
|
||||||
|
Beta 2 changed only the development compiler. No `sando/v1.0.0-beta.2` tag was
|
||||||
|
created. The release preflight compared the retained runtime tag
|
||||||
|
`sando/v1.0.0-beta.1` with the Beta 2 commit and verified that their `sando`
|
||||||
|
subtrees were byte-identical at tree
|
||||||
|
`3035e948f77f160d399089be3ae80c88bab3fed2`.
|
||||||
|
|
||||||
|
The exact public Beta 2 source passed the full race-enabled verifier and the
|
||||||
|
compiler-only release preflight on executed Linux with Go 1.26.5. Fresh native
|
||||||
|
Windows checkouts on NTFS passed the full race-enabled PowerShell verifier,
|
||||||
|
focused process-tree/watcher/consumer tests, candidate-stamped version checks,
|
||||||
|
and deterministic generation on Go 1.25.12 and Go 1.26.5. Clean isolated
|
||||||
|
`GOPROXY=direct` and public-proxy-only installs produced
|
||||||
|
`features:["lsp-stdio"]`; the public-proxy path also verified the retained
|
||||||
|
runtime through `sum.golang.org`. The Windows result is retained as historical
|
||||||
|
portability evidence and does not create an ongoing support promise.
|
||||||
|
|
||||||
|
The Beta 2 language server is additive development tooling. Its tested
|
||||||
|
security boundary includes protocol-only stdout; bounded header and message
|
||||||
|
framing; integer/string JSON-RPC identifiers; full-document in-memory overlays;
|
||||||
|
UTF-16 conversion at the protocol boundary; cancellation and shutdown;
|
||||||
|
workspace, nested-module, VCS, symlink, and file-count boundaries; and explicit
|
||||||
|
no-write/no-network/no-Go-tool execution tests. Fuzz targets exercise bounded
|
||||||
|
JSON-RPC framing and document changes. These checks do not make an untrusted
|
||||||
|
workspace safe to execute: `himesan dev` and project commands remain trusted
|
||||||
|
local-code operations, while `himesan lsp --stdio` performs analysis only.
|
||||||
|
|
||||||
## Observed security self-assessment evidence
|
## Observed security self-assessment evidence
|
||||||
|
|
||||||
@@ -45,11 +83,11 @@ baseline commit.
|
|||||||
| Parser robustness smoke | Two bounded Go fuzz targets | Pass; no panic found |
|
| Parser robustness smoke | Two bounded Go fuzz targets | Pass; no panic found |
|
||||||
| Deterministic generation | repeated generate/check/hash/mtime gates | Pass |
|
| Deterministic generation | repeated generate/check/hash/mtime gates | Pass |
|
||||||
| Writer failures | runtime error/short-write/nil-writer tests | Pass |
|
| Writer failures | runtime error/short-write/nil-writer tests | Pass |
|
||||||
| HTML text/attribute/RCDATA escaping | compiler and runtime adversarial cases | Pass for enumerated cases |
|
| HTML text/attribute/RCDATA escaping | compiler/runtime adversarial cases plus the committed `html/template` overlap corpus | Pass for the committed corpus; documented stricter invalid-UTF-8 handling remains intentional |
|
||||||
| URL scheme handling | ordinary/trusted URL test matrix | Pass for enumerated cases |
|
| URL scheme handling | ordinary/trusted URL matrices plus safe, unsafe, and intentionally divergent `html/template` cases | Pass for the committed corpus; control rejection and the explicit `tel` allowlist are documented policy differences |
|
||||||
| 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 | Exact-candidate native Windows and executed Linux matrices; macOS cross-compilation | Windows/Linux pass for tested lanes; native macOS pending |
|
| Platform behavior | Historical exact-candidate native Windows and executed Linux matrices | Windows/Linux passed for the tested lanes; the v1 RC requires fresh Linux/amd64 and Darwin/arm64 evidence |
|
||||||
|
|
||||||
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.
|
||||||
@@ -58,7 +96,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.
|
||||||
|
|
||||||
## Beta 1 native compatibility matrix
|
## Historical Beta 1 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.
|
||||||
@@ -68,7 +106,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 | 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 |
|
| 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 | No native Beta 1 evidence; Darwin/arm64 becomes a maintained target at the v1 RC and requires fresh evidence |
|
||||||
|
|
||||||
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
|
||||||
`63fa75a3049a3a8a12d769d7f9b6b510dfe763baacf706775b75cef2c57a984f`
|
`63fa75a3049a3a8a12d769d7f9b6b510dfe763baacf706775b75cef2c57a984f`
|
||||||
@@ -76,12 +114,12 @@ on every tested Windows and Linux lane. Repeated generation also preserved its
|
|||||||
timestamp. This demonstrates cross-host agreement for one compiler-owned
|
timestamp. This demonstrates cross-host agreement for one compiler-owned
|
||||||
fixture, not equivalence for every possible template.
|
fixture, not equivalence for every possible template.
|
||||||
|
|
||||||
Mac learners and Go developers are warmly invited to report ordinary
|
Portability reports for unsupported targets may include the operating system,
|
||||||
compatibility results with macOS version, architecture, `go version`, exact
|
architecture, `go version`, exact command, and a minimal reproduction.
|
||||||
command, and a minimal reproduction. Suspected vulnerabilities use the private
|
Suspected vulnerabilities use the private route in
|
||||||
route in [SECURITY.md](../SECURITY.md). Community reports help find gaps;
|
[SECURITY.md](../SECURITY.md). Such reports help find gaps but do not create a
|
||||||
maintainers remain responsible for reproducing security-relevant behavior,
|
support promise; maintainers remain responsible for security triage and fixes
|
||||||
triage, remediation, and release decisions.
|
on both supported native targets.
|
||||||
|
|
||||||
## Security-relevant design evidence
|
## Security-relevant design evidence
|
||||||
|
|
||||||
@@ -146,8 +184,13 @@ go test ./internal/compiler -run '^$' \
|
|||||||
./scripts/verify-public-install.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
|
Those historical Beta 1 fuzz targets asserted process robustness and result
|
||||||
not yet prove semantic HTML safety.
|
bounds; they did not prove semantic HTML safety. The v1 compiler target now
|
||||||
|
also asserts deterministic diagnostics and generated Go, valid formatted Go,
|
||||||
|
source-digest binding, bounded public diagnostic shape, and sanitized source
|
||||||
|
map directives. A separate runtime target asserts deterministic, fail-closed
|
||||||
|
URL handling with no partial output. These properties still do not replace the
|
||||||
|
committed differential corpus or real-browser testing.
|
||||||
|
|
||||||
## Assessment findings and remediation status
|
## Assessment findings and remediation status
|
||||||
|
|
||||||
@@ -169,10 +212,17 @@ known-vulnerability scans, candidate-version provenance checks, native Windows
|
|||||||
and executed Linux matrices, and Windows/macOS cross-compilation on 2026-08-12.
|
and executed Linux matrices, and Windows/macOS cross-compilation on 2026-08-12.
|
||||||
Signed annotated runtime and compiler tags were then published from that commit
|
Signed annotated runtime and compiler tags were then published from that commit
|
||||||
in that order. Fresh runtime-first installation passed through both direct Git
|
in that order. Fresh runtime-first installation passed through both direct Git
|
||||||
resolution and the public Go proxy after normal proxy propagation. Native
|
resolution and the public Go proxy after normal proxy propagation. Future
|
||||||
macOS and the other gaps below remain separate release decisions.
|
release decisions require fresh evidence for the maintained Linux/amd64 and
|
||||||
|
Darwin/arm64 targets rather than reusing this historical campaign.
|
||||||
|
|
||||||
## Open assurance gaps
|
## Historical pre-RC assurance gaps
|
||||||
|
|
||||||
|
The list below preserves the pre-RC assessment, not current completion status.
|
||||||
|
RC.1 native verification and artifact publication are recorded in
|
||||||
|
`../CHANGELOG.md`; the final-release queue is `../ROADMAP.md`. The v1
|
||||||
|
dispositions below distinguish required exact-candidate evidence from explicitly
|
||||||
|
accepted recovery deferrals.
|
||||||
|
|
||||||
- delivery to `security@sandwichhime.com` is owner-confirmed through a
|
- delivery to `security@sandwichhime.com` is owner-confirmed through a
|
||||||
controlled domain catch-all; encrypted reporting, documented backup, and
|
controlled domain catch-all; encrypted reporting, documented backup, and
|
||||||
@@ -180,10 +230,10 @@ macOS and the other gaps below remain separate release decisions.
|
|||||||
- the signed annotated Beta tags and their common peeled commit were verified;
|
- the signed annotated Beta tags and their common peeled commit were verified;
|
||||||
prebuilt-artifact signing, checksums, SBOM, reproducible provenance, and key
|
prebuilt-artifact signing, checksums, SBOM, reproducible provenance, and key
|
||||||
recovery remain incomplete;
|
recovery remain incomplete;
|
||||||
- native macOS, Linux/arm64, and Windows/arm64 execution remain outstanding;
|
- Linux/arm64, Darwin/amd64, Windows, and other targets are outside the current
|
||||||
- Windows symlink rejection was not natively exercised because the test account
|
maintained release set;
|
||||||
lacked symlink privilege;
|
- the exact public candidate still needs the committed real-browser generated
|
||||||
- browser-parser differential and semantic property testing need expansion;
|
document and development-supervisor campaign on both maintained hosts;
|
||||||
- compiler input size, CPU, and memory have no built-in hard budget;
|
- compiler input size, CPU, and memory have no built-in hard budget;
|
||||||
- filesystem checks do not defend against a hostile local actor racing path
|
- filesystem checks do not defend against a hostile local actor racing path
|
||||||
components between inspection and use;
|
components between inspection and use;
|
||||||
@@ -197,6 +247,33 @@ macOS and the other gaps below remain separate release decisions.
|
|||||||
- static cycle detection and trust-use warnings are best-effort analyses; and
|
- static cycle detection and trust-use warnings are best-effort analyses; and
|
||||||
- the project has no independent security audit or bug-bounty program.
|
- the project has no independent security audit or bug-bounty program.
|
||||||
|
|
||||||
|
## v1 disposition of open gaps
|
||||||
|
|
||||||
|
The list above intentionally mixes incomplete release evidence with boundaries
|
||||||
|
that are not promised by this product. The RC may not convert either category
|
||||||
|
into vague assurance. The following disposition is explicit and remains
|
||||||
|
subject to exact-public-candidate review:
|
||||||
|
|
||||||
|
| Gap | v1 disposition |
|
||||||
|
| --- | --- |
|
||||||
|
| Security mailbox delivery, backup, and recovery | Delivery/reply remains required and is maintainer-confirmed. Offline restoration and independent second-person recovery/verification are explicitly deferred assurance work for v1.0.0, not completed drills. Encrypted reporting may remain optional if the supported confidential channel and its limit are stated accurately. |
|
||||||
|
| Artifact signing, provenance, and key recovery | Deterministic native artifacts, Developer ID notarization and signed-tag evidence remain required for the exact release candidate. Offline signing-key restoration and independent second-person recovery/verification are explicitly deferred for v1.0.0; successful signing does not prove recovery. |
|
||||||
|
| Maintained native matrix | Release blocker for Linux/amd64 and Darwin/arm64 only. Other architectures and operating systems are explicitly unsupported, not silently untested promises. |
|
||||||
|
| Real-browser parser and supervisor evidence | Release blocker. The repository-owned gate covers a generated typed document, parsed structure, hostile-value inertness, and supervisor behavior. Execute it against the exact public candidate on both maintained hosts before publication. |
|
||||||
|
| Compiler resource budgets | Accepted v1 boundary. The compiler is a trusted local build tool; operating-system and runner limits own CPU, memory, and input quotas. No hostile-input resource guarantee is made. |
|
||||||
|
| Hostile local filesystem races | Accepted v1 boundary. Symlinks and ownership are checked, but an actor able to mutate the workspace concurrently is outside the trust model. |
|
||||||
|
| Watcher integrity | Accepted v1 boundary. Watching is development convenience; explicit `check`, generation, Go tests, and builds remain release/deployment authority. |
|
||||||
|
| Human-readable child diagnostics | Accepted v1 boundary. They are bounded for resources but remain trusted local terminal output, not a sanitized telemetry format. |
|
||||||
|
| Development CSP rewriting | Accepted v1 boundary. It enables reload on trusted loopback pages and is not production CSP validation. |
|
||||||
|
| Deliberately detached descendants | Accepted v1 boundary. Ordinary process groups are terminated and waited for; adversarial detachment is outside the trusted-project development model. |
|
||||||
|
| Render recursion, output, panic, allocation, CPU, and deadlines | Accepted v1 boundary. Components are ordinary trusted Go; applications own recovery, deadlines, and resource policy. |
|
||||||
|
| Static cycles and trust warnings | Accepted v1 boundary. They are documented best-effort audit hints and never replace Go review/tests or explicit trust decisions. |
|
||||||
|
| Independent audit and bug bounty | Accepted disclosure, not a security claim. Neither exists for RC. Public tests, threat model, reporting, and correction policy must not be described as an independent audit. |
|
||||||
|
|
||||||
|
An accepted boundary is permitted only because matching compatibility, threat
|
||||||
|
model, and release copy already avoid the stronger promise. Any conflicting
|
||||||
|
marketing or documentation reopens the item as a release blocker.
|
||||||
|
|
||||||
## Interpreting this ledger
|
## Interpreting this ledger
|
||||||
|
|
||||||
“Pass” means the named command or case produced its expected result in the named
|
“Pass” means the named command or case produced its expected result in the named
|
||||||
|
|||||||
@@ -177,7 +177,8 @@ Sandwich Hime does not:
|
|||||||
## Open release work
|
## Open release work
|
||||||
|
|
||||||
- broaden semantic and browser-parser differential testing;
|
- broaden semantic and browser-parser differential testing;
|
||||||
- execute the native Windows/macOS security and process-lifecycle matrix;
|
- execute the Linux/amd64 and Darwin/arm64 security and process-lifecycle
|
||||||
|
release matrices;
|
||||||
- complete signed release provenance, checksums, and SBOM evidence;
|
- complete signed release provenance, checksums, and SBOM evidence;
|
||||||
- test the confidential reporting and signing-key recovery procedures; and
|
- test the confidential reporting and signing-key recovery procedures; and
|
||||||
- close or explicitly accept every finding listed in the evidence ledger before
|
- close or explicitly accept every finding listed in the evidence ledger before
|
||||||
|
|||||||
+81
-55
@@ -16,29 +16,40 @@ releases originate only from the reviewed canonical public tree.
|
|||||||
| Surface | Purpose | History and tags |
|
| Surface | Purpose | History and tags |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Private development storage | Working branches, private review records, and historical context | identities and history are not exported; no public release tags |
|
| Private development storage | Working branches, private review records, and historical context | identities and history are not exported; no public release tags |
|
||||||
| Public Gitea `sandwich-hime` | Canonical sanitized source, contribution venue, module origin, and releases | fresh reviewed history; authoritative immutable `sando/vX.Y.Z` and `vX.Y.Z` tags |
|
| Public Gitea `sandwich-hime` | Canonical sanitized source, contribution venue, module origin, and releases | stable reviewed public history; authoritative immutable `sando/vX.Y.Z` and `vX.Y.Z` tags |
|
||||||
| GitHub `gamertan/sandwich-hime` | Discoverability and a convenient sanitized source snapshot | no private refs, force-mirrors, workflows, contribution authority, release artifacts, or semver tags |
|
| GitHub `gamertan/sandwich-hime` | Discoverability and a convenient sanitized source snapshot | no private refs, force-mirrors, workflows, contribution authority, release artifacts, or semver tags |
|
||||||
|
|
||||||
Each public update is exported through the exact committed allowlist, inspected,
|
Each public update is exported through the exact committed allowlist, inspected,
|
||||||
committed as a fresh public snapshot, and compared byte-for-byte with the
|
appended as a meaningful publication commit to the existing public `main`, and
|
||||||
reviewed export. GitHub receives that public tree only. It never receives
|
compared byte-for-byte with the reviewed export. Development commits may be
|
||||||
private history or an indiscriminate Git mirror.
|
squashed for publication; do not reset public ancestry for each version, merge
|
||||||
|
private history into public branches, or force-push existing releases. Versions
|
||||||
|
are immutable tags on that public history, not separate orphan branches.
|
||||||
|
Preserve contributor attribution, sign-offs and any output-permission grants
|
||||||
|
as required by [CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||||
|
|
||||||
|
GitHub receives the reviewed public tree only. It never receives private
|
||||||
|
branches, private notes or an indiscriminate Git mirror. Branch names and
|
||||||
|
protected branches are not privacy boundaries: private development remains in
|
||||||
|
a separate access-controlled repository. The tutorial, tooling and website
|
||||||
|
remain independent projects, not archival branches of the compiler.
|
||||||
|
|
||||||
## Current readiness
|
## Current readiness
|
||||||
|
|
||||||
Beta 1 is deliberately earlier than a release candidate. It creates a real,
|
Beta 1 is deliberately earlier than a release candidate. It creates a real,
|
||||||
repeatable install for learners and evaluators without claiming that the final
|
repeatable install for learners and evaluators without claiming that the final
|
||||||
v1 compatibility, native-platform, artifact, signing, or soak gates are
|
v1 compatibility, native release, artifact, signing, or soak gates are complete.
|
||||||
complete.
|
|
||||||
|
|
||||||
### Demonstrated for Beta 1
|
### Demonstrated for Beta 1
|
||||||
|
|
||||||
Public commit `b7a84054d755e42285e50298e41e47f06a8325a5` (tree
|
Public commit `b7a84054d755e42285e50298e41e47f06a8325a5` (tree
|
||||||
`be9e118e38dfebed19f60403ededdadabe07d2aa`) 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 and Linux/amd64 under
|
||||||
with the earlier pre-beta server-container run retained only as supplementary
|
WSL2, with the earlier pre-beta server-container run retained only as
|
||||||
Linux evidence. The same generated golden SHA-256 was observed across the exact
|
supplementary Linux evidence. The same generated golden SHA-256 was observed
|
||||||
Beta Windows and Linux lanes.
|
across the exact Beta Windows and Linux lanes. This is historical evidence,
|
||||||
|
not the current support definition; Linux/amd64 and Darwin/arm64 are the
|
||||||
|
maintained v1 release targets.
|
||||||
|
|
||||||
Other demonstrated controls include:
|
Other demonstrated controls include:
|
||||||
|
|
||||||
@@ -50,17 +61,25 @@ Other demonstrated controls include:
|
|||||||
- loopback-only, browser-origin-hardened development proxy behavior; and
|
- loopback-only, browser-origin-hardened development proxy behavior; and
|
||||||
- a public threat model, security policy, and dated evidence ledger.
|
- a public threat model, security policy, and dated evidence ledger.
|
||||||
|
|
||||||
### Not demonstrated yet
|
### Demonstrated for the first RC candidate
|
||||||
|
|
||||||
- native maintainer-run macOS execution; macOS is provisional for Beta 1;
|
- native Linux/amd64 and Darwin/arm64 release-candidate lanes on pinned Go
|
||||||
- stable final-v1 API, CLI, schema, diagnostic, and generated snapshots;
|
1.26.7 and Go 1.27.0 toolchains;
|
||||||
- systematic browser-parser and `html/template` differential testing;
|
- exact-public-candidate execution of the committed real-browser generated
|
||||||
|
document and development-supervisor gates;
|
||||||
- a long semantic fuzz campaign beyond bounded no-panic smoke;
|
- a long semantic fuzz campaign beyond bounded no-panic smoke;
|
||||||
- committed comparative benchmarks and predefined regression thresholds;
|
- exact-candidate comparative benchmark results;
|
||||||
- complete real-browser development-supervisor evidence;
|
- deterministic prebuilt archives, checksums, SPDX SBOMs, and provenance;
|
||||||
- deterministic prebuilt archives, checksums, SBOMs, signed binaries, and
|
- Developer ID signing, notarization, stapling, and Gatekeeper validation for
|
||||||
tested signing/recovery procedures; or
|
the Darwin/arm64 distribution; and
|
||||||
- native macOS installation of the published Beta 1 tags.
|
- byte-identical generated output and independently reproduced release
|
||||||
|
archives across the maintained native evidence lanes.
|
||||||
|
|
||||||
|
The final immutable tags, clean direct/proxy installation and public observation
|
||||||
|
review remain publication or post-publication gates rather than properties
|
||||||
|
that source code can prove by itself. Offline restoration and second-person
|
||||||
|
recovery/verification are explicitly deferred assurance work for v1.0.0, as
|
||||||
|
recorded in [ROADMAP.md](../ROADMAP.md).
|
||||||
|
|
||||||
## Beta 1 publication lane
|
## Beta 1 publication lane
|
||||||
|
|
||||||
@@ -68,9 +87,8 @@ Beta 1 is supported for learning, classroom projects, evaluation, prototypes,
|
|||||||
and compatibility feedback. It is not recommended as a production-stable
|
and compatibility feedback. It is not recommended as a production-stable
|
||||||
dependency, and its interfaces may change.
|
dependency, and its interfaces may change.
|
||||||
|
|
||||||
- [x] Define beta support, security, compatibility, and macOS-provisional
|
- [x] Define beta support, security, and compatibility language.
|
||||||
language.
|
- [x] Establish the historical public pre-beta Linux/Windows evidence baseline.
|
||||||
- [x] Establish the named public pre-beta Linux/Windows baseline.
|
|
||||||
- [x] 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.
|
||||||
- [x] Run the candidate-version freshness, bounded fuzz, vulnerability, and
|
- [x] Run the candidate-version freshness, bounded fuzz, vulnerability, and
|
||||||
@@ -79,64 +97,68 @@ dependency, and its interfaces may change.
|
|||||||
`v1.0.0-beta.1`, from the same reviewed public commit.
|
`v1.0.0-beta.1`, from the same reviewed public commit.
|
||||||
- [x] Verify clean runtime-first direct and public-proxy installs and record the
|
- [x] Verify clean runtime-first direct and public-proxy installs and record the
|
||||||
result.
|
result.
|
||||||
- [ ] Add native macOS maintainer evidence before RC; community reports inform
|
|
||||||
that work but do not replace maintainer responsibility.
|
|
||||||
|
|
||||||
## Milestone 1: contract freeze
|
## Milestone 1: contract freeze
|
||||||
|
|
||||||
Required before security/platform release-candidate work is declared complete:
|
Required before security/native release-candidate work is declared complete:
|
||||||
|
|
||||||
- [ ] Decide and specify whether generic component function signatures are v1.
|
- [x] Specify generic component function signatures as ordinary Go-backed v1
|
||||||
- [ ] Inventory and freeze every exported `sando` symbol, trusted type,
|
APIs and compile them in a temporary consumer module.
|
||||||
|
- [x] Inventory and freeze every exported `sando` symbol, trusted type,
|
||||||
sentinel error, concrete error field, helper, and ABI marker.
|
sentinel error, concrete error field, helper, and ABI marker.
|
||||||
- [ ] Freeze CLI commands, exit-code meanings, diagnostic codes, JSON schemas,
|
- [x] Freeze CLI commands, exit-code meanings, diagnostic codes, JSON schemas,
|
||||||
`himesan.json` schema, and generated provenance fields.
|
`himesan.json` schema, and generated provenance fields.
|
||||||
- [ ] Specify nil/stringification behavior, supported HTML-context matrix,
|
- [x] Specify nil/stringification behavior, supported HTML-context matrix,
|
||||||
component trust boundary, URL semantics, and explicit unsupported cases.
|
component trust boundary, URL semantics, and explicit unsupported cases.
|
||||||
- [ ] Add machine-checked public API, CLI, diagnostic, schema, and generated
|
- [x] Add machine-checked public API, CLI, diagnostic, schema, and generated
|
||||||
output compatibility snapshots.
|
output compatibility snapshots.
|
||||||
- [ ] Define the v1 deprecation and security-support policy.
|
- [x] Define the v1 deprecation and security-support policy.
|
||||||
|
|
||||||
## Milestone 2: security and native-platform evidence
|
## Milestone 2: security and native release evidence
|
||||||
|
|
||||||
- [ ] Run the minimum supported Go line and the latest two stable Go lines on
|
- [x] Run the pinned patched Go 1.26 and Go 1.27 lines on Linux/amd64 and
|
||||||
native Linux, macOS, and Windows hosts.
|
native Darwin/arm64 runners, plus a Linux deployment-class host.
|
||||||
- [ ] Prove identical generated bytes across those hosts and exercise native
|
- [x] Prove identical generated bytes across both native platforms and exercise
|
||||||
path, replacement, permission, race, process-tree, and watcher behavior.
|
path, replacement, permission, race, process-tree, and watcher behavior.
|
||||||
- [ ] Build a systematic differential corpus against Go's documented
|
- [x] Build a systematic differential corpus against Go's documented
|
||||||
`html/template` safety baseline for overlapping supported contexts.
|
`html/template` safety baseline for overlapping supported contexts.
|
||||||
- [ ] Parse representative outputs in real browsers and test structure/code
|
- [x] Commit a real-browser gate that generates and builds a representative
|
||||||
invariants rather than only byte equality.
|
typed document, then tests parsed structure and hostile-value inertness in
|
||||||
- [ ] Extend semantic fuzzing across delimiters, HTML transitions, imports,
|
Chrome rather than relying only on byte equality.
|
||||||
|
- [x] Extend semantic fuzzing across delimiters, HTML transitions, imports,
|
||||||
paths, source maps, URL normalization, and filesystem operations.
|
paths, source maps, URL normalization, and filesystem operations.
|
||||||
- [ ] Resolve or explicitly accept every open item in
|
- [ ] Resolve or explicitly accept every open item in
|
||||||
`SECURITY_EVIDENCE.md`; no accepted item may contradict a public guarantee.
|
`SECURITY_EVIDENCE.md`; no accepted item may contradict a public guarantee.
|
||||||
- [ ] Test delivery and reply through `security@sandwichhime.com`.
|
- [ ] Test delivery and reply through `security@sandwichhime.com`.
|
||||||
- [ ] Define severity, advisory, retraction, and CVE-request handling.
|
- [x] Define severity, advisory, retraction, and CVE-request handling.
|
||||||
|
|
||||||
## Milestone 3: measured performance and development UX
|
## Milestone 3: measured performance and development UX
|
||||||
|
|
||||||
- [ ] Commit a synthetic, repository-owned benchmark corpus comparing
|
- [x] Commit a synthetic, repository-owned benchmark corpus comparing
|
||||||
equivalent typed views and output with `html/template`.
|
equivalent typed views and output with `html/template`.
|
||||||
- [ ] Define “no material regression” before measuring the release candidate;
|
- [x] Define “no material regression” before measuring the release candidate.
|
||||||
publish hardware, OS, Go version, commands, samples, allocations, and output
|
- [x] Measure the exact release candidate and preserve hardware, OS, Go version,
|
||||||
equivalence with every result.
|
commands, samples, allocations, and output equivalence with every result.
|
||||||
- [ ] Test SSE reconnect, reload, diagnostic overlays, CSP changes, fragment/API
|
- [x] Commit a real-browser development-supervisor gate covering SSE
|
||||||
exclusions, caching, and child cleanup in a real browser on supported hosts.
|
diagnostics, reload, CSP behavior, fragment/API exclusions, caching, and
|
||||||
|
child cleanup.
|
||||||
|
- [x] Execute the committed browser gate on the exact public candidate on both
|
||||||
|
maintained hosts and preserve the browser identities and results.
|
||||||
- [ ] Remove any v1 development-supervisor guarantee that cannot be evidenced
|
- [ ] Remove any v1 development-supervisor guarantee that cannot be evidenced
|
||||||
reliably instead of substituting prose for a test.
|
reliably instead of substituting prose for a test.
|
||||||
|
|
||||||
## Milestone 4: release rehearsal
|
## Milestone 4: release rehearsal
|
||||||
|
|
||||||
- [ ] Make version validation identical in the CLI, generated headers, scripts,
|
- [x] Make version validation identical in the CLI, generated headers, scripts,
|
||||||
and release artifacts; reject ambiguous build metadata.
|
and release artifacts; reject ambiguous build metadata.
|
||||||
- [ ] Build the candidate compiler at its candidate version and prove its
|
- [x] Build the candidate compiler at its candidate version and prove its
|
||||||
committed outputs are current under that exact binary.
|
committed outputs are current under that exact binary.
|
||||||
- [ ] Produce deterministic archives/binaries, checksums, SBOMs, signatures,
|
- [x] Produce deterministic archives/binaries, checksums, SBOMs, signatures,
|
||||||
and source/build provenance from a clean sanitized canonical checkout.
|
and source/build provenance from a clean sanitized canonical checkout.
|
||||||
- [ ] Test release-key backup and two-person recovery for Gitea, domains,
|
- [x] Record the explicit v1.0.0 deferral of offline release-key restoration and
|
||||||
signing material, and publication instructions.
|
independent second-person recovery; retain the drills as assurance follow-ups
|
||||||
- [ ] Make evidence gates validate content and commit identity rather than only
|
in [ROADMAP.md](../ROADMAP.md), without claiming they passed.
|
||||||
|
- [x] Make evidence gates validate content and commit identity rather than only
|
||||||
the presence of non-empty files.
|
the presence of non-empty files.
|
||||||
- [ ] Rehearse runtime-first publication and rollback without creating public
|
- [ ] Rehearse runtime-first publication and rollback without creating public
|
||||||
semver tags.
|
semver tags.
|
||||||
@@ -147,12 +169,16 @@ Required before security/platform release-candidate work is declared complete:
|
|||||||
2. Publish signed `sando/v1.0.0-rc.1`, then signed `v1.0.0-rc.1` from the same
|
2. Publish signed `sando/v1.0.0-rc.1`, then signed `v1.0.0-rc.1` from the same
|
||||||
reviewed public Gitea commit.
|
reviewed public Gitea commit.
|
||||||
3. Verify documented installs through fresh `GOPROXY=direct` and
|
3. Verify documented installs through fresh `GOPROXY=direct` and
|
||||||
`proxy.golang.org` caches on supported Go versions and native platforms.
|
`proxy.golang.org` caches on supported Go versions under Linux/amd64 and
|
||||||
|
Darwin/arm64.
|
||||||
4. Run the complete evidence suite again from the exact public commit.
|
4. Run the complete evidence suite again from the exact public commit.
|
||||||
5. Operate the official Sandwich Hime website on the RC runtime for a 14-day
|
5. Operate the official Sandwich Hime website on the RC runtime for a 14-day
|
||||||
observation period with no unresolved Hime render, security, accessibility,
|
observation period with no unresolved Hime render, security, accessibility,
|
||||||
or rollback regression. This is product dogfooding, not a dependency on
|
or rollback regression. This is product dogfooding, not a dependency on
|
||||||
another application's private repository.
|
another application's private repository. For v1.0.0 the maintainer accepted
|
||||||
|
the existing live-use experience despite missing timed checkpoint notes;
|
||||||
|
see [ROADMAP.md](../ROADMAP.md). This does not assert the missing reviews
|
||||||
|
occurred or waive a reproduced Hime defect.
|
||||||
6. Fix findings in a new RC; restart the observation period when the affected
|
6. Fix findings in a new RC; restart the observation period when the affected
|
||||||
boundary warrants it.
|
boundary warrants it.
|
||||||
7. Finalize the changelog, supported-version table, migration notes, release
|
7. Finalize the changelog, supported-version table, migration notes, release
|
||||||
@@ -172,6 +198,6 @@ marketing. New features do not outrank a small stable contract.
|
|||||||
## Definition of confidence
|
## Definition of confidence
|
||||||
|
|
||||||
“Ready for v1” means a reviewer can trace each promise to a stable public
|
“Ready for v1” means a reviewer can trace each promise to a stable public
|
||||||
contract, executable evidence from supported native environments, and a signed
|
contract, executable evidence from supported Linux environments, and a signed
|
||||||
artifact built from the exact canonical source. It does not mean perfect,
|
artifact built from the exact canonical source. It does not mean perfect,
|
||||||
invulnerable, or finished forever.
|
invulnerable, or finished forever.
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
package compiler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"go/ast"
|
||||||
|
"go/parser"
|
||||||
|
"go/token"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var diagnosticCodePattern = regexp.MustCompile(`^HIM[0-9]{4}$`)
|
||||||
|
|
||||||
|
func TestV1DiagnosticCodeContract(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
directory := packageDirectory(t)
|
||||||
|
entries, err := os.ReadDir(directory)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
codes := make(map[string]struct{})
|
||||||
|
fileSet := token.NewFileSet()
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") || strings.HasSuffix(entry.Name(), "_test.go") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parsed, err := parser.ParseFile(fileSet, filepath.Join(directory, entry.Name()), nil, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse %s: %v", entry.Name(), err)
|
||||||
|
}
|
||||||
|
ast.Inspect(parsed, func(node ast.Node) bool {
|
||||||
|
literal, ok := node.(*ast.BasicLit)
|
||||||
|
if !ok || literal.Kind != token.STRING {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
value, err := strconv.Unquote(literal.Value)
|
||||||
|
if err == nil && diagnosticCodePattern.MatchString(value) {
|
||||||
|
codes[value] = struct{}{}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
actual := make([]string, 0, len(codes))
|
||||||
|
for code := range codes {
|
||||||
|
actual = append(actual, code)
|
||||||
|
}
|
||||||
|
sort.Strings(actual)
|
||||||
|
assertContractFile(t, filepath.Join(directory, "..", "..", "contracts", "diagnostic-codes-v1.txt"), strings.Join(actual, "\n")+"\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV1GeneratedProvenanceContract(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
compiled, diagnostics := Compile("views/generic.sando", []byte(`<?sando go
|
||||||
|
package views
|
||||||
|
func List[T ~string](values []T)
|
||||||
|
?>
|
||||||
|
<ul><? for _, value := range values { ?><li><?= value ?></li><? } ?></ul>`))
|
||||||
|
assertNoErrorDiagnostics(t, diagnostics)
|
||||||
|
lines := strings.Split(string(compiled.Code), "\n")
|
||||||
|
if len(lines) < 4 {
|
||||||
|
t.Fatalf("generated header has %d lines", len(lines))
|
||||||
|
}
|
||||||
|
actual := []string{lines[0]}
|
||||||
|
if !strings.HasPrefix(lines[1], "// himesan:compiler ") {
|
||||||
|
t.Fatalf("compiler provenance line = %q", lines[1])
|
||||||
|
}
|
||||||
|
actual = append(actual, "// himesan:compiler <compiler-version>")
|
||||||
|
if !strings.HasPrefix(lines[2], "// himesan:runtime-abi ") {
|
||||||
|
t.Fatalf("runtime provenance line = %q", lines[2])
|
||||||
|
}
|
||||||
|
actual = append(actual, "// himesan:runtime-abi <runtime-abi>")
|
||||||
|
if !regexp.MustCompile(`^// himesan:source-sha256 [0-9a-f]{64}$`).MatchString(lines[3]) {
|
||||||
|
t.Fatalf("source provenance line = %q", lines[3])
|
||||||
|
}
|
||||||
|
actual = append(actual, "// himesan:source-sha256 <lowercase-sha256>")
|
||||||
|
generated := string(compiled.Code)
|
||||||
|
if !regexp.MustCompile(`(?m)^var _ = [A-Za-z_][A-Za-z0-9_]*\.ABISandoV1$`).MatchString(generated) {
|
||||||
|
t.Fatal("generated output is missing the compile-time ABI marker")
|
||||||
|
}
|
||||||
|
actual = append(actual, "var _ = <sando-import>.ABISandoV1")
|
||||||
|
if !regexp.MustCompile(`(?m)^//line [^\r\n]+:[1-9][0-9]*:[1-9][0-9]*$`).MatchString(generated) {
|
||||||
|
t.Fatal("generated output is missing source mappings")
|
||||||
|
}
|
||||||
|
actual = append(actual, "//line <source-path>:<line>:<column>")
|
||||||
|
assertContractFile(t, filepath.Join(packageDirectory(t), "..", "..", "contracts", "generated-provenance-v1.txt"), strings.Join(actual, "\n")+"\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV1GenericComponentSignature(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
source := []byte(`<?sando go
|
||||||
|
package views
|
||||||
|
func List[T ~string](values []T)
|
||||||
|
?>
|
||||||
|
<ul><? for _, value := range values { ?><li><?= value ?></li><? } ?></ul>`)
|
||||||
|
compiled, diagnostics := Compile("views/list.sando", source)
|
||||||
|
assertNoErrorDiagnostics(t, diagnostics)
|
||||||
|
if !bytes.Contains(compiled.Code, []byte("func List[T ~string](values []T)")) {
|
||||||
|
t.Fatalf("generic signature was not preserved:\n%s", compiled.Code)
|
||||||
|
}
|
||||||
|
analyses := AnalyzeSources(context.Background(), []SourceInput{{Path: "views/list.sando", Source: source}})
|
||||||
|
if len(analyses) != 1 {
|
||||||
|
t.Fatalf("analysis count = %d, want 1", len(analyses))
|
||||||
|
}
|
||||||
|
analysis := analyses[0]
|
||||||
|
if analysis.TypeParams != "[T ~string]" || analysis.Params != "(values []T)" || analysis.Signature != "func List[T ~string](values []T)" {
|
||||||
|
t.Fatalf("generic analysis contract = %#v", analysis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func packageDirectory(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
_, file, _, ok := runtime.Caller(0)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("runtime.Caller failed")
|
||||||
|
}
|
||||||
|
return filepath.Dir(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertContractFile(t *testing.T, path, actual string) {
|
||||||
|
t.Helper()
|
||||||
|
expected, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
expected = bytes.TrimPrefix(expected, []byte("# SPDX-License-Identifier: AGPL-3.0-only\n\n"))
|
||||||
|
if string(expected) != actual {
|
||||||
|
t.Fatalf("contract drift in %s\n--- expected ---\n%s--- actual ---\n%s", path, expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -128,6 +128,9 @@ func TestRCDATACannotBeBypassedByTrustedHTML(t *testing.T) {
|
|||||||
view := View{Name: "title", URL: "/", JS: sando.TrustJS(""), HTML: sando.TrustHTML("</textarea><script>bad()</script>")}
|
view := View{Name: "title", URL: "/", JS: sando.TrustJS(""), HTML: sando.TrustHTML("</textarea><script>bad()</script>")}
|
||||||
if err := sando.Render(context.Background(), &output, Page(view)); err != nil { t.Fatal(err) }
|
if err := sando.Render(context.Background(), &output, Page(view)); err != nil { t.Fatal(err) }
|
||||||
if strings.Contains(output.String(), "</textarea><script>") { t.Fatalf("RCDATA boundary escaped: %s", output.String()) }
|
if strings.Contains(output.String(), "</textarea><script>") { t.Fatalf("RCDATA boundary escaped: %s", output.String()) }
|
||||||
|
output.Reset()
|
||||||
|
if err := sando.Render(context.Background(), &output, List([]string{"one", "two"})); err != nil { t.Fatal(err) }
|
||||||
|
if output.String() != "\n<ul><li>one</li><li>two</li></ul>" { t.Fatalf("generic component output: %q", output.String()) }
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
templatePath := filepath.Join(directory, "page.sando")
|
templatePath := filepath.Join(directory, "page.sando")
|
||||||
@@ -141,7 +144,13 @@ func Page(view View)
|
|||||||
<script><?= view.JS ?></script>
|
<script><?= view.JS ?></script>
|
||||||
<textarea><?= view.HTML ?></textarea>
|
<textarea><?= view.HTML ?></textarea>
|
||||||
</body></html>`)
|
</body></html>`)
|
||||||
result, err := Generate(context.Background(), []string{templatePath})
|
genericPath := filepath.Join(directory, "list.sando")
|
||||||
|
mustWrite(t, genericPath, `<?sando go
|
||||||
|
package generated
|
||||||
|
func List[T ~string](values []T)
|
||||||
|
?>
|
||||||
|
<ul><? for _, value := range values { ?><li><?= value ?></li><? } ?></ul>`)
|
||||||
|
result, err := Generate(context.Background(), []string{templatePath, genericPath})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Generate failed: %v (%v)", err, result.Diagnostics)
|
t.Fatalf("Generate failed: %v (%v)", err, result.Diagnostics)
|
||||||
}
|
}
|
||||||
@@ -163,7 +172,7 @@ func buildHimesanBinary(t *testing.T, repositoryRoot, name, linkerFlags string)
|
|||||||
name += ".exe"
|
name += ".exe"
|
||||||
}
|
}
|
||||||
path := filepath.Join(t.TempDir(), name)
|
path := filepath.Join(t.TempDir(), name)
|
||||||
arguments := []string{"build", "-trimpath"}
|
arguments := []string{"build", "-buildvcs=false", "-trimpath"}
|
||||||
if linkerFlags != "" {
|
if linkerFlags != "" {
|
||||||
arguments = append(arguments, "-ldflags", linkerFlags)
|
arguments = append(arguments, "-ldflags", linkerFlags)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,16 @@
|
|||||||
|
|
||||||
package compiler
|
package compiler
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"go/parser"
|
||||||
|
"go/token"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func FuzzCompileNeverPanics(f *testing.F) {
|
func FuzzCompileNeverPanics(f *testing.F) {
|
||||||
for _, seed := range []string{
|
for _, seed := range []string{
|
||||||
@@ -14,13 +23,69 @@ func FuzzCompileNeverPanics(f *testing.F) {
|
|||||||
"<?sando go\npackage p\nfunc F(v string)\n?>\n<script><!--<script></script>\n<?= v ?>\n<!--\n</script>\n-->",
|
"<?sando go\npackage p\nfunc F(v string)\n?>\n<script><!--<script></script>\n<?= v ?>\n<!--\n</script>\n-->",
|
||||||
"\xef\xbb\xbf\r\n<?sando go\r\npackage p\r\nfunc F()\r\n?>\r\n<p>x</p>",
|
"\xef\xbb\xbf\r\n<?sando go\r\npackage p\r\nfunc F()\r\n?>\r\n<p>x</p>",
|
||||||
} {
|
} {
|
||||||
f.Add(seed)
|
f.Add(seed, "fuzz.sando")
|
||||||
}
|
}
|
||||||
f.Fuzz(func(t *testing.T, source string) {
|
f.Add("<?sando go\npackage p\nfunc F()\n?>\n<p>x</p>", "path%with\ncontrols\x00.sando")
|
||||||
_, _ = Compile("fuzz.sando", []byte(source))
|
f.Fuzz(func(t *testing.T, source, mapping string) {
|
||||||
|
if len(source) > 64<<10 || len(mapping) > 4<<10 {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
input := []byte(source)
|
||||||
|
before := append([]byte(nil), input...)
|
||||||
|
first, firstDiagnostics := compileWithMapping("fuzz.sando", input, mapping)
|
||||||
|
second, secondDiagnostics := compileWithMapping("fuzz.sando", input, mapping)
|
||||||
|
if !bytes.Equal(input, before) {
|
||||||
|
t.Fatal("compiler modified its source input")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(firstDiagnostics, secondDiagnostics) ||
|
||||||
|
first.SourcePath != second.SourcePath || first.OutputPath != second.OutputPath ||
|
||||||
|
first.Package != second.Package || first.Component != second.Component ||
|
||||||
|
first.Digest != second.Digest || !bytes.Equal(first.Code, second.Code) {
|
||||||
|
t.Fatal("repeated in-memory compilation was not deterministic")
|
||||||
|
}
|
||||||
|
for _, diagnostic := range firstDiagnostics {
|
||||||
|
if diagnostic.Path != "fuzz.sando" || diagnostic.Line < 1 || diagnostic.Column < 1 {
|
||||||
|
t.Fatalf("diagnostic has an invalid location: %#v", diagnostic)
|
||||||
|
}
|
||||||
|
if !validDiagnosticCode(diagnostic.Code) || strings.TrimSpace(diagnostic.Message) != diagnostic.Message || diagnostic.Message == "" {
|
||||||
|
t.Fatalf("diagnostic violates the public shape: %#v", diagnostic)
|
||||||
|
}
|
||||||
|
if diagnostic.Severity != SeverityError && diagnostic.Severity != SeverityWarning {
|
||||||
|
t.Fatalf("diagnostic has an invalid severity: %#v", diagnostic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(first.Code) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if first.SourcePath != "fuzz.sando" || first.OutputPath != "fuzz.sando.go" {
|
||||||
|
t.Fatalf("compiled paths are invalid: %#v", first)
|
||||||
|
}
|
||||||
|
if first.Digest != fmt.Sprintf("%x", sha256.Sum256(input)) {
|
||||||
|
t.Fatalf("source digest is not bound to the exact input: %s", first.Digest)
|
||||||
|
}
|
||||||
|
if _, err := parser.ParseFile(token.NewFileSet(), "fuzz.sando.go", first.Code, parser.AllErrors); err != nil {
|
||||||
|
t.Fatalf("successful compilation produced invalid Go: %v\n%s", err, first.Code)
|
||||||
|
}
|
||||||
|
for _, line := range bytes.Split(first.Code, []byte{'\n'}) {
|
||||||
|
if bytes.HasPrefix(line, []byte("//line ")) && (bytes.ContainsAny(line, "\r\x00") || bytes.Count(line, []byte(":")) < 2) {
|
||||||
|
t.Fatalf("source-map directive was not safely encoded: %q", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validDiagnosticCode(code string) bool {
|
||||||
|
if len(code) != 7 || !strings.HasPrefix(code, "HIM") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, digit := range code[3:] {
|
||||||
|
if digit < '0' || digit > '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func FuzzGoDelimiterNeverPanics(f *testing.F) {
|
func FuzzGoDelimiterNeverPanics(f *testing.F) {
|
||||||
for _, seed := range []string{`?>`, `"?>" ?>`, "`?>` ?>", `/* ?> */ ?>`, "// ?>\n?>", `'?' ?>`} {
|
for _, seed := range []string{`?>`, `"?>" ?>`, "`?>` ?>", `/* ?> */ ?>`, "// ?>\n?>", `'?' ?>`} {
|
||||||
f.Add(seed, uint8(0))
|
f.Add(seed, uint8(0))
|
||||||
|
|||||||
@@ -0,0 +1,467 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
//go:build himesan_browser_evidence
|
||||||
|
|
||||||
|
package devserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gamertan.com/sandwich-hime/internal/compiler"
|
||||||
|
)
|
||||||
|
|
||||||
|
const realBrowserTimeout = 20 * time.Second
|
||||||
|
|
||||||
|
func TestRealBrowserDevelopmentClient(t *testing.T) {
|
||||||
|
chrome := os.Getenv("HIMESAN_CHROME")
|
||||||
|
if chrome == "" {
|
||||||
|
t.Fatal("HIMESAN_CHROME must name the reviewed Chrome or Chromium executable")
|
||||||
|
}
|
||||||
|
if info, err := os.Stat(chrome); err != nil || info.IsDir() || info.Mode()&0o111 == 0 {
|
||||||
|
t.Fatalf("HIMESAN_CHROME is not an executable regular file: %q", chrome)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("diagnostic overlay and CSP", func(t *testing.T) {
|
||||||
|
harness := newRealBrowserHarness(t)
|
||||||
|
document := runRealBrowser(t, chrome, harness.url("/"), "browser evidence diagnostic", func() {
|
||||||
|
waitForRealBrowserSubscriber(t, harness.hub)
|
||||||
|
harness.hub.publish(Event{
|
||||||
|
Type: "diagnostic", Phase: "generate", Message: "browser evidence diagnostic",
|
||||||
|
Diagnostics: []Diagnostic{{Path: "views/home.sando", Line: 7, Column: 3, Code: "HIM1300", Message: "deliberate evidence fixture"}},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
for _, want := range []string{"id=\"__himesan_overlay\"", "browser evidence diagnostic", "views/home.sando:7:3 [HIM1300]"} {
|
||||||
|
if !strings.Contains(document, want) {
|
||||||
|
t.Fatalf("browser DOM lacks %q:\n%s", want, document)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("reload", func(t *testing.T) {
|
||||||
|
harness := newRealBrowserHarness(t)
|
||||||
|
document := runRealBrowser(t, chrome, harness.url("/"), "version two", func() {
|
||||||
|
waitForRealBrowserSubscriber(t, harness.hub)
|
||||||
|
harness.version.Store(2)
|
||||||
|
harness.hub.publish(Event{Type: "reload", Phase: "serve", Message: "healthy application activated"})
|
||||||
|
})
|
||||||
|
if !strings.Contains(document, `<main id="page-version">version two</main>`) {
|
||||||
|
t.Fatalf("browser did not reload the selected document:\n%s", document)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fragment and API exclusions", func(t *testing.T) {
|
||||||
|
harness := newRealBrowserHarness(t)
|
||||||
|
for _, path := range []string{"/fragment", "/api"} {
|
||||||
|
document := runRealBrowser(t, chrome, harness.url(path), "", nil)
|
||||||
|
if strings.Contains(document, "data-himesan-reload") || strings.Contains(document, "__himesan_overlay") {
|
||||||
|
t.Fatalf("development client leaked into %s:\n%s", path, document)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("generated document parsing", func(t *testing.T) {
|
||||||
|
root := writeGeneratedBrowserApplication(t)
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
cfg.ProxyAddress = "127.0.0.1:0"
|
||||||
|
cfg.HealthPath = "/healthz"
|
||||||
|
supervisor, err := New(Options{
|
||||||
|
RootDir: root,
|
||||||
|
Config: cfg,
|
||||||
|
Generate: func(ctx context.Context) error {
|
||||||
|
_, generateErr := compiler.Generate(ctx, []string{root})
|
||||||
|
return generateErr
|
||||||
|
},
|
||||||
|
CacheDir: filepath.Join(t.TempDir(), "cache"),
|
||||||
|
PollInterval: 30 * time.Second,
|
||||||
|
Debounce: 25 * time.Millisecond,
|
||||||
|
BuildTimeout: 30 * time.Second,
|
||||||
|
StartupTimeout: integrationCandidateStartupTimeout,
|
||||||
|
ShutdownTimeout: 2 * time.Second,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
runResult := make(chan error, 1)
|
||||||
|
go func() { runResult <- supervisor.Run(ctx) }()
|
||||||
|
proxyAddress := waitForProxyAddress(t, supervisor)
|
||||||
|
waitForBody(t, "http://"+proxyAddress+"/", "generated-browser-document")
|
||||||
|
document := runRealBrowser(t, chrome, "http://"+proxyAddress+"/", "generated-browser-document", nil)
|
||||||
|
for _, want := range []string{
|
||||||
|
`<main id="generated-browser-document"`,
|
||||||
|
`<h1 id="title"><unsafe> & "quoted"</h1>`,
|
||||||
|
`<textarea id="note"></textarea><script id="attacker">window.evidenceFailed=true</script></textarea>`,
|
||||||
|
`<a id="destination" href="/safe?q=a&b=c">Open</a>`,
|
||||||
|
`<table id="table"><tbody><tr><td id="cell"><unsafe> & "quoted"</td></tr></tbody></table>`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(document, want) {
|
||||||
|
t.Fatalf("generated browser DOM lacks %q:\n%s", want, document)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(document, `<script id="attacker">`) {
|
||||||
|
t.Fatalf("hostile RCDATA became executable structure:\n%s", document)
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
select {
|
||||||
|
case err := <-runResult:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("stop generated-document supervisor: %v", err)
|
||||||
|
}
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("generated-document supervisor did not stop")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeGeneratedBrowserApplication(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
root, err := filepath.EvalSymlinks(t.TempDir())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolve test-owned browser fixture: %v", err)
|
||||||
|
}
|
||||||
|
_, sourceFile, _, ok := runtime.Caller(0)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("locate browser evidence source")
|
||||||
|
}
|
||||||
|
repositoryRoot := filepath.Clean(filepath.Join(filepath.Dir(sourceFile), "..", ".."))
|
||||||
|
files := map[string]string{
|
||||||
|
"go.mod": fmt.Sprintf("module example.test/himesan-browser-evidence\n\ngo 1.25\n\nrequire gamertan.com/sandwich-hime/sando v0.0.0\nreplace gamertan.com/sandwich-hime/sando => %s/sando\n", filepath.ToSlash(repositoryRoot)),
|
||||||
|
"page.sando": `<?sando go
|
||||||
|
package main
|
||||||
|
|
||||||
|
func Page(title string, note string, destination string)
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html><head><title><?= title ?></title></head><body>
|
||||||
|
<main id="generated-browser-document" data-note="<?= note ?>">
|
||||||
|
<h1 id="title"><?= title ?></h1>
|
||||||
|
<textarea id="note"><?= note ?></textarea>
|
||||||
|
<a id="destination" href="<?= destination ?>">Open</a>
|
||||||
|
<table id="table"><tbody><tr><td id="cell"><?= title ?></td></tr></tbody></table>
|
||||||
|
</main>
|
||||||
|
</body></html>
|
||||||
|
`,
|
||||||
|
"main.go": `package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gamertan.com/sandwich-hime/sando"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) })
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, request *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
if err := sando.Render(request.Context(), w, Page(
|
||||||
|
` + "`<unsafe> & \"quoted\"`" + `,
|
||||||
|
` + "`</textarea><script id=\"attacker\">window.evidenceFailed=true</script>`" + `,
|
||||||
|
"/safe?q=a&b=c",
|
||||||
|
)); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err := http.ListenAndServe(os.Getenv("HIMESAN_LISTEN_ADDR"), mux); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
for name, contents := range files {
|
||||||
|
if err := os.WriteFile(filepath.Join(root, name), []byte(contents), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := compiler.Generate(context.Background(), []string{root}); err != nil {
|
||||||
|
t.Fatalf("generate browser evidence fixture: %v", err)
|
||||||
|
}
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
type realBrowserHarness struct {
|
||||||
|
hub *eventHub
|
||||||
|
version atomic.Int32
|
||||||
|
upstream *httptest.Server
|
||||||
|
proxy *httptest.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRealBrowserHarness(t *testing.T) *realBrowserHarness {
|
||||||
|
t.Helper()
|
||||||
|
harness := &realBrowserHarness{hub: newEventHub()}
|
||||||
|
harness.version.Store(1)
|
||||||
|
harness.upstream = httptest.NewServer(http.HandlerFunc(harness.serveApplication))
|
||||||
|
development := newDevelopmentProxy(harness.hub)
|
||||||
|
upstreamURL, err := url.Parse(harness.upstream.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := development.setTarget(upstreamURL.Host); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
harness.proxy = httptest.NewServer(development)
|
||||||
|
proxyURL, err := url.Parse(harness.proxy.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := development.setAuthority(proxyURL.Host); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
harness.hub.close()
|
||||||
|
harness.proxy.Close()
|
||||||
|
harness.upstream.Close()
|
||||||
|
})
|
||||||
|
return harness
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *realBrowserHarness) url(path string) string {
|
||||||
|
return h.proxy.URL + path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *realBrowserHarness) serveApplication(w http.ResponseWriter, request *http.Request) {
|
||||||
|
switch request.URL.Path {
|
||||||
|
case "/":
|
||||||
|
version := "one"
|
||||||
|
if h.version.Load() == 2 {
|
||||||
|
version = "two"
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.Header().Set("Content-Security-Policy", "default-src 'none'; script-src 'none'; script-src-elem 'none'; connect-src 'none'")
|
||||||
|
fmt.Fprintf(w, `<!doctype html><html><body><main id="page-version">version %s</main></body></html>`, version)
|
||||||
|
case "/fragment":
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
fmt.Fprint(w, `<p id="fragment">fragment only</p>`)
|
||||||
|
case "/api":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
fmt.Fprint(w, `{"kind":"api","ok":true}`)
|
||||||
|
default:
|
||||||
|
http.NotFound(w, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRealBrowser(t *testing.T, chrome, target, want string, afterStart func()) string {
|
||||||
|
t.Helper()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), realBrowserTimeout)
|
||||||
|
defer cancel()
|
||||||
|
toBrowserRead, toBrowserWrite, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fromBrowserRead, fromBrowserWrite, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
_ = toBrowserRead.Close()
|
||||||
|
_ = toBrowserWrite.Close()
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer toBrowserWrite.Close()
|
||||||
|
defer fromBrowserRead.Close()
|
||||||
|
|
||||||
|
stderrPath := filepath.Join(t.TempDir(), "chrome.stderr")
|
||||||
|
stderr, err := os.OpenFile(stderrPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
|
||||||
|
if err != nil {
|
||||||
|
_ = stderr.Close()
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
command := exec.CommandContext(ctx, chrome,
|
||||||
|
"--headless=new",
|
||||||
|
"--disable-background-networking",
|
||||||
|
"--disable-component-update",
|
||||||
|
"--disable-default-apps",
|
||||||
|
"--disable-sync",
|
||||||
|
"--metrics-recording-only",
|
||||||
|
"--no-first-run",
|
||||||
|
"--no-default-browser-check",
|
||||||
|
"--user-data-dir="+t.TempDir(),
|
||||||
|
"--remote-debugging-pipe",
|
||||||
|
"about:blank",
|
||||||
|
)
|
||||||
|
command.ExtraFiles = []*os.File{toBrowserRead, fromBrowserWrite}
|
||||||
|
command.Stdout = devNull
|
||||||
|
command.Stderr = stderr
|
||||||
|
command.WaitDelay = 2 * time.Second
|
||||||
|
if err := command.Start(); err != nil {
|
||||||
|
_ = toBrowserRead.Close()
|
||||||
|
_ = fromBrowserWrite.Close()
|
||||||
|
_ = stderr.Close()
|
||||||
|
_ = devNull.Close()
|
||||||
|
t.Fatalf("start real browser: %v", err)
|
||||||
|
}
|
||||||
|
_ = toBrowserRead.Close()
|
||||||
|
_ = fromBrowserWrite.Close()
|
||||||
|
client := &devToolsPipe{reader: bufio.NewReader(fromBrowserRead), writer: toBrowserWrite}
|
||||||
|
targetResult := client.call(t, "", "Target.createTarget", map[string]any{"url": target})
|
||||||
|
var created struct {
|
||||||
|
TargetID string `json:"targetId"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(targetResult, &created); err != nil || created.TargetID == "" {
|
||||||
|
t.Fatalf("decode Chrome target: %v", err)
|
||||||
|
}
|
||||||
|
attachResult := client.call(t, "", "Target.attachToTarget", map[string]any{"targetId": created.TargetID, "flatten": true})
|
||||||
|
var attached struct {
|
||||||
|
SessionID string `json:"sessionId"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(attachResult, &attached); err != nil || attached.SessionID == "" {
|
||||||
|
t.Fatalf("decode Chrome session: %v", err)
|
||||||
|
}
|
||||||
|
client.call(t, attached.SessionID, "Runtime.enable", map[string]any{})
|
||||||
|
waitForBrowserDOM(t, client, attached.SessionID, "")
|
||||||
|
if afterStart != nil {
|
||||||
|
afterStart()
|
||||||
|
}
|
||||||
|
document := waitForBrowserDOM(t, client, attached.SessionID, want)
|
||||||
|
_ = client.callIgnoringClose("", "Browser.close", map[string]any{})
|
||||||
|
waitResult := make(chan error, 1)
|
||||||
|
go func() { waitResult <- command.Wait() }()
|
||||||
|
select {
|
||||||
|
case err := <-waitResult:
|
||||||
|
if err != nil && !errors.Is(err, exec.ErrWaitDelay) {
|
||||||
|
_ = stderr.Close()
|
||||||
|
contents, _ := os.ReadFile(stderrPath)
|
||||||
|
t.Fatalf("real browser failed: %v; stderr=%s", err, boundedBrowserOutput(string(contents)))
|
||||||
|
}
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
_ = command.Process.Kill()
|
||||||
|
t.Fatal("real browser did not exit after Browser.close")
|
||||||
|
}
|
||||||
|
_ = stderr.Close()
|
||||||
|
_ = devNull.Close()
|
||||||
|
return document
|
||||||
|
}
|
||||||
|
|
||||||
|
type devToolsPipe struct {
|
||||||
|
reader *bufio.Reader
|
||||||
|
writer io.Writer
|
||||||
|
nextID int
|
||||||
|
}
|
||||||
|
|
||||||
|
type devToolsEnvelope struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Result json.RawMessage `json:"result"`
|
||||||
|
Error *struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
} `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *devToolsPipe) call(t *testing.T, session, method string, parameters map[string]any) json.RawMessage {
|
||||||
|
t.Helper()
|
||||||
|
c.nextID++
|
||||||
|
request := map[string]any{"id": c.nextID, "method": method, "params": parameters}
|
||||||
|
if session != "" {
|
||||||
|
request["sessionId"] = session
|
||||||
|
}
|
||||||
|
contents, err := json.Marshal(request)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
contents = append(contents, 0)
|
||||||
|
if _, err := c.writer.Write(contents); err != nil {
|
||||||
|
t.Fatalf("write Chrome DevTools request: %v", err)
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
message, err := c.reader.ReadBytes(0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read Chrome DevTools response: %v", err)
|
||||||
|
}
|
||||||
|
if len(message) > 4<<20 {
|
||||||
|
t.Fatal("Chrome DevTools response exceeds 4 MiB")
|
||||||
|
}
|
||||||
|
var response devToolsEnvelope
|
||||||
|
if err := json.Unmarshal(bytes.TrimSuffix(message, []byte{0}), &response); err != nil {
|
||||||
|
t.Fatalf("decode Chrome DevTools response: %v", err)
|
||||||
|
}
|
||||||
|
if response.ID != c.nextID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if response.Error != nil {
|
||||||
|
t.Fatalf("Chrome DevTools %s failed (%d): %s", method, response.Error.Code, response.Error.Message)
|
||||||
|
}
|
||||||
|
return response.Result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *devToolsPipe) callIgnoringClose(session, method string, parameters map[string]any) error {
|
||||||
|
c.nextID++
|
||||||
|
request := map[string]any{"id": c.nextID, "method": method, "params": parameters}
|
||||||
|
if session != "" {
|
||||||
|
request["sessionId"] = session
|
||||||
|
}
|
||||||
|
contents, err := json.Marshal(request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = c.writer.Write(append(contents, 0))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForBrowserDOM(t *testing.T, client *devToolsPipe, session, want string) string {
|
||||||
|
t.Helper()
|
||||||
|
deadline := time.Now().Add(10 * time.Second)
|
||||||
|
var last string
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
result := client.call(t, session, "Runtime.evaluate", map[string]any{
|
||||||
|
"expression": "document.documentElement && document.documentElement.outerHTML",
|
||||||
|
"returnByValue": true,
|
||||||
|
})
|
||||||
|
var evaluated struct {
|
||||||
|
Result struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(result, &evaluated); err == nil && evaluated.Result.Type == "string" {
|
||||||
|
last = evaluated.Result.Value
|
||||||
|
if last != "" && (want == "" || strings.Contains(last, want)) {
|
||||||
|
return last
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
}
|
||||||
|
t.Fatalf("browser DOM did not contain %q; last DOM:\n%s", want, last)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForRealBrowserSubscriber(t *testing.T, hub *eventHub) {
|
||||||
|
t.Helper()
|
||||||
|
deadline := time.Now().Add(10 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
hub.mu.Lock()
|
||||||
|
count := len(hub.subscribers)
|
||||||
|
hub.mu.Unlock()
|
||||||
|
if count != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
t.Fatal("real browser did not establish the same-origin event stream")
|
||||||
|
}
|
||||||
|
|
||||||
|
func boundedBrowserOutput(value string) string {
|
||||||
|
const limit = 4096
|
||||||
|
if len(value) <= limit {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return value[:limit] + "\n[Chrome output truncated]"
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
package devserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestV1ConfigSchemaMatchesConfigTypeAndDefaults(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
contents, err := os.ReadFile(filepath.Join("..", "..", "contracts", "himesan-config-v1.schema.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var schema struct {
|
||||||
|
AdditionalProperties bool `json:"additionalProperties"`
|
||||||
|
Required []string `json:"required"`
|
||||||
|
Properties map[string]json.RawMessage `json:"properties"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(contents, &schema); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if schema.AdditionalProperties {
|
||||||
|
t.Fatal("v1 config schema must reject unknown fields")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(schema.Required, []string{"version"}) {
|
||||||
|
t.Fatalf("required config fields = %v, want [version]", schema.Required)
|
||||||
|
}
|
||||||
|
typeOfConfig := reflect.TypeOf(Config{})
|
||||||
|
fields := make([]string, 0, typeOfConfig.NumField())
|
||||||
|
for index := 0; index < typeOfConfig.NumField(); index++ {
|
||||||
|
name := strings.Split(typeOfConfig.Field(index).Tag.Get("json"), ",")[0]
|
||||||
|
fields = append(fields, name)
|
||||||
|
}
|
||||||
|
sort.Strings(fields)
|
||||||
|
properties := make([]string, 0, len(schema.Properties))
|
||||||
|
for name := range schema.Properties {
|
||||||
|
properties = append(properties, name)
|
||||||
|
}
|
||||||
|
sort.Strings(properties)
|
||||||
|
if !reflect.DeepEqual(fields, properties) {
|
||||||
|
t.Fatalf("Config JSON fields %v do not match schema properties %v", fields, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults := DefaultConfig()
|
||||||
|
wantDefaults := map[string]any{
|
||||||
|
"version": float64(defaults.Version),
|
||||||
|
"sourceRoots": []any{"."},
|
||||||
|
"goPackage": defaults.GoPackage,
|
||||||
|
"appArgs": []any{},
|
||||||
|
"listenAddressEnv": defaults.ListenAddressEnv,
|
||||||
|
"healthPath": defaults.HealthPath,
|
||||||
|
"proxyAddress": defaults.ProxyAddress,
|
||||||
|
"additionalWatchRoots": []any{},
|
||||||
|
}
|
||||||
|
for name, want := range wantDefaults {
|
||||||
|
var property map[string]any
|
||||||
|
if err := json.Unmarshal(schema.Properties[name], &property); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if name == "version" {
|
||||||
|
if !reflect.DeepEqual(property["const"], want) {
|
||||||
|
t.Fatalf("schema %s const = %#v, want %#v", name, property["const"], want)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(property["default"], want) {
|
||||||
|
t.Fatalf("schema %s default = %#v, want %#v", name, property["default"], want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,7 +81,7 @@ func (c *candidateProcess) cleanupProcessTree() error {
|
|||||||
processTree := c.processTree
|
processTree := c.processTree
|
||||||
c.processTree = 0
|
c.processTree = 0
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
return cleanupProcess(c.command, processTree)
|
return acceptableStopError(cleanupProcess(c.command, processTree))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *candidateProcess) stop(ctx context.Context) error {
|
func (c *candidateProcess) stop(ctx context.Context) error {
|
||||||
@@ -119,6 +119,9 @@ func acceptableStopError(err error) error {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, os.ErrProcessDone) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
var exitError *exec.ExitError
|
var exitError *exec.ExitError
|
||||||
if errors.As(err, &exitError) {
|
if errors.As(err, &exitError) {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -87,6 +87,13 @@ func TestTaskkillArguments(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAlreadyFinishedIsAnAcceptableStopResult(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if err := acceptableStopError(os.ErrProcessDone); err != nil {
|
||||||
|
t.Fatalf("already-finished process was treated as a stop failure: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestManagedProcessHelper(t *testing.T) {
|
func TestManagedProcessHelper(t *testing.T) {
|
||||||
switch os.Getenv("HIMESAN_PROCESS_HELPER") {
|
switch os.Getenv("HIMESAN_PROCESS_HELPER") {
|
||||||
case "":
|
case "":
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const integrationCandidateStartupTimeout = 3 * time.Second
|
||||||
|
|
||||||
func TestSupervisorBuildsSwapsAndCleansUp(t *testing.T) {
|
func TestSupervisorBuildsSwapsAndCleansUp(t *testing.T) {
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("integration test builds temporary Go applications")
|
t.Skip("integration test builds temporary Go applications")
|
||||||
@@ -46,11 +48,14 @@ func TestSupervisorBuildsSwapsAndCleansUp(t *testing.T) {
|
|||||||
t.Logf("supervisor event: type=%s phase=%s message=%s", event.Type, event.Phase, event.Message)
|
t.Logf("supervisor event: type=%s phase=%s message=%s", event.Type, event.Phase, event.Message)
|
||||||
events <- event
|
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,
|
||||||
BuildTimeout: 30 * time.Second,
|
BuildTimeout: 30 * time.Second,
|
||||||
StartupTimeout: 750 * time.Millisecond,
|
// Keep the production default unchanged. Native release runners may
|
||||||
|
// compile another Go line concurrently, so this integration test gives
|
||||||
|
// the temporary child enough time to be scheduled under honest load.
|
||||||
|
StartupTimeout: integrationCandidateStartupTimeout,
|
||||||
ShutdownTimeout: 2 * time.Second,
|
ShutdownTimeout: 2 * time.Second,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -128,7 +133,7 @@ func TestSupervisorClearsTargetWhenCurrentApplicationExits(t *testing.T) {
|
|||||||
PollInterval: 30 * time.Second,
|
PollInterval: 30 * time.Second,
|
||||||
Debounce: 25 * time.Millisecond,
|
Debounce: 25 * time.Millisecond,
|
||||||
BuildTimeout: 30 * time.Second,
|
BuildTimeout: 30 * time.Second,
|
||||||
StartupTimeout: time.Second,
|
StartupTimeout: integrationCandidateStartupTimeout,
|
||||||
ShutdownTimeout: 2 * time.Second,
|
ShutdownTimeout: 2 * time.Second,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -140,7 +145,11 @@ func TestSupervisorClearsTargetWhenCurrentApplicationExits(t *testing.T) {
|
|||||||
t.Cleanup(cancel)
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
proxyAddress := waitForProxyAddress(t, supervisor)
|
proxyAddress := waitForProxyAddress(t, supervisor)
|
||||||
waitForBody(t, "http://"+proxyAddress+"/", "short lived")
|
// Observe the supervisor's ordered activation event instead of racing an
|
||||||
|
// HTTP request against the deliberately short-lived child. Other tests
|
||||||
|
// prove proxy serving; this test proves that an activated child which exits
|
||||||
|
// clears its selected target under scheduler and compiler load.
|
||||||
|
waitForEvent(t, events, "reload", "serve")
|
||||||
waitForPhase(t, events, "run")
|
waitForPhase(t, events, "run")
|
||||||
if target := supervisor.proxy.target.Load(); target != nil {
|
if target := supervisor.proxy.target.Load(); target != nil {
|
||||||
t.Fatalf("proxy retained exited upstream %v", target)
|
t.Fatalf("proxy retained exited upstream %v", target)
|
||||||
@@ -290,17 +299,22 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func waitForPhase(t *testing.T, events <-chan Event, phase string) {
|
func waitForPhase(t *testing.T, events <-chan Event, phase string) {
|
||||||
|
t.Helper()
|
||||||
|
waitForEvent(t, events, "diagnostic", phase)
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForEvent(t *testing.T, events <-chan Event, eventType, phase string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
timer := time.NewTimer(10 * time.Second)
|
timer := time.NewTimer(10 * time.Second)
|
||||||
defer timer.Stop()
|
defer timer.Stop()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case event := <-events:
|
case event := <-events:
|
||||||
if event.Type == "diagnostic" && event.Phase == phase {
|
if event.Type == eventType && event.Phase == phase {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
t.Fatalf("did not receive %s diagnostic", phase)
|
t.Fatalf("did not receive %s/%s event", eventType, phase)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gamertan.com/sandwich-hime/internal/compiler"
|
"gamertan.com/sandwich-hime/internal/compiler"
|
||||||
|
"gamertan.com/sandwich-hime/internal/testpath"
|
||||||
)
|
)
|
||||||
|
|
||||||
type protocolClient struct {
|
type protocolClient struct {
|
||||||
@@ -140,7 +141,7 @@ func (client *protocolClient) waitDiagnostics(t *testing.T, uri string, wantCode
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestServerOverlayFeaturesAndNoWrites(t *testing.T) {
|
func TestServerOverlayFeaturesAndNoWrites(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := testpath.TempDir(t)
|
||||||
writeTestFile(t, filepath.Join(root, "go.mod"), "module example.test/project\n\ngo 1.25\n")
|
writeTestFile(t, filepath.Join(root, "go.mod"), "module example.test/project\n\ngo 1.25\n")
|
||||||
homePath := filepath.Join(root, "home.sando")
|
homePath := filepath.Join(root, "home.sando")
|
||||||
badgePath := filepath.Join(root, "cards", "badge.sando")
|
badgePath := filepath.Join(root, "cards", "badge.sando")
|
||||||
@@ -211,7 +212,7 @@ func TestServerOverlayFeaturesAndNoWrites(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestOverlayHonorsNestedModuleAndSymlinkBoundaries(t *testing.T) {
|
func TestOverlayHonorsNestedModuleAndSymlinkBoundaries(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := testpath.TempDir(t)
|
||||||
writeTestFile(t, filepath.Join(root, "go.mod"), "module example.test/root\n")
|
writeTestFile(t, filepath.Join(root, "go.mod"), "module example.test/root\n")
|
||||||
nestedPath := filepath.Join(root, "nested", "view.sando")
|
nestedPath := filepath.Join(root, "nested", "view.sando")
|
||||||
writeTestFile(t, filepath.Join(root, "nested", "go.mod"), "module example.test/nested\n")
|
writeTestFile(t, filepath.Join(root, "nested", "go.mod"), "module example.test/nested\n")
|
||||||
@@ -240,7 +241,7 @@ func TestOverlayHonorsNestedModuleAndSymlinkBoundaries(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestServerRejectsMultipleRootsAndCanceledRequest(t *testing.T) {
|
func TestServerRejectsMultipleRootsAndCanceledRequest(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := testpath.TempDir(t)
|
||||||
server := &Server{initialized: true, snapshot: workspaceSnapshot{documents: map[string]document{}, analyses: map[string]compiler.DocumentAnalysis{}}}
|
server := &Server{initialized: true, snapshot: workspaceSnapshot{documents: map[string]document{}, analyses: map[string]compiler.DocumentAnalysis{}}}
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
cancel()
|
cancel()
|
||||||
@@ -263,7 +264,7 @@ func TestServerRejectsMultipleRootsAndCanceledRequest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReindexCountsOpenOverlaysInWorkspaceLimit(t *testing.T) {
|
func TestReindexCountsOpenOverlaysInWorkspaceLimit(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := testpath.TempDir(t)
|
||||||
server := &Server{
|
server := &Server{
|
||||||
root: root,
|
root: root,
|
||||||
overlays: make(map[string]document, maxWorkspaceFiles+1),
|
overlays: make(map[string]document, maxWorkspaceFiles+1),
|
||||||
|
|||||||
@@ -0,0 +1,423 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// Package releaseartifact creates deterministic Hime-san release archives and
|
||||||
|
// checksummed native-platform verification receipts.
|
||||||
|
package releaseartifact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const receiptSchema = "himesan.native-verification.v1"
|
||||||
|
|
||||||
|
var (
|
||||||
|
releaseVersionPattern = regexp.MustCompile(`^v(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$`)
|
||||||
|
gitObjectPattern = regexp.MustCompile(`^[0-9a-f]{40,64}$`)
|
||||||
|
digestPattern = regexp.MustCompile(`^[0-9a-f]{64}$`)
|
||||||
|
goVersionPattern = regexp.MustCompile(`^go(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$`)
|
||||||
|
platformPattern = regexp.MustCompile(`^[a-z0-9]+$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// PackageOptions contains the reviewed inputs to one release archive.
|
||||||
|
type PackageOptions struct {
|
||||||
|
Version string
|
||||||
|
Commit string
|
||||||
|
Tree string
|
||||||
|
GoVersion string
|
||||||
|
GOOS string
|
||||||
|
GOARCH string
|
||||||
|
BinaryPath string
|
||||||
|
LicensePath string
|
||||||
|
ReleaseNotes string
|
||||||
|
OutputDirectory string
|
||||||
|
SourceDateEpoch int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageResult identifies the immutable unsigned release archive.
|
||||||
|
type PackageResult struct {
|
||||||
|
ArchivePath string `json:"archive_path"`
|
||||||
|
SHA256 string `json:"sha256"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type buildInfo struct {
|
||||||
|
SchemaVersion int `json:"schema_version"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Commit string `json:"commit"`
|
||||||
|
Tree string `json:"tree"`
|
||||||
|
GoVersion string `json:"go_version"`
|
||||||
|
GOOS string `json:"goos"`
|
||||||
|
GOARCH string `json:"goarch"`
|
||||||
|
BinarySHA256 string `json:"binary_sha256"`
|
||||||
|
UnsignedBinarySHA256 string `json:"unsigned_binary_sha256,omitempty"`
|
||||||
|
UnsignedArchiveSHA256 string `json:"unsigned_archive_sha256,omitempty"`
|
||||||
|
SigningIdentifier string `json:"signing_identifier,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type spdxDocument struct {
|
||||||
|
SPDXVersion string `json:"spdxVersion"`
|
||||||
|
DataLicense string `json:"dataLicense"`
|
||||||
|
SPDXID string `json:"SPDXID"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
DocumentNamespace string `json:"documentNamespace"`
|
||||||
|
CreationInfo creationInfo `json:"creationInfo"`
|
||||||
|
Packages []spdxPackage `json:"packages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type creationInfo struct {
|
||||||
|
Created string `json:"created"`
|
||||||
|
Creators []string `json:"creators"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type spdxPackage struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
SPDXID string `json:"SPDXID"`
|
||||||
|
VersionInfo string `json:"versionInfo"`
|
||||||
|
DownloadLocation string `json:"downloadLocation"`
|
||||||
|
FilesAnalyzed bool `json:"filesAnalyzed"`
|
||||||
|
LicenseConcluded string `json:"licenseConcluded"`
|
||||||
|
LicenseDeclared string `json:"licenseDeclared"`
|
||||||
|
Checksums []spdxChecksum `json:"checksums"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type spdxChecksum struct {
|
||||||
|
Algorithm string `json:"algorithm"`
|
||||||
|
ChecksumValue string `json:"checksumValue"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receipt describes one completed native verification run.
|
||||||
|
type Receipt struct {
|
||||||
|
SchemaVersion string `json:"schema_version"`
|
||||||
|
Repository string `json:"repository"`
|
||||||
|
Commit string `json:"commit"`
|
||||||
|
Tree string `json:"tree"`
|
||||||
|
GOOS string `json:"goos"`
|
||||||
|
GOARCH string `json:"goarch"`
|
||||||
|
GoVersion string `json:"go_version"`
|
||||||
|
RunnerVersion string `json:"runner_version"`
|
||||||
|
RunnerName string `json:"runner_name"`
|
||||||
|
GeneratedDigest string `json:"generated_output_sha256"`
|
||||||
|
UnsignedArtifactSHA string `json:"unsigned_artifact_sha256,omitempty"`
|
||||||
|
SuccessfulGates []string `json:"successful_gates"`
|
||||||
|
CompletedAt string `json:"completed_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type archiveMember struct {
|
||||||
|
name string
|
||||||
|
mode int64
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package creates a byte-reproducible unsigned tar.gz and its SHA-256 sidecar.
|
||||||
|
func Package(options PackageOptions) (PackageResult, error) {
|
||||||
|
if err := validatePackageOptions(options); err != nil {
|
||||||
|
return PackageResult{}, err
|
||||||
|
}
|
||||||
|
binary, err := os.ReadFile(options.BinaryPath)
|
||||||
|
if err != nil {
|
||||||
|
return PackageResult{}, fmt.Errorf("read binary: %w", err)
|
||||||
|
}
|
||||||
|
license, err := os.ReadFile(options.LicensePath)
|
||||||
|
if err != nil {
|
||||||
|
return PackageResult{}, fmt.Errorf("read license: %w", err)
|
||||||
|
}
|
||||||
|
releaseNotes, err := os.ReadFile(options.ReleaseNotes)
|
||||||
|
if err != nil {
|
||||||
|
return PackageResult{}, fmt.Errorf("read release notes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
binaryDigest := digest(binary)
|
||||||
|
infoBytes, err := indentedJSON(buildInfo{
|
||||||
|
SchemaVersion: 1,
|
||||||
|
Version: options.Version,
|
||||||
|
Commit: options.Commit,
|
||||||
|
Tree: options.Tree,
|
||||||
|
GoVersion: options.GoVersion,
|
||||||
|
GOOS: options.GOOS,
|
||||||
|
GOARCH: options.GOARCH,
|
||||||
|
BinarySHA256: binaryDigest,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return PackageResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
created := time.Unix(options.SourceDateEpoch, 0).UTC().Format(time.RFC3339)
|
||||||
|
sbomBytes, err := indentedJSON(spdxDocument{
|
||||||
|
SPDXVersion: "SPDX-2.3",
|
||||||
|
DataLicense: "CC0-1.0",
|
||||||
|
SPDXID: "SPDXRef-DOCUMENT",
|
||||||
|
Name: "himesan-" + options.Version + "-" + options.GOOS + "-" + options.GOARCH,
|
||||||
|
DocumentNamespace: "https://sandwichhime.com/spdx/himesan/" + options.Version + "/" + options.Commit + "/" + options.GOOS + "-" + options.GOARCH,
|
||||||
|
CreationInfo: creationInfo{
|
||||||
|
Created: created,
|
||||||
|
Creators: []string{"Organization: Gamertan"},
|
||||||
|
},
|
||||||
|
Packages: []spdxPackage{{
|
||||||
|
Name: "himesan",
|
||||||
|
SPDXID: "SPDXRef-Package-himesan",
|
||||||
|
VersionInfo: options.Version,
|
||||||
|
DownloadLocation: "NOASSERTION",
|
||||||
|
FilesAnalyzed: false,
|
||||||
|
LicenseConcluded: "AGPL-3.0-only",
|
||||||
|
LicenseDeclared: "AGPL-3.0-only",
|
||||||
|
Checksums: []spdxChecksum{{
|
||||||
|
Algorithm: "SHA256",
|
||||||
|
ChecksumValue: binaryDigest,
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return PackageResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
members := []archiveMember{
|
||||||
|
{name: "BUILDINFO.json", mode: 0o444, data: infoBytes},
|
||||||
|
{name: "LICENSE", mode: 0o444, data: license},
|
||||||
|
{name: "RELEASE.md", mode: 0o444, data: releaseNotes},
|
||||||
|
{name: "SBOM.spdx.json", mode: 0o444, data: sbomBytes},
|
||||||
|
{name: "himesan", mode: 0o555, data: binary},
|
||||||
|
}
|
||||||
|
checksumLines := make([]string, 0, len(members))
|
||||||
|
for _, member := range members {
|
||||||
|
checksumLines = append(checksumLines, digest(member.data)+" "+member.name)
|
||||||
|
}
|
||||||
|
members = append(members, archiveMember{
|
||||||
|
name: "SHA256SUMS", mode: 0o444,
|
||||||
|
data: []byte(strings.Join(checksumLines, "\n") + "\n"),
|
||||||
|
})
|
||||||
|
sort.Slice(members, func(i, j int) bool { return members[i].name < members[j].name })
|
||||||
|
|
||||||
|
if err := os.MkdirAll(options.OutputDirectory, 0o755); err != nil {
|
||||||
|
return PackageResult{}, fmt.Errorf("create output directory: %w", err)
|
||||||
|
}
|
||||||
|
base := "himesan-" + strings.TrimPrefix(options.Version, "v") + "-" + options.GOOS + "-" + options.GOARCH
|
||||||
|
archivePath := filepath.Join(options.OutputDirectory, base+".tar.gz")
|
||||||
|
if err := writeArchive(archivePath, base, members, time.Unix(options.SourceDateEpoch, 0).UTC()); err != nil {
|
||||||
|
return PackageResult{}, err
|
||||||
|
}
|
||||||
|
archive, err := os.ReadFile(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
return PackageResult{}, fmt.Errorf("read completed archive: %w", err)
|
||||||
|
}
|
||||||
|
archiveDigest := digest(archive)
|
||||||
|
if err := writeExclusive(archivePath+".sha256", []byte(archiveDigest+" "+filepath.Base(archivePath)+"\n"), 0o444); err != nil {
|
||||||
|
return PackageResult{}, fmt.Errorf("write archive checksum: %w", err)
|
||||||
|
}
|
||||||
|
return PackageResult{ArchivePath: archivePath, SHA256: archiveDigest}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteReceipt validates, canonicalizes, and writes a receipt plus SHA sidecar.
|
||||||
|
func WriteReceipt(path string, receipt Receipt) (string, error) {
|
||||||
|
receipt.SchemaVersion = receiptSchema
|
||||||
|
if err := validateReceipt(receipt); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
sort.Strings(receipt.SuccessfulGates)
|
||||||
|
contents, err := indentedJSON(receipt)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := writeExclusive(path, contents, 0o444); err != nil {
|
||||||
|
return "", fmt.Errorf("write receipt: %w", err)
|
||||||
|
}
|
||||||
|
checksum := digest(contents)
|
||||||
|
if err := writeExclusive(path+".sha256", []byte(checksum+" "+filepath.Base(path)+"\n"), 0o444); err != nil {
|
||||||
|
return "", fmt.Errorf("write receipt checksum: %w", err)
|
||||||
|
}
|
||||||
|
return checksum, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DigestFiles returns a stable digest over sorted names and file contents.
|
||||||
|
func DigestFiles(paths []string) (string, error) {
|
||||||
|
if len(paths) == 0 {
|
||||||
|
return "", errors.New("at least one generated file is required")
|
||||||
|
}
|
||||||
|
sorted := append([]string(nil), paths...)
|
||||||
|
sort.Strings(sorted)
|
||||||
|
hash := sha256.New()
|
||||||
|
for _, path := range sorted {
|
||||||
|
contents, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("read %s: %w", path, err)
|
||||||
|
}
|
||||||
|
_, _ = io.WriteString(hash, filepath.ToSlash(path))
|
||||||
|
_, _ = hash.Write([]byte{0})
|
||||||
|
_, _ = hash.Write(contents)
|
||||||
|
_, _ = hash.Write([]byte{0})
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePackageOptions(options PackageOptions) error {
|
||||||
|
for name, value := range map[string]string{
|
||||||
|
"version": options.Version, "commit": options.Commit, "tree": options.Tree,
|
||||||
|
"go version": options.GoVersion, "GOOS": options.GOOS, "GOARCH": options.GOARCH,
|
||||||
|
"binary": options.BinaryPath, "license": options.LicensePath,
|
||||||
|
"release notes": options.ReleaseNotes, "output directory": options.OutputDirectory,
|
||||||
|
} {
|
||||||
|
if strings.TrimSpace(value) == "" {
|
||||||
|
return fmt.Errorf("%s is required", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.SourceDateEpoch <= 0 {
|
||||||
|
return errors.New("source date epoch must be positive")
|
||||||
|
}
|
||||||
|
if !releaseVersionPattern.MatchString(options.Version) {
|
||||||
|
return errors.New("version must be a canonical v-prefixed semantic version without build metadata")
|
||||||
|
}
|
||||||
|
if !gitObjectPattern.MatchString(options.Commit) || !gitObjectPattern.MatchString(options.Tree) {
|
||||||
|
return errors.New("commit and tree must be full lowercase Git object identities")
|
||||||
|
}
|
||||||
|
if !goVersionPattern.MatchString(options.GoVersion) {
|
||||||
|
return errors.New("Go version must be a complete goX.Y.Z toolchain identity")
|
||||||
|
}
|
||||||
|
if !platformPattern.MatchString(options.GOOS) || !platformPattern.MatchString(options.GOARCH) {
|
||||||
|
return errors.New("GOOS and GOARCH must contain only lowercase letters and digits")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateReceipt(receipt Receipt) error {
|
||||||
|
for name, value := range map[string]string{
|
||||||
|
"repository": receipt.Repository, "commit": receipt.Commit, "tree": receipt.Tree,
|
||||||
|
"GOOS": receipt.GOOS, "GOARCH": receipt.GOARCH, "Go version": receipt.GoVersion,
|
||||||
|
"runner version": receipt.RunnerVersion, "runner name": receipt.RunnerName,
|
||||||
|
"generated digest": receipt.GeneratedDigest, "completed at": receipt.CompletedAt,
|
||||||
|
} {
|
||||||
|
if strings.TrimSpace(value) == "" {
|
||||||
|
return fmt.Errorf("%s is required", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(receipt.SuccessfulGates) == 0 {
|
||||||
|
return errors.New("at least one successful gate is required")
|
||||||
|
}
|
||||||
|
if !gitObjectPattern.MatchString(receipt.Commit) || !gitObjectPattern.MatchString(receipt.Tree) {
|
||||||
|
return errors.New("commit and tree must be full lowercase Git object identities")
|
||||||
|
}
|
||||||
|
if !goVersionPattern.MatchString(receipt.GoVersion) {
|
||||||
|
return errors.New("Go version must be a complete goX.Y.Z toolchain identity")
|
||||||
|
}
|
||||||
|
if !platformPattern.MatchString(receipt.GOOS) || !platformPattern.MatchString(receipt.GOARCH) {
|
||||||
|
return errors.New("GOOS and GOARCH must contain only lowercase letters and digits")
|
||||||
|
}
|
||||||
|
if !digestPattern.MatchString(receipt.GeneratedDigest) {
|
||||||
|
return errors.New("generated-output digest must be a lowercase SHA-256")
|
||||||
|
}
|
||||||
|
if receipt.UnsignedArtifactSHA != "" && !digestPattern.MatchString(receipt.UnsignedArtifactSHA) {
|
||||||
|
return errors.New("unsigned-artifact digest must be a lowercase SHA-256")
|
||||||
|
}
|
||||||
|
if _, err := time.Parse(time.RFC3339, receipt.CompletedAt); err != nil {
|
||||||
|
return fmt.Errorf("completed at must be RFC3339: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeArchive(path, root string, members []archiveMember, modified time.Time) error {
|
||||||
|
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create archive: %w", err)
|
||||||
|
}
|
||||||
|
success := false
|
||||||
|
defer func() {
|
||||||
|
_ = file.Close()
|
||||||
|
if !success {
|
||||||
|
_ = os.Remove(path)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
gzipWriter := gzip.NewWriter(file)
|
||||||
|
gzipWriter.Header.ModTime = modified
|
||||||
|
gzipWriter.Header.OS = 255
|
||||||
|
tarWriter := tar.NewWriter(gzipWriter)
|
||||||
|
for _, member := range members {
|
||||||
|
header := &tar.Header{
|
||||||
|
Name: root + "/" + member.name,
|
||||||
|
Mode: member.mode,
|
||||||
|
Size: int64(len(member.data)),
|
||||||
|
ModTime: modified,
|
||||||
|
AccessTime: time.Time{},
|
||||||
|
ChangeTime: time.Time{},
|
||||||
|
Uid: 0,
|
||||||
|
Gid: 0,
|
||||||
|
Uname: "",
|
||||||
|
Gname: "",
|
||||||
|
Format: tar.FormatUSTAR,
|
||||||
|
}
|
||||||
|
if err := tarWriter.WriteHeader(header); err != nil {
|
||||||
|
return fmt.Errorf("write archive header: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := tarWriter.Write(member.data); err != nil {
|
||||||
|
return fmt.Errorf("write archive member: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := tarWriter.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close tar stream: %w", err)
|
||||||
|
}
|
||||||
|
if err := gzipWriter.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close gzip stream: %w", err)
|
||||||
|
}
|
||||||
|
if err := file.Sync(); err != nil {
|
||||||
|
return fmt.Errorf("sync archive: %w", err)
|
||||||
|
}
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close archive: %w", err)
|
||||||
|
}
|
||||||
|
if err := os.Chmod(path, 0o444); err != nil {
|
||||||
|
return fmt.Errorf("set archive permissions: %w", err)
|
||||||
|
}
|
||||||
|
success = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeExclusive(path string, contents []byte, mode os.FileMode) error {
|
||||||
|
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
success := false
|
||||||
|
defer func() {
|
||||||
|
_ = file.Close()
|
||||||
|
if !success {
|
||||||
|
_ = os.Remove(path)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if _, err := file.Write(contents); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := file.Sync(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.Chmod(path, mode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
success = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func indentedJSON(value any) ([]byte, error) {
|
||||||
|
contents, err := json.MarshalIndent(value, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("encode JSON: %w", err)
|
||||||
|
}
|
||||||
|
return append(contents, '\n'), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func digest(contents []byte) string {
|
||||||
|
sum := sha256.Sum256(contents)
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
package releaseartifact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPackageIsByteReproducible(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
binary := writeFixture(t, directory, "himesan", "native binary")
|
||||||
|
license := writeFixture(t, directory, "LICENSE", "license")
|
||||||
|
releaseNotes := writeFixture(t, directory, "RELEASE.md", "release")
|
||||||
|
options := PackageOptions{
|
||||||
|
Version: "v1.0.0-rc.1", Commit: strings.Repeat("a", 40), Tree: strings.Repeat("b", 40), GoVersion: "go1.27.0",
|
||||||
|
GOOS: "darwin", GOARCH: "arm64", BinaryPath: binary, LicensePath: license,
|
||||||
|
ReleaseNotes: releaseNotes, SourceDateEpoch: 1_700_000_000,
|
||||||
|
}
|
||||||
|
options.OutputDirectory = filepath.Join(directory, "first")
|
||||||
|
first, err := Package(options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
options.OutputDirectory = filepath.Join(directory, "second")
|
||||||
|
second, err := Package(options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if first.SHA256 != second.SHA256 {
|
||||||
|
t.Fatalf("repeated packages differ: %s != %s", first.SHA256, second.SHA256)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteReceiptSortsGatesAndWritesChecksum(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "receipt.json")
|
||||||
|
checksum, err := WriteReceipt(path, Receipt{
|
||||||
|
Repository: "example.test/development-source", Commit: strings.Repeat("a", 40), Tree: strings.Repeat("b", 40),
|
||||||
|
GOOS: "darwin", GOARCH: "arm64", GoVersion: "go1.27.0",
|
||||||
|
RunnerVersion: "v3.3.0", RunnerName: "mac", GeneratedDigest: strings.Repeat("c", 64),
|
||||||
|
SuccessfulGates: []string{"vet", "test"}, CompletedAt: time.Unix(1_700_000_000, 0).UTC().Format(time.RFC3339),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if checksum == "" {
|
||||||
|
t.Fatal("empty checksum")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(path + ".sha256"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackageRejectsUnsafeVersionAndExistingArtifact(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
options := PackageOptions{
|
||||||
|
Version: "../../outside", Commit: strings.Repeat("a", 40), Tree: strings.Repeat("b", 40),
|
||||||
|
GoVersion: "go1.27.0", GOOS: "darwin", GOARCH: "arm64",
|
||||||
|
BinaryPath: writeFixture(t, directory, "himesan", "native binary"),
|
||||||
|
LicensePath: writeFixture(t, directory, "LICENSE", "license"),
|
||||||
|
ReleaseNotes: writeFixture(t, directory, "RELEASE.md", "release"),
|
||||||
|
OutputDirectory: filepath.Join(directory, "output"), SourceDateEpoch: 1_700_000_000,
|
||||||
|
}
|
||||||
|
if _, err := Package(options); err == nil {
|
||||||
|
t.Fatal("unsafe version was accepted")
|
||||||
|
}
|
||||||
|
options.Version = "v1.0.0-rc.1"
|
||||||
|
if _, err := Package(options); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := Package(options); err == nil {
|
||||||
|
t.Fatal("existing immutable artifact was overwritten")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDigestFilesBindsNamesAndBytes(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
one := writeFixture(t, directory, "one", "same")
|
||||||
|
two := writeFixture(t, directory, "two", "same")
|
||||||
|
forward, err := DigestFiles([]string{two, one})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
reverse, err := DigestFiles([]string{one, two})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if forward != reverse {
|
||||||
|
t.Fatal("file ordering changed digest")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFixture(t *testing.T, directory, name, contents string) string {
|
||||||
|
t.Helper()
|
||||||
|
path := filepath.Join(directory, name)
|
||||||
|
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
package releaseartifact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
evidenceSchema = "himesan.release-evidence.v1"
|
||||||
|
evidenceManifestName = "RELEASE-EVIDENCE.json"
|
||||||
|
maximumEvidenceSize = 1 << 20
|
||||||
|
)
|
||||||
|
|
||||||
|
var requiredEvidenceFiles = []string{
|
||||||
|
"benchmark-methodology.md",
|
||||||
|
"development-supervisor.md",
|
||||||
|
"legal-review.md",
|
||||||
|
"native-platforms.md",
|
||||||
|
"security.md",
|
||||||
|
"signing-and-recovery.md",
|
||||||
|
"vanity-imports.md",
|
||||||
|
}
|
||||||
|
|
||||||
|
// EvidenceIdentity binds human-reviewed release evidence to one source state.
|
||||||
|
type EvidenceIdentity struct {
|
||||||
|
Repository string
|
||||||
|
Version string
|
||||||
|
Commit string
|
||||||
|
Tree string
|
||||||
|
ReviewedBy string
|
||||||
|
ReviewedAt string
|
||||||
|
}
|
||||||
|
|
||||||
|
type evidenceManifest struct {
|
||||||
|
SchemaVersion string `json:"schema_version"`
|
||||||
|
Repository string `json:"repository"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Commit string `json:"commit"`
|
||||||
|
Tree string `json:"tree"`
|
||||||
|
ReviewStatus string `json:"review_status"`
|
||||||
|
ReviewedBy string `json:"reviewed_by"`
|
||||||
|
ReviewedAt string `json:"reviewed_at"`
|
||||||
|
Files []evidenceFile `json:"files"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type evidenceFile struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
SHA256 string `json:"sha256"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequiredEvidenceFiles returns the fixed v1 human-review document set.
|
||||||
|
func RequiredEvidenceFiles() []string {
|
||||||
|
return append([]string(nil), requiredEvidenceFiles...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteEvidenceManifest seals the reviewed evidence documents without
|
||||||
|
// modifying them. The manifest is immutable and fails if it already exists.
|
||||||
|
func WriteEvidenceManifest(directory string, identity EvidenceIdentity) (string, error) {
|
||||||
|
if err := validateEvidenceIdentity(identity, true); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
files, err := inspectEvidenceFiles(directory)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
manifest := evidenceManifest{
|
||||||
|
SchemaVersion: evidenceSchema,
|
||||||
|
Repository: identity.Repository,
|
||||||
|
Version: identity.Version,
|
||||||
|
Commit: identity.Commit,
|
||||||
|
Tree: identity.Tree,
|
||||||
|
ReviewStatus: "reviewed",
|
||||||
|
ReviewedBy: identity.ReviewedBy,
|
||||||
|
ReviewedAt: identity.ReviewedAt,
|
||||||
|
Files: files,
|
||||||
|
}
|
||||||
|
contents, err := indentedJSON(manifest)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
path := filepath.Join(directory, evidenceManifestName)
|
||||||
|
if err := writeExclusive(path, contents, 0o444); err != nil {
|
||||||
|
return "", fmt.Errorf("write release evidence manifest: %w", err)
|
||||||
|
}
|
||||||
|
return digest(contents), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyEvidenceManifest proves that the sealed evidence is for the expected
|
||||||
|
// source state and that every reviewed byte remains unchanged.
|
||||||
|
func VerifyEvidenceManifest(directory string, expected EvidenceIdentity) error {
|
||||||
|
if err := validateEvidenceIdentity(expected, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
manifestPath := filepath.Join(directory, evidenceManifestName)
|
||||||
|
contents, err := readEvidenceFile(manifestPath, true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read release evidence manifest: %w", err)
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(contents))
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
var manifest evidenceManifest
|
||||||
|
if err := decoder.Decode(&manifest); err != nil {
|
||||||
|
return fmt.Errorf("decode release evidence manifest: %w", err)
|
||||||
|
}
|
||||||
|
if err := requireJSONEOF(decoder); err != nil {
|
||||||
|
return fmt.Errorf("decode release evidence manifest: %w", err)
|
||||||
|
}
|
||||||
|
if manifest.SchemaVersion != evidenceSchema {
|
||||||
|
return fmt.Errorf("release evidence schema = %q, want %q", manifest.SchemaVersion, evidenceSchema)
|
||||||
|
}
|
||||||
|
if manifest.ReviewStatus != "reviewed" {
|
||||||
|
return errors.New("release evidence review status must be reviewed")
|
||||||
|
}
|
||||||
|
actual := EvidenceIdentity{
|
||||||
|
Repository: manifest.Repository,
|
||||||
|
Version: manifest.Version,
|
||||||
|
Commit: manifest.Commit,
|
||||||
|
Tree: manifest.Tree,
|
||||||
|
ReviewedBy: manifest.ReviewedBy,
|
||||||
|
ReviewedAt: manifest.ReviewedAt,
|
||||||
|
}
|
||||||
|
if err := validateEvidenceIdentity(actual, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if actual.Repository != expected.Repository || actual.Version != expected.Version ||
|
||||||
|
actual.Commit != expected.Commit || actual.Tree != expected.Tree {
|
||||||
|
return errors.New("release evidence repository, version, commit, or tree does not match the candidate")
|
||||||
|
}
|
||||||
|
|
||||||
|
observed, err := inspectEvidenceFiles(directory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(manifest.Files) != len(observed) {
|
||||||
|
return fmt.Errorf("release evidence manifest contains %d files, want %d", len(manifest.Files), len(observed))
|
||||||
|
}
|
||||||
|
for index := range observed {
|
||||||
|
if manifest.Files[index] != observed[index] {
|
||||||
|
return fmt.Errorf("release evidence file %q is missing, reordered, or has a changed digest", observed[index].Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func inspectEvidenceFiles(directory string) ([]evidenceFile, error) {
|
||||||
|
info, err := os.Lstat(directory)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("inspect evidence directory: %w", err)
|
||||||
|
}
|
||||||
|
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
return nil, errors.New("evidence directory must be a real directory, not a symlink")
|
||||||
|
}
|
||||||
|
files := make([]evidenceFile, 0, len(requiredEvidenceFiles))
|
||||||
|
for _, name := range requiredEvidenceFiles {
|
||||||
|
contents, err := readEvidenceFile(filepath.Join(directory, name), false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("inspect %s: %w", name, err)
|
||||||
|
}
|
||||||
|
if !utf8.Valid(contents) || bytes.IndexByte(contents, 0) >= 0 {
|
||||||
|
return nil, fmt.Errorf("%s must be NUL-free UTF-8 text", name)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(contents), "# ") {
|
||||||
|
return nil, fmt.Errorf("%s must contain a Markdown heading", name)
|
||||||
|
}
|
||||||
|
files = append(files, evidenceFile{Path: name, SHA256: digest(contents)})
|
||||||
|
}
|
||||||
|
sort.Slice(files, func(i, j int) bool { return files[i].Path < files[j].Path })
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readEvidenceFile(path string, manifest bool) ([]byte, error) {
|
||||||
|
info, err := os.Lstat(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
return nil, errors.New("must be a regular file, not a symlink")
|
||||||
|
}
|
||||||
|
if info.Size() == 0 || info.Size() > maximumEvidenceSize {
|
||||||
|
return nil, fmt.Errorf("size must be between 1 and %d bytes", maximumEvidenceSize)
|
||||||
|
}
|
||||||
|
contents, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if manifest && len(contents) > maximumEvidenceSize {
|
||||||
|
return nil, errors.New("manifest exceeds the size limit")
|
||||||
|
}
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateEvidenceIdentity(identity EvidenceIdentity, requireReview bool) error {
|
||||||
|
if strings.TrimSpace(identity.Repository) == "" || strings.ContainsAny(identity.Repository, "\r\n\x00") {
|
||||||
|
return errors.New("evidence repository is required and must be one line")
|
||||||
|
}
|
||||||
|
if !releaseVersionPattern.MatchString(identity.Version) {
|
||||||
|
return errors.New("evidence version must be a canonical v-prefixed semantic version")
|
||||||
|
}
|
||||||
|
if !gitObjectPattern.MatchString(identity.Commit) || !gitObjectPattern.MatchString(identity.Tree) {
|
||||||
|
return errors.New("evidence commit and tree must be full lowercase Git object identities")
|
||||||
|
}
|
||||||
|
if !requireReview {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(identity.ReviewedBy) == "" || strings.ContainsAny(identity.ReviewedBy, "\r\n\x00") {
|
||||||
|
return errors.New("evidence reviewer is required and must be one line")
|
||||||
|
}
|
||||||
|
reviewedAt, err := time.Parse(time.RFC3339, identity.ReviewedAt)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("evidence review time must be RFC3339: %w", err)
|
||||||
|
}
|
||||||
|
if reviewedAt.After(time.Now().UTC().Add(5 * time.Minute)) {
|
||||||
|
return errors.New("evidence review time cannot be in the future")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireJSONEOF(decoder *json.Decoder) error {
|
||||||
|
var extra any
|
||||||
|
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
|
||||||
|
if err == nil {
|
||||||
|
return errors.New("multiple JSON values are not allowed")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
package releaseartifact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEvidenceManifestBindsReviewedContentAndIdentity(t *testing.T) {
|
||||||
|
directory := evidenceFixture(t)
|
||||||
|
identity := evidenceIdentity()
|
||||||
|
checksum, err := WriteEvidenceManifest(directory, identity)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !digestPattern.MatchString(checksum) {
|
||||||
|
t.Fatalf("manifest checksum = %q", checksum)
|
||||||
|
}
|
||||||
|
if err := VerifyEvidenceManifest(directory, identity); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(directory, requiredEvidenceFiles[0])
|
||||||
|
if err := os.WriteFile(path, []byte("# Changed after review\n"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := VerifyEvidenceManifest(directory, identity); err == nil {
|
||||||
|
t.Fatal("changed evidence bytes were accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvidenceManifestRejectsCandidateMismatchAndUnknownFields(t *testing.T) {
|
||||||
|
t.Run("candidate identity", func(t *testing.T) {
|
||||||
|
directory := evidenceFixture(t)
|
||||||
|
identity := evidenceIdentity()
|
||||||
|
if _, err := WriteEvidenceManifest(directory, identity); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
identity.Commit = strings.Repeat("d", 40)
|
||||||
|
if err := VerifyEvidenceManifest(directory, identity); err == nil {
|
||||||
|
t.Fatal("mismatched candidate was accepted")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unknown manifest field", func(t *testing.T) {
|
||||||
|
directory := evidenceFixture(t)
|
||||||
|
identity := evidenceIdentity()
|
||||||
|
if _, err := WriteEvidenceManifest(directory, identity); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
path := filepath.Join(directory, evidenceManifestName)
|
||||||
|
contents, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
contents = []byte(strings.Replace(string(contents), `"files":`, `"unexpected": true, "files":`, 1))
|
||||||
|
if err := os.Chmod(path, 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, contents, 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := VerifyEvidenceManifest(directory, identity); err == nil {
|
||||||
|
t.Fatal("unknown manifest field was accepted")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvidenceManifestRejectsSymlinkAndPlaceholderDocuments(t *testing.T) {
|
||||||
|
t.Run("symlink", func(t *testing.T) {
|
||||||
|
directory := evidenceFixture(t)
|
||||||
|
name := requiredEvidenceFiles[0]
|
||||||
|
path := filepath.Join(directory, name)
|
||||||
|
target := filepath.Join(directory, "target.md")
|
||||||
|
if err := os.WriteFile(target, []byte("# Target\n"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Remove(path); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Symlink(target, path); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := WriteEvidenceManifest(directory, evidenceIdentity()); err == nil {
|
||||||
|
t.Fatal("symlinked evidence was accepted")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("placeholder", func(t *testing.T) {
|
||||||
|
directory := evidenceFixture(t)
|
||||||
|
if err := os.WriteFile(filepath.Join(directory, requiredEvidenceFiles[0]), []byte("not reviewed\n"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := WriteEvidenceManifest(directory, evidenceIdentity()); err == nil {
|
||||||
|
t.Fatal("heading-free placeholder was accepted")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func evidenceFixture(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
directory := t.TempDir()
|
||||||
|
for _, name := range requiredEvidenceFiles {
|
||||||
|
if err := os.WriteFile(filepath.Join(directory, name), []byte("# Reviewed "+name+"\n\nExact bounded evidence.\n"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return directory
|
||||||
|
}
|
||||||
|
|
||||||
|
func evidenceIdentity() EvidenceIdentity {
|
||||||
|
return EvidenceIdentity{
|
||||||
|
Repository: "gamertan/sandwich-hime",
|
||||||
|
Version: "v1.0.0-rc.1",
|
||||||
|
Commit: strings.Repeat("a", 40),
|
||||||
|
Tree: strings.Repeat("b", 40),
|
||||||
|
ReviewedBy: "release operator",
|
||||||
|
ReviewedAt: "2026-08-23T18:00:00Z",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,364 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
package releaseartifact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maximumNativeArchiveSize = 256 << 20
|
||||||
|
|
||||||
|
var unsignedPackageMembers = map[string]os.FileMode{
|
||||||
|
"BUILDINFO.json": 0o444,
|
||||||
|
"LICENSE": 0o444,
|
||||||
|
"RELEASE.md": 0o444,
|
||||||
|
"SBOM.spdx.json": 0o444,
|
||||||
|
"SHA256SUMS": 0o444,
|
||||||
|
"himesan": 0o555,
|
||||||
|
}
|
||||||
|
|
||||||
|
// MacOSSigningOptions are the reviewed identities added after Developer ID
|
||||||
|
// signing changes the native binary bytes.
|
||||||
|
type MacOSSigningOptions struct {
|
||||||
|
Directory string
|
||||||
|
UnsignedArchiveSHA256 string
|
||||||
|
Identity string
|
||||||
|
Identifier string
|
||||||
|
FinalizedAt string
|
||||||
|
}
|
||||||
|
|
||||||
|
type signingInfo struct {
|
||||||
|
SchemaVersion int `json:"schema_version"`
|
||||||
|
UnsignedArchiveSHA256 string `json:"unsigned_archive_sha256"`
|
||||||
|
UnsignedBinarySHA256 string `json:"unsigned_binary_sha256"`
|
||||||
|
SignedBinarySHA256 string `json:"signed_binary_sha256"`
|
||||||
|
Identity string `json:"identity"`
|
||||||
|
Identifier string `json:"identifier"`
|
||||||
|
FinalizedAt string `json:"finalized_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractVerifiedMacOSPackage verifies the approved unsigned archive and
|
||||||
|
// extracts its fixed file set without delegating path handling to system tar.
|
||||||
|
func ExtractVerifiedMacOSPackage(archivePath, expectedSHA256, outputDirectory string) (string, error) {
|
||||||
|
if !digestPattern.MatchString(expectedSHA256) {
|
||||||
|
return "", errors.New("approved archive digest must be a lowercase SHA-256")
|
||||||
|
}
|
||||||
|
info, err := os.Lstat(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("inspect unsigned archive: %w", err)
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
return "", errors.New("unsigned archive must be a regular file, not a symlink")
|
||||||
|
}
|
||||||
|
if info.Size() <= 0 || info.Size() > maximumNativeArchiveSize {
|
||||||
|
return "", errors.New("unsigned archive size is outside the permitted range")
|
||||||
|
}
|
||||||
|
archive, err := os.ReadFile(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("read unsigned archive: %w", err)
|
||||||
|
}
|
||||||
|
if digest(archive) != expectedSHA256 {
|
||||||
|
return "", errors.New("unsigned archive does not match the approved digest")
|
||||||
|
}
|
||||||
|
members, root, err := readUnsignedPackage(archive)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := validateUnsignedPackage(members, root); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
outputInfo, err := os.Lstat(outputDirectory)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("inspect extraction directory: %w", err)
|
||||||
|
}
|
||||||
|
if !outputInfo.IsDir() || outputInfo.Mode()&os.ModeSymlink != 0 {
|
||||||
|
return "", errors.New("extraction destination must be a real directory")
|
||||||
|
}
|
||||||
|
rootPath := filepath.Join(outputDirectory, root)
|
||||||
|
if err := os.Mkdir(rootPath, 0o700); err != nil {
|
||||||
|
return "", fmt.Errorf("create extracted package root: %w", err)
|
||||||
|
}
|
||||||
|
success := false
|
||||||
|
defer func() {
|
||||||
|
if !success {
|
||||||
|
_ = os.RemoveAll(rootPath)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for name, mode := range unsignedPackageMembers {
|
||||||
|
if err := writeExclusive(filepath.Join(rootPath, name), members[name], mode); err != nil {
|
||||||
|
return "", fmt.Errorf("extract %s: %w", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
success = true
|
||||||
|
return rootPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FinalizeSignedMacOSDistribution replaces unsigned binary provenance with a
|
||||||
|
// complete signed-distribution record and regenerates every internal checksum.
|
||||||
|
func FinalizeSignedMacOSDistribution(options MacOSSigningOptions) error {
|
||||||
|
if !digestPattern.MatchString(options.UnsignedArchiveSHA256) {
|
||||||
|
return errors.New("unsigned archive digest must be a lowercase SHA-256")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(options.Identity) == "" || strings.ContainsAny(options.Identity, "\r\n\x00") {
|
||||||
|
return errors.New("signing identity is required and must be one line")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(options.Identifier) == "" || strings.ContainsAny(options.Identifier, "\r\n\x00") {
|
||||||
|
return errors.New("signing identifier is required and must be one line")
|
||||||
|
}
|
||||||
|
if _, err := time.Parse(time.RFC3339, options.FinalizedAt); err != nil {
|
||||||
|
return fmt.Errorf("finalization time must be RFC3339: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name := range unsignedPackageMembers {
|
||||||
|
if _, err := readRegularFile(filepath.Join(options.Directory, name), maximumNativeArchiveSize); err != nil {
|
||||||
|
return fmt.Errorf("inspect signed distribution %s: %w", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildPath := filepath.Join(options.Directory, "BUILDINFO.json")
|
||||||
|
buildContents, err := os.ReadFile(buildPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read unsigned build information: %w", err)
|
||||||
|
}
|
||||||
|
var build buildInfo
|
||||||
|
if err := decodeStrictJSON(buildContents, &build); err != nil {
|
||||||
|
return fmt.Errorf("decode unsigned build information: %w", err)
|
||||||
|
}
|
||||||
|
if build.SchemaVersion != 1 || !digestPattern.MatchString(build.BinarySHA256) || build.GOOS != "darwin" || build.GOARCH != "arm64" {
|
||||||
|
return errors.New("unsigned build information is not a supported Darwin/arm64 package")
|
||||||
|
}
|
||||||
|
unsignedBinaryDigest := build.BinarySHA256
|
||||||
|
signedBinary, err := os.ReadFile(filepath.Join(options.Directory, "himesan"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read signed binary: %w", err)
|
||||||
|
}
|
||||||
|
signedBinaryDigest := digest(signedBinary)
|
||||||
|
if signedBinaryDigest == unsignedBinaryDigest {
|
||||||
|
return errors.New("Developer ID signing did not change the native binary bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
build.SchemaVersion = 2
|
||||||
|
build.BinarySHA256 = signedBinaryDigest
|
||||||
|
build.UnsignedBinarySHA256 = unsignedBinaryDigest
|
||||||
|
build.UnsignedArchiveSHA256 = options.UnsignedArchiveSHA256
|
||||||
|
build.SigningIdentifier = options.Identifier
|
||||||
|
newBuild, err := indentedJSON(build)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sbomPath := filepath.Join(options.Directory, "SBOM.spdx.json")
|
||||||
|
sbomContents, err := os.ReadFile(sbomPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read unsigned SBOM: %w", err)
|
||||||
|
}
|
||||||
|
var sbom spdxDocument
|
||||||
|
if err := decodeStrictJSON(sbomContents, &sbom); err != nil {
|
||||||
|
return fmt.Errorf("decode unsigned SBOM: %w", err)
|
||||||
|
}
|
||||||
|
if len(sbom.Packages) != 1 || len(sbom.Packages[0].Checksums) != 1 ||
|
||||||
|
sbom.Packages[0].Checksums[0].Algorithm != "SHA256" ||
|
||||||
|
sbom.Packages[0].Checksums[0].ChecksumValue != unsignedBinaryDigest {
|
||||||
|
return errors.New("unsigned SBOM does not match the unsigned binary")
|
||||||
|
}
|
||||||
|
sbom.Packages[0].Checksums[0].ChecksumValue = signedBinaryDigest
|
||||||
|
newSBOM, err := indentedJSON(sbom)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
signingContents, err := indentedJSON(signingInfo{
|
||||||
|
SchemaVersion: 1,
|
||||||
|
UnsignedArchiveSHA256: options.UnsignedArchiveSHA256,
|
||||||
|
UnsignedBinarySHA256: unsignedBinaryDigest,
|
||||||
|
SignedBinarySHA256: signedBinaryDigest,
|
||||||
|
Identity: options.Identity,
|
||||||
|
Identifier: options.Identifier,
|
||||||
|
FinalizedAt: options.FinalizedAt,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := replaceRegularFile(buildPath, newBuild, 0o444); err != nil {
|
||||||
|
return fmt.Errorf("replace signed build information: %w", err)
|
||||||
|
}
|
||||||
|
if err := replaceRegularFile(sbomPath, newSBOM, 0o444); err != nil {
|
||||||
|
return fmt.Errorf("replace signed SBOM: %w", err)
|
||||||
|
}
|
||||||
|
if err := writeExclusive(filepath.Join(options.Directory, "SIGNING.json"), signingContents, 0o444); err != nil {
|
||||||
|
return fmt.Errorf("write signing information: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checksumNames := []string{"BUILDINFO.json", "LICENSE", "RELEASE.md", "SBOM.spdx.json", "SIGNING.json", "himesan"}
|
||||||
|
var checksumLines []string
|
||||||
|
for _, name := range checksumNames {
|
||||||
|
contents, err := os.ReadFile(filepath.Join(options.Directory, name))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read signed distribution member %s: %w", name, err)
|
||||||
|
}
|
||||||
|
checksumLines = append(checksumLines, digest(contents)+" "+name)
|
||||||
|
}
|
||||||
|
checksums := []byte(strings.Join(checksumLines, "\n") + "\n")
|
||||||
|
if err := replaceRegularFile(filepath.Join(options.Directory, "SHA256SUMS"), checksums, 0o444); err != nil {
|
||||||
|
return fmt.Errorf("replace signed distribution checksums: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readUnsignedPackage(archive []byte) (map[string][]byte, string, error) {
|
||||||
|
gzipReader, err := gzip.NewReader(bytes.NewReader(archive))
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("open unsigned gzip archive: %w", err)
|
||||||
|
}
|
||||||
|
defer gzipReader.Close()
|
||||||
|
tarReader := tar.NewReader(io.LimitReader(gzipReader, maximumNativeArchiveSize+1))
|
||||||
|
members := make(map[string][]byte, len(unsignedPackageMembers))
|
||||||
|
root := ""
|
||||||
|
var totalSize int64
|
||||||
|
for {
|
||||||
|
header, err := tarReader.Next()
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("read unsigned tar archive: %w", err)
|
||||||
|
}
|
||||||
|
if header.Typeflag != tar.TypeReg || header.Size < 0 || header.Size > maximumNativeArchiveSize {
|
||||||
|
return nil, "", errors.New("unsigned archive contains a non-regular or oversized member")
|
||||||
|
}
|
||||||
|
totalSize += header.Size
|
||||||
|
if totalSize > maximumNativeArchiveSize {
|
||||||
|
return nil, "", errors.New("unsigned archive expands beyond the permitted size")
|
||||||
|
}
|
||||||
|
clean := filepath.ToSlash(filepath.Clean(header.Name))
|
||||||
|
parts := strings.Split(clean, "/")
|
||||||
|
if len(parts) != 2 || parts[0] == "" || parts[0] == "." || parts[0] == ".." {
|
||||||
|
return nil, "", fmt.Errorf("unsigned archive member has an unsafe path: %q", header.Name)
|
||||||
|
}
|
||||||
|
if root == "" {
|
||||||
|
root = parts[0]
|
||||||
|
} else if root != parts[0] {
|
||||||
|
return nil, "", errors.New("unsigned archive contains more than one package root")
|
||||||
|
}
|
||||||
|
name := parts[1]
|
||||||
|
expectedMode, ok := unsignedPackageMembers[name]
|
||||||
|
if !ok || os.FileMode(header.Mode).Perm() != expectedMode || header.Linkname != "" {
|
||||||
|
return nil, "", fmt.Errorf("unsigned archive member %q has an unexpected name, mode, or link", name)
|
||||||
|
}
|
||||||
|
if _, exists := members[name]; exists {
|
||||||
|
return nil, "", fmt.Errorf("unsigned archive repeats member %q", name)
|
||||||
|
}
|
||||||
|
contents, err := io.ReadAll(io.LimitReader(tarReader, header.Size+1))
|
||||||
|
if err != nil || int64(len(contents)) != header.Size {
|
||||||
|
return nil, "", fmt.Errorf("read unsigned archive member %q", name)
|
||||||
|
}
|
||||||
|
members[name] = contents
|
||||||
|
}
|
||||||
|
if err := gzipReader.Close(); err != nil {
|
||||||
|
return nil, "", fmt.Errorf("finish unsigned gzip archive: %w", err)
|
||||||
|
}
|
||||||
|
return members, root, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateUnsignedPackage(members map[string][]byte, root string) error {
|
||||||
|
if len(members) != len(unsignedPackageMembers) || root == "" {
|
||||||
|
return errors.New("unsigned archive does not contain the exact release file set")
|
||||||
|
}
|
||||||
|
var build buildInfo
|
||||||
|
if err := decodeStrictJSON(members["BUILDINFO.json"], &build); err != nil {
|
||||||
|
return fmt.Errorf("decode unsigned build information: %w", err)
|
||||||
|
}
|
||||||
|
if build.SchemaVersion != 1 || build.GOOS != "darwin" || build.GOARCH != "arm64" ||
|
||||||
|
!releaseVersionPattern.MatchString(build.Version) || root != "himesan-"+strings.TrimPrefix(build.Version, "v")+"-darwin-arm64" ||
|
||||||
|
!gitObjectPattern.MatchString(build.Commit) || !gitObjectPattern.MatchString(build.Tree) ||
|
||||||
|
!goVersionPattern.MatchString(build.GoVersion) || build.BinarySHA256 != digest(members["himesan"]) {
|
||||||
|
return errors.New("unsigned build information does not match the archive")
|
||||||
|
}
|
||||||
|
var sbom spdxDocument
|
||||||
|
if err := decodeStrictJSON(members["SBOM.spdx.json"], &sbom); err != nil {
|
||||||
|
return fmt.Errorf("decode unsigned SBOM: %w", err)
|
||||||
|
}
|
||||||
|
if len(sbom.Packages) != 1 || len(sbom.Packages[0].Checksums) != 1 ||
|
||||||
|
sbom.Packages[0].Checksums[0].Algorithm != "SHA256" ||
|
||||||
|
sbom.Packages[0].Checksums[0].ChecksumValue != build.BinarySHA256 {
|
||||||
|
return errors.New("unsigned SBOM does not match the native binary")
|
||||||
|
}
|
||||||
|
expectedNames := []string{"BUILDINFO.json", "LICENSE", "RELEASE.md", "SBOM.spdx.json", "himesan"}
|
||||||
|
var expectedLines []string
|
||||||
|
for _, name := range expectedNames {
|
||||||
|
expectedLines = append(expectedLines, digest(members[name])+" "+name)
|
||||||
|
}
|
||||||
|
if string(members["SHA256SUMS"]) != strings.Join(expectedLines, "\n")+"\n" {
|
||||||
|
return errors.New("unsigned package checksum manifest does not match its members")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readRegularFile(path string, maximum int64) ([]byte, error) {
|
||||||
|
info, err := os.Lstat(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 || info.Size() < 0 || info.Size() > maximum {
|
||||||
|
return nil, errors.New("must be a bounded regular file, not a symlink")
|
||||||
|
}
|
||||||
|
return os.ReadFile(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeStrictJSON(contents []byte, target any) error {
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(contents))
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
if err := decoder.Decode(target); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return requireJSONEOF(decoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceRegularFile(path string, contents []byte, mode os.FileMode) error {
|
||||||
|
info, err := os.Lstat(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
return errors.New("replacement target must be a regular file, not a symlink")
|
||||||
|
}
|
||||||
|
temporary, err := os.CreateTemp(filepath.Dir(path), ".himesan-release-*")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
temporaryPath := temporary.Name()
|
||||||
|
success := false
|
||||||
|
defer func() {
|
||||||
|
_ = temporary.Close()
|
||||||
|
if !success {
|
||||||
|
_ = os.Remove(temporaryPath)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if _, err := temporary.Write(contents); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := temporary.Sync(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := temporary.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.Chmod(temporaryPath, mode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.Rename(temporaryPath, path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
success = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
package releaseartifact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExtractAndFinalizeSignedMacOSDistribution(t *testing.T) {
|
||||||
|
archive, archiveDigest := macOSPackageFixture(t)
|
||||||
|
extraction := t.TempDir()
|
||||||
|
root, err := ExtractVerifiedMacOSPackage(archive, archiveDigest, extraction)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
binaryPath := filepath.Join(root, "himesan")
|
||||||
|
unsignedBinary, err := os.ReadFile(binaryPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Chmod(binaryPath, 0o700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
binary, err := os.OpenFile(binaryPath, os.O_APPEND|os.O_WRONLY, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := binary.WriteString("developer-id-signature"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := binary.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
options := MacOSSigningOptions{
|
||||||
|
Directory: root, UnsignedArchiveSHA256: archiveDigest,
|
||||||
|
Identity: "Developer ID Application: Example (TEAMID)",
|
||||||
|
Identifier: "com.gamertan.sandwich-hime.himesan",
|
||||||
|
FinalizedAt: "2026-08-23T18:00:00Z",
|
||||||
|
}
|
||||||
|
if err := FinalizeSignedMacOSDistribution(options); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var build buildInfo
|
||||||
|
buildContents, err := os.ReadFile(filepath.Join(root, "BUILDINFO.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := decodeStrictJSON(buildContents, &build); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
signedBinary, err := os.ReadFile(binaryPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if build.SchemaVersion != 2 || build.BinarySHA256 != digest(signedBinary) ||
|
||||||
|
build.UnsignedBinarySHA256 != digest(unsignedBinary) || build.UnsignedArchiveSHA256 != archiveDigest ||
|
||||||
|
build.SigningIdentifier != options.Identifier {
|
||||||
|
t.Fatalf("signed build information is incomplete: %#v", build)
|
||||||
|
}
|
||||||
|
|
||||||
|
var signing signingInfo
|
||||||
|
signingContents, err := os.ReadFile(filepath.Join(root, "SIGNING.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := decodeStrictJSON(signingContents, &signing); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if signing.SignedBinarySHA256 != build.BinarySHA256 || signing.UnsignedBinarySHA256 != build.UnsignedBinarySHA256 {
|
||||||
|
t.Fatalf("signing record does not match build information: %#v", signing)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sbom spdxDocument
|
||||||
|
sbomContents, err := os.ReadFile(filepath.Join(root, "SBOM.spdx.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := decodeStrictJSON(sbomContents, &sbom); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := sbom.Packages[0].Checksums[0].ChecksumValue; got != build.BinarySHA256 {
|
||||||
|
t.Fatalf("signed SBOM checksum = %s, want %s", got, build.BinarySHA256)
|
||||||
|
}
|
||||||
|
assertChecksumManifest(t, root, []string{"BUILDINFO.json", "LICENSE", "RELEASE.md", "SBOM.spdx.json", "SIGNING.json", "himesan"})
|
||||||
|
if err := FinalizeSignedMacOSDistribution(options); err == nil {
|
||||||
|
t.Fatal("signed distribution was finalized twice")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractVerifiedMacOSPackageRejectsSubstitutionAndSymlink(t *testing.T) {
|
||||||
|
archive, archiveDigest := macOSPackageFixture(t)
|
||||||
|
if _, err := ExtractVerifiedMacOSPackage(archive, strings.Repeat("0", 64), t.TempDir()); err == nil {
|
||||||
|
t.Fatal("archive substitution was accepted")
|
||||||
|
}
|
||||||
|
symlink := filepath.Join(t.TempDir(), "candidate.tar.gz")
|
||||||
|
if err := os.Symlink(archive, symlink); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := ExtractVerifiedMacOSPackage(symlink, archiveDigest, t.TempDir()); err == nil {
|
||||||
|
t.Fatal("symlinked archive was accepted")
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafeArchive := filepath.Join(t.TempDir(), "unsafe.tar.gz")
|
||||||
|
destination := t.TempDir()
|
||||||
|
escapeName := filepath.Base(destination) + "-outside"
|
||||||
|
unsafeContents := unsafeTarGzip(t, "../"+escapeName, []byte("not a package"))
|
||||||
|
if err := os.WriteFile(unsafeArchive, unsafeContents, 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := ExtractVerifiedMacOSPackage(unsafeArchive, digest(unsafeContents), destination); err == nil {
|
||||||
|
t.Fatal("archive path traversal was accepted")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(filepath.Dir(destination), escapeName)); !os.IsNotExist(err) {
|
||||||
|
t.Fatal("unsafe archive wrote outside the extraction directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func macOSPackageFixture(t *testing.T) (string, string) {
|
||||||
|
t.Helper()
|
||||||
|
directory := t.TempDir()
|
||||||
|
options := PackageOptions{
|
||||||
|
Version: "v1.0.0-rc.1", Commit: strings.Repeat("a", 40), Tree: strings.Repeat("b", 40),
|
||||||
|
GoVersion: "go1.27.0", GOOS: "darwin", GOARCH: "arm64",
|
||||||
|
BinaryPath: writeFixture(t, directory, "himesan", "unsigned Mach-O fixture"),
|
||||||
|
LicensePath: writeFixture(t, directory, "LICENSE", "licence"),
|
||||||
|
ReleaseNotes: writeFixture(t, directory, "RELEASE.md", "release"),
|
||||||
|
OutputDirectory: filepath.Join(directory, "package"), SourceDateEpoch: 1_700_000_000,
|
||||||
|
}
|
||||||
|
result, err := Package(options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return result.ArchivePath, result.SHA256
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertChecksumManifest(t *testing.T, directory string, names []string) {
|
||||||
|
t.Helper()
|
||||||
|
var lines []string
|
||||||
|
for _, name := range names {
|
||||||
|
contents, err := os.ReadFile(filepath.Join(directory, name))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
lines = append(lines, digest(contents)+" "+name)
|
||||||
|
}
|
||||||
|
checksums, err := os.ReadFile(filepath.Join(directory, "SHA256SUMS"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got, want := string(checksums), strings.Join(lines, "\n")+"\n"; got != want {
|
||||||
|
t.Fatalf("checksum manifest mismatch\ngot:\n%s\nwant:\n%s", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unsafeTarGzip(t *testing.T, name string, contents []byte) []byte {
|
||||||
|
t.Helper()
|
||||||
|
var output bytes.Buffer
|
||||||
|
gzipWriter := gzip.NewWriter(&output)
|
||||||
|
tarWriter := tar.NewWriter(gzipWriter)
|
||||||
|
if err := tarWriter.WriteHeader(&tar.Header{Name: name, Mode: 0o444, Size: int64(len(contents)), Typeflag: tar.TypeReg}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := tarWriter.Write(contents); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := tarWriter.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := gzipWriter.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return output.Bytes()
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
package releaseartifact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const nativeReceiptMaximumAge = 30 * 24 * time.Hour
|
||||||
|
|
||||||
|
var runnerVersionPattern = regexp.MustCompile(`^v(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$`)
|
||||||
|
|
||||||
|
type nativeLane struct {
|
||||||
|
directory string
|
||||||
|
goos string
|
||||||
|
goarch string
|
||||||
|
goVersion string
|
||||||
|
runner string
|
||||||
|
}
|
||||||
|
|
||||||
|
var requiredNativeLanes = []nativeLane{
|
||||||
|
{directory: "darwin-arm64-go1.26.7", goos: "darwin", goarch: "arm64", goVersion: "go1.26.7", runner: "macbook-air-himesan-darwin-arm64"},
|
||||||
|
{directory: "darwin-arm64-go1.27.0", goos: "darwin", goarch: "arm64", goVersion: "go1.27.0", runner: "macbook-air-himesan-darwin-arm64"},
|
||||||
|
{directory: "linux-amd64-go1.26.7", goos: "linux", goarch: "amd64", goVersion: "go1.26.7", runner: "cliff-himesan-linux-amd64"},
|
||||||
|
{directory: "linux-amd64-go1.27.0", goos: "linux", goarch: "amd64", goVersion: "go1.27.0", runner: "cliff-himesan-linux-amd64"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var requiredNativeGates = []string{
|
||||||
|
"build", "consumer", "contracts", "fuzz", "generation", "package",
|
||||||
|
"public-snapshot", "race", "test", "vet", "vulnerability",
|
||||||
|
}
|
||||||
|
|
||||||
|
// NativeReceiptExpectation identifies the exact source verified by all native
|
||||||
|
// runner lanes.
|
||||||
|
type NativeReceiptExpectation struct {
|
||||||
|
Repository string
|
||||||
|
Commit string
|
||||||
|
Tree string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NativeReceiptSummary is safe to include in human release evidence.
|
||||||
|
type NativeReceiptSummary struct {
|
||||||
|
SchemaVersion string `json:"schema_version"`
|
||||||
|
Repository string `json:"repository"`
|
||||||
|
Commit string `json:"commit"`
|
||||||
|
Tree string `json:"tree"`
|
||||||
|
GeneratedDigest string `json:"generated_output_sha256"`
|
||||||
|
Lanes []NativeLaneSummary `json:"lanes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NativeLaneSummary records one verified native receipt without local paths.
|
||||||
|
type NativeLaneSummary struct {
|
||||||
|
Platform string `json:"platform"`
|
||||||
|
GoVersion string `json:"go_version"`
|
||||||
|
RunnerVersion string `json:"runner_version"`
|
||||||
|
ArtifactSHA256 string `json:"unsigned_artifact_sha256"`
|
||||||
|
ReceiptSHA256 string `json:"receipt_sha256"`
|
||||||
|
CompletedAt string `json:"completed_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyNativeReceiptSet requires all four maintained target/toolchain lanes,
|
||||||
|
// validates their checksum sidecars, and proves identical generated output.
|
||||||
|
func VerifyNativeReceiptSet(directory string, expected NativeReceiptExpectation) (NativeReceiptSummary, error) {
|
||||||
|
if strings.TrimSpace(expected.Repository) == "" || strings.ContainsAny(expected.Repository, "\r\n\x00") {
|
||||||
|
return NativeReceiptSummary{}, errors.New("native receipt repository is required and must be one line")
|
||||||
|
}
|
||||||
|
if !gitObjectPattern.MatchString(expected.Commit) || !gitObjectPattern.MatchString(expected.Tree) {
|
||||||
|
return NativeReceiptSummary{}, errors.New("native receipt commit and tree must be full lowercase Git object identities")
|
||||||
|
}
|
||||||
|
if err := validateNativeDirectory(directory); err != nil {
|
||||||
|
return NativeReceiptSummary{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := NativeReceiptSummary{
|
||||||
|
SchemaVersion: "himesan.native-receipt-set.v1",
|
||||||
|
Repository: expected.Repository, Commit: expected.Commit, Tree: expected.Tree,
|
||||||
|
}
|
||||||
|
for _, lane := range requiredNativeLanes {
|
||||||
|
receipt, receiptDigest, err := readNativeLane(filepath.Join(directory, lane.directory))
|
||||||
|
if err != nil {
|
||||||
|
return NativeReceiptSummary{}, fmt.Errorf("verify native lane %s: %w", lane.directory, err)
|
||||||
|
}
|
||||||
|
if receipt.SchemaVersion != receiptSchema || receipt.Repository != expected.Repository ||
|
||||||
|
receipt.Commit != expected.Commit || receipt.Tree != expected.Tree ||
|
||||||
|
receipt.GOOS != lane.goos || receipt.GOARCH != lane.goarch ||
|
||||||
|
receipt.GoVersion != lane.goVersion || receipt.RunnerName != lane.runner {
|
||||||
|
return NativeReceiptSummary{}, fmt.Errorf("native lane %s does not match its source, platform, toolchain, or runner", lane.directory)
|
||||||
|
}
|
||||||
|
if !runnerVersionPattern.MatchString(receipt.RunnerVersion) || !digestPattern.MatchString(receipt.UnsignedArtifactSHA) {
|
||||||
|
return NativeReceiptSummary{}, fmt.Errorf("native lane %s has an invalid runner or artifact identity", lane.directory)
|
||||||
|
}
|
||||||
|
completedAt, err := time.Parse(time.RFC3339, receipt.CompletedAt)
|
||||||
|
if err != nil || completedAt.After(time.Now().UTC().Add(5*time.Minute)) || time.Since(completedAt) > nativeReceiptMaximumAge {
|
||||||
|
return NativeReceiptSummary{}, fmt.Errorf("native lane %s is not fresh, completed, RFC3339 evidence", lane.directory)
|
||||||
|
}
|
||||||
|
if err := requireNativeGates(receipt.SuccessfulGates); err != nil {
|
||||||
|
return NativeReceiptSummary{}, fmt.Errorf("native lane %s: %w", lane.directory, err)
|
||||||
|
}
|
||||||
|
if summary.GeneratedDigest == "" {
|
||||||
|
summary.GeneratedDigest = receipt.GeneratedDigest
|
||||||
|
} else if summary.GeneratedDigest != receipt.GeneratedDigest {
|
||||||
|
return NativeReceiptSummary{}, errors.New("native lanes did not produce identical generated output")
|
||||||
|
}
|
||||||
|
summary.Lanes = append(summary.Lanes, NativeLaneSummary{
|
||||||
|
Platform: lane.goos + "/" + lane.goarch, GoVersion: lane.goVersion,
|
||||||
|
RunnerVersion: receipt.RunnerVersion, ArtifactSHA256: receipt.UnsignedArtifactSHA,
|
||||||
|
ReceiptSHA256: receiptDigest, CompletedAt: receipt.CompletedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return summary, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateNativeDirectory(directory string) error {
|
||||||
|
info, err := os.Lstat(directory)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect native evidence directory: %w", err)
|
||||||
|
}
|
||||||
|
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
return errors.New("native evidence must be a real directory")
|
||||||
|
}
|
||||||
|
entries, err := os.ReadDir(directory)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read native evidence directory: %w", err)
|
||||||
|
}
|
||||||
|
expected := make([]string, 0, len(requiredNativeLanes))
|
||||||
|
for _, lane := range requiredNativeLanes {
|
||||||
|
expected = append(expected, lane.directory)
|
||||||
|
}
|
||||||
|
observed := make([]string, 0, len(entries))
|
||||||
|
for _, entry := range entries {
|
||||||
|
observed = append(observed, entry.Name())
|
||||||
|
}
|
||||||
|
sort.Strings(expected)
|
||||||
|
sort.Strings(observed)
|
||||||
|
if !equalStrings(expected, observed) {
|
||||||
|
return fmt.Errorf("native evidence directories = %v, want exactly %v", observed, expected)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readNativeLane(directory string) (Receipt, string, error) {
|
||||||
|
info, err := os.Lstat(directory)
|
||||||
|
if err != nil {
|
||||||
|
return Receipt{}, "", err
|
||||||
|
}
|
||||||
|
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
return Receipt{}, "", errors.New("lane must be a real directory")
|
||||||
|
}
|
||||||
|
entries, err := os.ReadDir(directory)
|
||||||
|
if err != nil {
|
||||||
|
return Receipt{}, "", err
|
||||||
|
}
|
||||||
|
if len(entries) != 2 || entries[0].Name() != "TEND-CI-VERIFICATION.json" || entries[1].Name() != "TEND-CI-VERIFICATION.json.sha256" {
|
||||||
|
return Receipt{}, "", errors.New("lane must contain only its receipt and checksum sidecar")
|
||||||
|
}
|
||||||
|
receiptPath := filepath.Join(directory, "TEND-CI-VERIFICATION.json")
|
||||||
|
contents, err := readRegularFile(receiptPath, maximumEvidenceSize)
|
||||||
|
if err != nil || len(contents) == 0 {
|
||||||
|
return Receipt{}, "", errors.New("receipt must be a non-empty bounded regular file")
|
||||||
|
}
|
||||||
|
receiptDigest := digest(contents)
|
||||||
|
sidecar, err := readRegularFile(receiptPath+".sha256", 512)
|
||||||
|
if err != nil {
|
||||||
|
return Receipt{}, "", fmt.Errorf("read receipt checksum: %w", err)
|
||||||
|
}
|
||||||
|
expectedSidecar := receiptDigest + " TEND-CI-VERIFICATION.json\n"
|
||||||
|
if string(sidecar) != expectedSidecar {
|
||||||
|
return Receipt{}, "", errors.New("receipt checksum sidecar does not match")
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(contents))
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
var receipt Receipt
|
||||||
|
if err := decoder.Decode(&receipt); err != nil {
|
||||||
|
return Receipt{}, "", fmt.Errorf("decode receipt: %w", err)
|
||||||
|
}
|
||||||
|
if err := requireJSONEOF(decoder); err != nil {
|
||||||
|
return Receipt{}, "", fmt.Errorf("decode receipt: %w", err)
|
||||||
|
}
|
||||||
|
if err := validateReceipt(receipt); err != nil {
|
||||||
|
return Receipt{}, "", err
|
||||||
|
}
|
||||||
|
return receipt, receiptDigest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireNativeGates(gates []string) error {
|
||||||
|
observed := make(map[string]bool, len(gates))
|
||||||
|
for _, gate := range gates {
|
||||||
|
if observed[gate] {
|
||||||
|
return fmt.Errorf("successful gate %q is duplicated", gate)
|
||||||
|
}
|
||||||
|
observed[gate] = true
|
||||||
|
}
|
||||||
|
for _, gate := range requiredNativeGates {
|
||||||
|
if !observed[gate] {
|
||||||
|
return fmt.Errorf("required successful gate %q is missing", gate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func equalStrings(left, right []string) bool {
|
||||||
|
if len(left) != len(right) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for index := range left {
|
||||||
|
if left[index] != right[index] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
package releaseartifact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVerifyNativeReceiptSetRequiresEveryLaneAndMatchingGeneratedOutput(t *testing.T) {
|
||||||
|
expected := NativeReceiptExpectation{
|
||||||
|
Repository: "gamertan/sandwich-hime",
|
||||||
|
Commit: strings.Repeat("a", 40), Tree: strings.Repeat("b", 40),
|
||||||
|
}
|
||||||
|
directory := nativeReceiptFixture(t, expected, strings.Repeat("c", 64))
|
||||||
|
summary, err := VerifyNativeReceiptSet(directory, expected)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if summary.GeneratedDigest != strings.Repeat("c", 64) || len(summary.Lanes) != 4 {
|
||||||
|
t.Fatalf("native summary is incomplete: %#v", summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("source mismatch", func(t *testing.T) {
|
||||||
|
mismatch := expected
|
||||||
|
mismatch.Commit = strings.Repeat("d", 40)
|
||||||
|
if _, err := VerifyNativeReceiptSet(directory, mismatch); err == nil {
|
||||||
|
t.Fatal("mismatched source commit was accepted")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("generated mismatch", func(t *testing.T) {
|
||||||
|
mismatched := nativeReceiptFixture(t, expected, strings.Repeat("c", 64))
|
||||||
|
lane := requiredNativeLanes[0]
|
||||||
|
laneDirectory := filepath.Join(mismatched, lane.directory)
|
||||||
|
if err := os.RemoveAll(laneDirectory); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
writeNativeLaneFixture(t, laneDirectory, lane, expected, strings.Repeat("e", 64))
|
||||||
|
if _, err := VerifyNativeReceiptSet(mismatched, expected); err == nil {
|
||||||
|
t.Fatal("different generated output was accepted")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyNativeReceiptSetRejectsTamperingAndUnexpectedFiles(t *testing.T) {
|
||||||
|
expected := NativeReceiptExpectation{
|
||||||
|
Repository: "gamertan/sandwich-hime",
|
||||||
|
Commit: strings.Repeat("a", 40), Tree: strings.Repeat("b", 40),
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("checksum", func(t *testing.T) {
|
||||||
|
directory := nativeReceiptFixture(t, expected, strings.Repeat("c", 64))
|
||||||
|
sidecar := filepath.Join(directory, requiredNativeLanes[0].directory, "TEND-CI-VERIFICATION.json.sha256")
|
||||||
|
if err := os.Chmod(sidecar, 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(sidecar, []byte(strings.Repeat("0", 64)+" TEND-CI-VERIFICATION.json\n"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := VerifyNativeReceiptSet(directory, expected); err == nil {
|
||||||
|
t.Fatal("tampered checksum was accepted")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unexpected entry", func(t *testing.T) {
|
||||||
|
directory := nativeReceiptFixture(t, expected, strings.Repeat("c", 64))
|
||||||
|
if err := os.WriteFile(filepath.Join(directory, "notes.txt"), []byte("not a receipt"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := VerifyNativeReceiptSet(directory, expected); err == nil {
|
||||||
|
t.Fatal("unexpected native evidence entry was accepted")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func nativeReceiptFixture(t *testing.T, expected NativeReceiptExpectation, generatedDigest string) string {
|
||||||
|
t.Helper()
|
||||||
|
directory := t.TempDir()
|
||||||
|
for _, lane := range requiredNativeLanes {
|
||||||
|
writeNativeLaneFixture(t, filepath.Join(directory, lane.directory), lane, expected, generatedDigest)
|
||||||
|
}
|
||||||
|
return directory
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeNativeLaneFixture(t *testing.T, directory string, lane nativeLane, expected NativeReceiptExpectation, generatedDigest string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.Mkdir(directory, 0o700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
runnerVersion := "v3.1.0"
|
||||||
|
if lane.goos == "darwin" {
|
||||||
|
runnerVersion = "v3.3.0"
|
||||||
|
}
|
||||||
|
receipt := Receipt{
|
||||||
|
Repository: expected.Repository, Commit: expected.Commit, Tree: expected.Tree,
|
||||||
|
GOOS: lane.goos, GOARCH: lane.goarch, GoVersion: lane.goVersion,
|
||||||
|
RunnerVersion: runnerVersion, RunnerName: lane.runner,
|
||||||
|
GeneratedDigest: generatedDigest, UnsignedArtifactSHA: strings.Repeat("f", 64),
|
||||||
|
SuccessfulGates: append([]string(nil), requiredNativeGates...),
|
||||||
|
CompletedAt: time.Now().UTC().Add(-time.Minute).Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
if _, err := WriteReceipt(filepath.Join(directory, "TEND-CI-VERIFICATION.json"), receipt); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
// Package testpath provides filesystem helpers for tests that exercise
|
||||||
|
// Hime-san's deliberate symlink boundaries.
|
||||||
|
package testpath
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TempDir returns the physical path to a fresh directory owned by the test.
|
||||||
|
//
|
||||||
|
// macOS commonly exposes its temporary directory through /var even though
|
||||||
|
// /var is a root-owned system symlink to /private/var. Resolving a directory
|
||||||
|
// immediately after testing.TB creates it keeps tests portable without
|
||||||
|
// teaching production path validation to follow user-controlled symlinks.
|
||||||
|
func TempDir(t testing.TB) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
directory := t.TempDir()
|
||||||
|
resolved, err := filepath.EvalSymlinks(directory)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolve test temporary directory: %v", err)
|
||||||
|
}
|
||||||
|
resolved = filepath.Clean(resolved)
|
||||||
|
info, err := os.Lstat(resolved)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("inspect resolved test temporary directory: %v", err)
|
||||||
|
}
|
||||||
|
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
t.Fatalf("resolved test temporary directory is not a physical directory: %s", resolved)
|
||||||
|
}
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
package testpath
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTempDirReturnsPhysicalPath(t *testing.T) {
|
||||||
|
directory := TempDir(t)
|
||||||
|
resolved, err := filepath.EvalSymlinks(directory)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if filepath.Clean(resolved) != directory {
|
||||||
|
t.Fatalf("TempDir() = %q, physical path = %q", directory, resolved)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,24 @@
|
|||||||
|
|
||||||
`gamertan.com/sandwich-hime/sando` is the independent Apache-2.0 runtime ABI for Go generated by Sandwich Hime. It contains no compiler, router, HTTP server, middleware, reflection registry, project discovery, or development supervisor.
|
`gamertan.com/sandwich-hime/sando` is the independent Apache-2.0 runtime ABI for Go generated by Sandwich Hime. It contains no compiler, router, HTTP server, middleware, reflection registry, project discovery, or development supervisor.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
This is the source and documentation for runtime `sando/v1.0.0`, paired with
|
||||||
|
compiler `v1.0.0` without changing runtime ABI `sando.v1`. Canonical Gitea tags
|
||||||
|
and release announcements establish availability. In an application module,
|
||||||
|
add the runtime first, then install the compiler:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get gamertan.com/sandwich-hime/sando@v1.0.0
|
||||||
|
go install gamertan.com/sandwich-hime/cmd/himesan@v1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
The runtime-first order avoids parent/nested-module path-selection ambiguity.
|
||||||
|
The compiler is a development tool and is not a runtime dependency of the
|
||||||
|
generated application. Compiler and runtime remain independently versioned.
|
||||||
|
|
||||||
|
## API and licensing
|
||||||
|
|
||||||
The stable surface is:
|
The stable surface is:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -16,4 +34,7 @@ type ComponentFunc func(context.Context, io.Writer) error
|
|||||||
|
|
||||||
`Render` invokes components; generated code uses the context-specific writer helpers and checks `ABI`. Opaque `TrustedHTML`, `TrustedURL`, `TrustedJS`, and `TrustedCSS` values can be constructed only by conspicuous `Trust*` calls in trusted application code.
|
`Render` invokes components; generated code uses the context-specific writer helpers and checks `ABI`. Opaque `TrustedHTML`, `TrustedURL`, `TrustedJS`, and `TrustedCSS` values can be constructed only by conspicuous `Trust*` calls in trusted application code.
|
||||||
|
|
||||||
|
The exact exported v1 symbol, value, method, field, and signature inventory is
|
||||||
|
machine-checked in [`testdata/public-api-v1.txt`](testdata/public-api-v1.txt).
|
||||||
|
|
||||||
The runtime has its own `go.mod`, Apache-2.0 license, `COPYRIGHT` record, semantic version, and `sando/vX.Y.Z` tags. It never imports the AGPL compiler. Importing the runtime does not make an application AGPL under the project's terms. Applications may license their own code and generated files under terms they choose to the extent they hold the necessary rights, while redistribution of the runtime remains subject to Apache-2.0.
|
The runtime has its own `go.mod`, Apache-2.0 license, `COPYRIGHT` record, semantic version, and `sando/vX.Y.Z` tags. It never imports the AGPL compiler. Importing the runtime does not make an application AGPL under the project's terms. Applications may license their own code and generated files under terms they choose to the extent they hold the necessary rights, while redistribution of the runtime remains subject to Apache-2.0.
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sando
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/ast"
|
||||||
|
"go/importer"
|
||||||
|
"go/parser"
|
||||||
|
"go/token"
|
||||||
|
"go/types"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestV1PublicAPIContract(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
entries, err := os.ReadDir(".")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fileSet := token.NewFileSet()
|
||||||
|
var files []*ast.File
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") || strings.HasSuffix(entry.Name(), "_test.go") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
file, err := parser.ParseFile(fileSet, entry.Name(), nil, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse %s: %v", entry.Name(), err)
|
||||||
|
}
|
||||||
|
files = append(files, file)
|
||||||
|
}
|
||||||
|
configuration := types.Config{Importer: importer.Default()}
|
||||||
|
checked, err := configuration.Check("gamertan.com/sandwich-hime/sando", fileSet, files, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
qualifier := func(pkg *types.Package) string {
|
||||||
|
if pkg == nil || pkg.Path() == checked.Path() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return pkg.Name()
|
||||||
|
}
|
||||||
|
var actual []string
|
||||||
|
for _, name := range checked.Scope().Names() {
|
||||||
|
if !token.IsExported(name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
object := checked.Scope().Lookup(name)
|
||||||
|
switch object := object.(type) {
|
||||||
|
case *types.Const:
|
||||||
|
actual = append(actual, fmt.Sprintf("const %s = %s", object.Name(), object.Val().ExactString()))
|
||||||
|
case *types.TypeName:
|
||||||
|
named, ok := object.Type().(*types.Named)
|
||||||
|
if !ok {
|
||||||
|
actual = append(actual, types.ObjectString(object, qualifier))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if structure, ok := named.Underlying().(*types.Struct); ok {
|
||||||
|
var fields []string
|
||||||
|
for index := 0; index < structure.NumFields(); index++ {
|
||||||
|
field := structure.Field(index)
|
||||||
|
if field.Exported() {
|
||||||
|
fields = append(fields, field.Name()+" "+types.TypeString(field.Type(), qualifier))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(fields) == 0 {
|
||||||
|
actual = append(actual, "type "+object.Name()+" struct{ /* opaque */ }")
|
||||||
|
} else {
|
||||||
|
actual = append(actual, "type "+object.Name()+" struct{"+strings.Join(fields, "; ")+"}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
actual = append(actual, types.ObjectString(object, qualifier))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
actual = append(actual, types.ObjectString(object, qualifier))
|
||||||
|
}
|
||||||
|
typeName, ok := object.(*types.TypeName)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
named, ok := typeName.Type().(*types.Named)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for index := 0; index < named.NumMethods(); index++ {
|
||||||
|
method := named.Method(index)
|
||||||
|
if method.Exported() {
|
||||||
|
actual = append(actual, types.ObjectString(method, qualifier))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(actual)
|
||||||
|
got := strings.Join(actual, "\n") + "\n"
|
||||||
|
want, err := os.ReadFile("testdata/public-api-v1.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want = []byte(strings.TrimPrefix(string(want), "# SPDX-License-Identifier: Apache-2.0\n\n"))
|
||||||
|
if string(want) != got {
|
||||||
|
t.Fatalf("v1 public API drifted\n--- committed contract ---\n%s--- observed API ---\n%s", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sando
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type benchmarkView struct {
|
||||||
|
Title string
|
||||||
|
URL string
|
||||||
|
Admin bool
|
||||||
|
Items []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
benchmarkContext = context.Background()
|
||||||
|
benchmarkData = benchmarkView{
|
||||||
|
Title: `A typed <view> & its "output"`,
|
||||||
|
URL: "/projects/sandwich-hime/?from=benchmark&mode=equivalent",
|
||||||
|
Admin: true,
|
||||||
|
Items: []string{"compiler", "runtime", "language server", "editor tooling"},
|
||||||
|
}
|
||||||
|
benchmarkHTMLTemplate = template.Must(template.New("v1-corpus").Parse(`<article data-title="{{.Title}}"><h1>{{.Title}}</h1>{{if .Admin}}<strong>Admin</strong>{{end}}<ul>{{range .Items}}<li>{{.}}</li>{{end}}</ul><a href="{{.URL}}">Open</a></article>`))
|
||||||
|
)
|
||||||
|
|
||||||
|
func benchmarkSandoComponent(view benchmarkView) Component {
|
||||||
|
return ComponentFunc(func(_ context.Context, writer io.Writer) error {
|
||||||
|
if err := WriteString(writer, `<article data-title="`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := WriteAttr(writer, view.Title); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := WriteString(writer, `"><h1>`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := WriteText(writer, view.Title); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := WriteString(writer, `</h1>`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if view.Admin {
|
||||||
|
if err := WriteString(writer, `<strong>Admin</strong>`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := WriteString(writer, `<ul>`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, item := range view.Items {
|
||||||
|
if err := WriteString(writer, `<li>`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := WriteText(writer, item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := WriteString(writer, `</li>`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := WriteString(writer, `</ul><a href="`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := WriteURL(writer, view.URL); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return WriteString(writer, `">Open</a></article>`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBenchmarkCorpusEquivalent(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
var himeOutput bytes.Buffer
|
||||||
|
if err := Render(benchmarkContext, &himeOutput, benchmarkSandoComponent(benchmarkData)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var standardOutput bytes.Buffer
|
||||||
|
if err := benchmarkHTMLTemplate.Execute(&standardOutput, benchmarkData); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if himeOutput.String() != standardOutput.String() {
|
||||||
|
t.Fatalf("benchmark corpus is not output-equivalent\nhtml/template: %q\nSandwich Hime: %q", standardOutput.String(), himeOutput.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkV1CorpusSandwichHime(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(int64(renderedBenchmarkSize(b)))
|
||||||
|
for b.Loop() {
|
||||||
|
if err := Render(benchmarkContext, io.Discard, benchmarkSandoComponent(benchmarkData)); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkV1CorpusHTMLTemplate(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(int64(renderedBenchmarkSize(b)))
|
||||||
|
for b.Loop() {
|
||||||
|
if err := benchmarkHTMLTemplate.Execute(io.Discard, benchmarkData); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderedBenchmarkSize(tb testing.TB) int {
|
||||||
|
tb.Helper()
|
||||||
|
var output bytes.Buffer
|
||||||
|
if err := Render(benchmarkContext, &output, benchmarkSandoComponent(benchmarkData)); err != nil {
|
||||||
|
tb.Fatal(err)
|
||||||
|
}
|
||||||
|
return output.Len()
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package sando
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHTMLTemplateDifferentialCorpus(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
source string
|
||||||
|
prefix string
|
||||||
|
suffix string
|
||||||
|
write func(*bytes.Buffer, any) error
|
||||||
|
values []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "HTML text", source: `<p>{{.}}</p>`, prefix: `<p>`, suffix: `</p>`,
|
||||||
|
write: func(output *bytes.Buffer, value any) error { return WriteText(output, value) },
|
||||||
|
values: differentialTextValues(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "quoted attribute", source: `<p title="{{.}}">x</p>`, prefix: `<p title="`, suffix: `">x</p>`,
|
||||||
|
write: func(output *bytes.Buffer, value any) error { return WriteAttr(output, value) },
|
||||||
|
values: differentialTextValues(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "RCDATA", source: `<textarea>{{.}}</textarea>`, prefix: `<textarea>`, suffix: `</textarea>`,
|
||||||
|
write: func(output *bytes.Buffer, value any) error { return WriteRCDATA(output, value) },
|
||||||
|
values: differentialTextValues(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "safe URL", source: `<a href="{{.}}">x</a>`, prefix: `<a href="`, suffix: `">x</a>`,
|
||||||
|
write: func(output *bytes.Buffer, value any) error { return WriteURL(output, value) },
|
||||||
|
values: []string{"", "/", "./relative", "?q=a&next=b", "#section", "https://example.test/a?x=1&y=2", "HTTP://example.test/", "mailto:reader@example.test"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
test := test
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
parsed := template.Must(template.New(test.name).Parse(test.source))
|
||||||
|
for _, value := range test.values {
|
||||||
|
var baseline bytes.Buffer
|
||||||
|
if err := parsed.Execute(&baseline, value); err != nil {
|
||||||
|
t.Fatalf("html/template value %q: %v", value, err)
|
||||||
|
}
|
||||||
|
var output bytes.Buffer
|
||||||
|
output.WriteString(test.prefix)
|
||||||
|
if err := test.write(&output, value); err != nil {
|
||||||
|
t.Fatalf("Sandwich Hime value %q: %v", value, err)
|
||||||
|
}
|
||||||
|
output.WriteString(test.suffix)
|
||||||
|
if output.String() != baseline.String() {
|
||||||
|
t.Fatalf("differential mismatch for %q\nhtml/template: %q\nSandwich Hime: %q", value, baseline.String(), output.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTMLTemplateDifferentialUnsafeURLPolicy(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
parsed := template.Must(template.New("url").Parse(`<a href="{{.}}">x</a>`))
|
||||||
|
values := []string{
|
||||||
|
"javascript:alert(1)",
|
||||||
|
" JAVASCRIPT:alert(1) ",
|
||||||
|
"data:text/html,<script>alert(1)</script>",
|
||||||
|
"vbscript:msgbox(1)",
|
||||||
|
"unknown:opaque",
|
||||||
|
"java%73cript:alert(1)",
|
||||||
|
}
|
||||||
|
for _, value := range values {
|
||||||
|
var baseline bytes.Buffer
|
||||||
|
if err := parsed.Execute(&baseline, value); err != nil {
|
||||||
|
t.Fatalf("html/template value %q: %v", value, err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(baseline.String(), "#ZgotmplZ") {
|
||||||
|
t.Fatalf("html/template did not block corpus URL %q: %q", value, baseline.String())
|
||||||
|
}
|
||||||
|
var output bytes.Buffer
|
||||||
|
err := WriteURL(&output, value)
|
||||||
|
if !errors.Is(err, ErrUnsafeURL) {
|
||||||
|
t.Fatalf("Sandwich Hime accepted corpus URL %q: output=%q err=%v", value, output.String(), err)
|
||||||
|
}
|
||||||
|
if output.Len() != 0 {
|
||||||
|
t.Fatalf("Sandwich Hime wrote bytes before rejecting %q: %q", value, output.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTMLTemplateDifferentialDocumentedStrictness(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("invalid UTF-8", func(t *testing.T) {
|
||||||
|
value := "invalid UTF-8: \xff:end"
|
||||||
|
parsed := template.Must(template.New("text").Parse(`<p>{{.}}</p>`))
|
||||||
|
var baseline bytes.Buffer
|
||||||
|
if err := parsed.Execute(&baseline, value); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if utf8.Valid(baseline.Bytes()) {
|
||||||
|
t.Fatalf("baseline unexpectedly normalized invalid UTF-8: %q", baseline.Bytes())
|
||||||
|
}
|
||||||
|
var output bytes.Buffer
|
||||||
|
if err := WriteText(&output, value); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !utf8.Valid(output.Bytes()) || !strings.Contains(output.String(), "\uFFFD") {
|
||||||
|
t.Fatalf("Sandwich Hime did not normalize invalid UTF-8: %q", output.Bytes())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("control in otherwise allowed URL", func(t *testing.T) {
|
||||||
|
value := "https:\n//example.test/"
|
||||||
|
parsed := template.Must(template.New("url").Parse(`<a href="{{.}}">x</a>`))
|
||||||
|
var baseline bytes.Buffer
|
||||||
|
if err := parsed.Execute(&baseline, value); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.ToLower(baseline.String()), "%0a") {
|
||||||
|
t.Fatalf("baseline did not visibly encode the control: %q", baseline.String())
|
||||||
|
}
|
||||||
|
var output bytes.Buffer
|
||||||
|
if err := WriteURL(&output, value); !errors.Is(err, ErrUnsafeURL) {
|
||||||
|
t.Fatalf("Sandwich Hime did not fail closed: output=%q err=%v", output.String(), err)
|
||||||
|
}
|
||||||
|
if output.Len() != 0 {
|
||||||
|
t.Fatalf("Sandwich Hime wrote before rejecting the control: %q", output.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("explicit tel allowlist", func(t *testing.T) {
|
||||||
|
value := "tel:+15555550100"
|
||||||
|
parsed := template.Must(template.New("url").Parse(`<a href="{{.}}">x</a>`))
|
||||||
|
var baseline bytes.Buffer
|
||||||
|
if err := parsed.Execute(&baseline, value); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(baseline.String(), "#ZgotmplZ") {
|
||||||
|
t.Fatalf("baseline URL policy changed: %q", baseline.String())
|
||||||
|
}
|
||||||
|
var output bytes.Buffer
|
||||||
|
if err := WriteURL(&output, value); err != nil {
|
||||||
|
t.Fatalf("Sandwich Hime rejected its documented tel scheme: %v", err)
|
||||||
|
}
|
||||||
|
if output.String() != value {
|
||||||
|
t.Fatalf("Sandwich Hime tel output = %q", output.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTMLTemplateDifferentialExplicitTrustedHTML(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
value := `<strong data-note="reviewed & trusted">ok</strong>`
|
||||||
|
parsed := template.Must(template.New("trusted HTML").Parse(`<div>{{.}}</div>`))
|
||||||
|
var baseline bytes.Buffer
|
||||||
|
if err := parsed.Execute(&baseline, template.HTML(value)); err != nil { // #nosec G203 -- the test is the explicit trust-boundary comparison.
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var output bytes.Buffer
|
||||||
|
output.WriteString("<div>")
|
||||||
|
if err := WriteText(&output, TrustHTML(value)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
output.WriteString("</div>")
|
||||||
|
if output.String() != baseline.String() {
|
||||||
|
t.Fatalf("trusted HTML mismatch\nhtml/template: %q\nSandwich Hime: %q", baseline.String(), output.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func differentialTextValues() []string {
|
||||||
|
return []string{
|
||||||
|
"",
|
||||||
|
"ordinary text",
|
||||||
|
`<script>alert("x")</script>`,
|
||||||
|
`quotes: "double" and 'single' & ampersand`,
|
||||||
|
"Unicode: 雪 🥪 e\u0301",
|
||||||
|
"NUL:\x00:end",
|
||||||
|
"line separators: \u2028\u2029",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzWriteURLPolicy(f *testing.F) {
|
||||||
|
for _, seed := range []string{
|
||||||
|
"",
|
||||||
|
"/relative?one=1&two=2",
|
||||||
|
"https://example.test/path",
|
||||||
|
" JAVASCRIPT:alert(1) ",
|
||||||
|
"https:\n//example.test/",
|
||||||
|
"tel:+15555550100",
|
||||||
|
"invalid:\xff",
|
||||||
|
} {
|
||||||
|
f.Add(seed)
|
||||||
|
}
|
||||||
|
f.Fuzz(func(t *testing.T, value string) {
|
||||||
|
if len(value) > 64<<10 {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
var first, second bytes.Buffer
|
||||||
|
firstErr := WriteURL(&first, value)
|
||||||
|
secondErr := WriteURL(&second, value)
|
||||||
|
if first.String() != second.String() || fmt.Sprint(firstErr) != fmt.Sprint(secondErr) {
|
||||||
|
t.Fatal("URL policy was not deterministic")
|
||||||
|
}
|
||||||
|
if firstErr != nil {
|
||||||
|
if !errors.Is(firstErr, ErrUnsafeURL) || first.Len() != 0 {
|
||||||
|
t.Fatalf("URL rejection was not fail-closed: output=%q err=%v", first.String(), firstErr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !utf8.ValidString(first.String()) || strings.ContainsAny(first.String(), "\x00\r\n") {
|
||||||
|
t.Fatalf("accepted URL output is not valid single-line UTF-8: %q", first.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Vendored
+32
@@ -0,0 +1,32 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
const ABI = "sando.v1"
|
||||||
|
const ABISandoV1 = "sando.v1"
|
||||||
|
const RuntimeABI = "sando.v1"
|
||||||
|
func (*URLSafetyError).Error() string
|
||||||
|
func (*URLSafetyError).Unwrap() error
|
||||||
|
func (ComponentFunc).Render(ctx context.Context, w io.Writer) error
|
||||||
|
func Render(ctx context.Context, w io.Writer, component Component) error
|
||||||
|
func TrustCSS(value string) TrustedCSS
|
||||||
|
func TrustHTML(value string) TrustedHTML
|
||||||
|
func TrustJS(value string) TrustedJS
|
||||||
|
func TrustURL(value string) TrustedURL
|
||||||
|
func WriteAttr(w io.Writer, value any) error
|
||||||
|
func WriteCSS(w io.Writer, value TrustedCSS) error
|
||||||
|
func WriteHTML(w io.Writer, value TrustedHTML) error
|
||||||
|
func WriteJS(w io.Writer, value TrustedJS) error
|
||||||
|
func WriteRCDATA(w io.Writer, value any) error
|
||||||
|
func WriteString(w io.Writer, value string) error
|
||||||
|
func WriteText(w io.Writer, value any) error
|
||||||
|
func WriteURL(w io.Writer, value any) error
|
||||||
|
type Component interface{Render(context.Context, io.Writer) error}
|
||||||
|
type ComponentFunc func(context.Context, io.Writer) error
|
||||||
|
type TrustedCSS struct{ /* opaque */ }
|
||||||
|
type TrustedHTML struct{ /* opaque */ }
|
||||||
|
type TrustedJS struct{ /* opaque */ }
|
||||||
|
type TrustedURL struct{ /* opaque */ }
|
||||||
|
type URLSafetyError struct{Scheme string; Reason string}
|
||||||
|
var ErrNilComponent error
|
||||||
|
var ErrNilContext error
|
||||||
|
var ErrNilWriter error
|
||||||
|
var ErrUnsafeURL error
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||||
|
|
||||||
|
# Sanitized public source snapshots
|
||||||
|
|
||||||
|
`export-public-snapshot.sh` creates a host-neutral filesystem snapshot from a
|
||||||
|
committed Git tree. It does not initialize a repository, copy `.git`, configure
|
||||||
|
a remote, commit, tag, push, or publish anything.
|
||||||
|
|
||||||
|
Release use requires a clean worktree, a ref resolving exactly to `HEAD`, and
|
||||||
|
the exact-file policy committed at `scripts/public-snapshot.allow` in that
|
||||||
|
ref:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/export-public-snapshot.sh \
|
||||||
|
--mode release \
|
||||||
|
--ref HEAD \
|
||||||
|
--destination ../sandwich-hime-public-review
|
||||||
|
```
|
||||||
|
|
||||||
|
The destination must not exist and its canonical parent must be outside the
|
||||||
|
source worktree, its worktree-specific Git directory, and its shared Git common
|
||||||
|
directory. This includes ordinary `.git` directories and linked-worktree
|
||||||
|
metadata stored elsewhere. The exporter creates a private sibling staging
|
||||||
|
directory and renames it into place only after all checks pass. It never clears
|
||||||
|
or replaces an existing destination; failure cleanup is limited to a staging
|
||||||
|
directory carrying the exporter's ownership marker.
|
||||||
|
|
||||||
|
Review mode may use an externally reviewed exact-file policy while changes to
|
||||||
|
the exporter itself await a commit. Its provenance is conspicuously marked
|
||||||
|
`review` and is not a release artifact:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/export-public-snapshot.sh \
|
||||||
|
--mode review \
|
||||||
|
--allowlist /path/to/reviewed-exact-files.allow \
|
||||||
|
--destination ../snapshot-for-review
|
||||||
|
```
|
||||||
|
|
||||||
|
The policy accepts individual files only—never directories or globs. The
|
||||||
|
export fails for missing/duplicate/forbidden entries, non-regular Git objects,
|
||||||
|
symlinks, binary or oversized content, aggregate size limits, private developer
|
||||||
|
filesystem indicators, common private-key/token indicators, database or build
|
||||||
|
artifacts, and explicitly private integration material. Host workflow folders,
|
||||||
|
private trees, prototype/history trees, and application-specific integrations
|
||||||
|
are not in the reviewed policy.
|
||||||
|
|
||||||
|
`PUBLIC-SNAPSHOT.sha256` records every exported source file. The deterministic
|
||||||
|
`PUBLIC-SNAPSHOT.json` records only the project identifier, export policy and
|
||||||
|
mode, file count, and policy/manifest digests. Private commit and tree IDs,
|
||||||
|
commit timestamps, author or committer identity, email, hostname, branch name,
|
||||||
|
remote URL, and checkout path stay outside the exported tree. Filesystem
|
||||||
|
timestamps are normalized to the Unix epoch. A separate private release ledger
|
||||||
|
may map the private source commit to the resulting public commit and signed
|
||||||
|
tags.
|
||||||
|
|
||||||
|
When an exported tree is reviewed into an existing public checkout, compare and
|
||||||
|
copy files by content (for example, checksum-aware synchronization or a fresh
|
||||||
|
tree replacement). Size-and-modification-time shortcuts are unsafe here because
|
||||||
|
the exporter deliberately gives every snapshot the same normalized timestamp;
|
||||||
|
the manifest and provenance records must be verified again before publication.
|
||||||
|
|
||||||
|
Run the focused checks with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bash scripts/test-public-snapshot.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The implementation expects Bash, Git, tar, GNU-compatible core utilities, and
|
||||||
|
a filesystem supporting an atomic rename within the destination parent.
|
||||||
+32
-5
@@ -2,20 +2,47 @@
|
|||||||
|
|
||||||
# Repository verification tools
|
# Repository verification tools
|
||||||
|
|
||||||
These scripts are intentionally understandable shell and PowerShell rather than a release framework with hidden defaults.
|
These scripts are intentionally understandable shell rather than a release
|
||||||
|
framework with hidden defaults. The maintained native verification paths are
|
||||||
|
Linux/amd64 and macOS/arm64.
|
||||||
|
|
||||||
- `verify.sh` runs root and nested-module tests and vet, builds `himesan`, checks the compiler-owned golden output, and proves two generation passes leave the same bytes and unchanged modification times. Set `HIMESAN_RACE=1` for race tests.
|
- `verify.sh` runs root and nested-module tests and vet, builds `himesan`, checks the compiler-owned golden output, and proves two generation passes leave the same bytes and unchanged modification times. Set `HIMESAN_RACE=1` for race tests.
|
||||||
- `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.
|
- `test-public-snapshot.sh` proves the exact allowlist, secret/path scanner, legal boundary, deterministic manifest, and no-overwrite export behavior on both maintained native hosts.
|
||||||
|
- Contract tests bind the exported runtime API, CLI help, JSON/configuration schemas, diagnostic-code inventory, generic component signature, and generated provenance to the reviewed files under `contracts/` and `sando/testdata/`.
|
||||||
|
- `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` and the four-lane `HIMESAN_NATIVE_EVIDENCE_DIR`. Seal the review directory with `go run ./cmd/himesan-release evidence-manifest`; `verify-native` independently checks every native receipt sidecar, source identity, gate, freshness bound, and generated-output digest. 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, adds the nested runtime before installing the parent compiler, and exercises 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.
|
||||||
|
- `package-native.sh` performs two native builds and uses the repository-owned Go packager for a deterministic archive, manifest, SBOM, and checksums. `package-macos.sh` is the explicit Apple Silicon entrypoint used by the release operator.
|
||||||
|
- `sign-notarize-macos.sh` is a deliberately manual boundary. It requires the explicitly approved unsigned archive digest, uses Cole's Developer ID and Keychain-held notary profile, regenerates provenance and checksums for the changed signed Mach-O bytes, and produces a signed, notarized, and stapled DMG. The native runner receives neither credential.
|
||||||
|
- `verify-real-browser.sh` is opt-in release evidence. It runs the development client in an actual reviewed Chrome/Chromium binary, exercising CSP-restricted execution, SSE diagnostics, reload, and fragment/API exclusions, then reruns the process cleanup integration cases. Chrome is not a normal build or consumer dependency.
|
||||||
|
|
||||||
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 export scanner also rejects Markdown references to known excluded operator
|
||||||
|
documentation roots. It is a bounded publication check, not a complete Markdown
|
||||||
|
link checker or a substitute for inspecting the exported diff. Source-code
|
||||||
|
fixtures can still describe the scanner's rejected paths.
|
||||||
|
|
||||||
|
The canonical Linux and macOS CI gates run the contract and public-snapshot
|
||||||
|
checks plus bounded fuzz sessions for the parser/context compiler, Go-aware
|
||||||
|
delimiter scanner, URL policy, and LSP boundaries. The compiler target also
|
||||||
|
asserts deterministic diagnostics and generated Go, source-digest binding, and
|
||||||
|
safe source-map directives. Seed-corpus execution remains part of ordinary
|
||||||
|
`go test`; the bounded sessions are extra evidence, not a substitute for the
|
||||||
|
long exact-candidate campaign before v1.
|
||||||
|
|
||||||
The release preflight invokes `govulncheck` from the official Go vulnerability project at the exact module version `golang.org/x/vuln@v1.6.0`. Updating that pin requires reviewing the upstream tag and rerunning the supported Go lines.
|
The release preflight invokes `govulncheck` from the official Go vulnerability project at the exact module version `golang.org/x/vuln@v1.6.0`. Updating that pin requires reviewing the upstream tag and rerunning the supported Go lines.
|
||||||
|
|
||||||
## Preview automation status
|
## Preview automation status
|
||||||
|
|
||||||
Forge workflows are intentionally excluded from the sanitized pre-1.0 public snapshot until the project has confirmed its own Gitea runner availability and reviewed locally hosted or otherwise pinned dependencies. Local `verify.sh`, `verify.ps1`, license, and release-preflight results are the preview gates.
|
Forge workflows are intentionally excluded from the sanitized pre-1.0 public
|
||||||
|
snapshot. The private development repository uses pinned Linux/amd64 and
|
||||||
|
repository-scoped native macOS/arm64 runners; the public source remains
|
||||||
|
independently verifiable with the repository scripts and both native release
|
||||||
|
preflights.
|
||||||
|
|
||||||
|
The private `public-candidate-verification` workflow is a release controller,
|
||||||
|
not public-source evidence by association. It accepts only the exact lowercase
|
||||||
|
commit currently at canonical public `main`, clones only that fixed Gitea origin without credentials,
|
||||||
|
and produces receipts naming `gamertan/sandwich-hime`. Development-repository
|
||||||
|
receipts cannot satisfy the public release preflight.
|
||||||
|
|
||||||
If Gitea automation is later added to the public repository, pin every external action to a reviewed immutable commit, document its provenance, grant minimum permissions, and keep a local verification path. A secondary forge may host a sanitized, read-only discovery snapshot, but hosted workflows stay disabled there and it does not become a release or contribution authority.
|
If Gitea automation is later added to the public repository, pin every external action to a reviewed immutable commit, document its provenance, grant minimum permissions, and keep a local verification path. A secondary forge may host a sanitized, read-only discovery snapshot, but hosted workflows stay disabled there and it does not become a release or contribution authority.
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ check_sha256() {
|
|||||||
|
|
||||||
is_comment_capable_project_file() {
|
is_comment_capable_project_file() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
COPYRIGHT | */COPYRIGHT | .editorconfig | .gitattributes | .gitignore | *.go | *.mod | *.md | *.sh | *.ps1 | *.yml | *.yaml | *.html | *.css | *.js | *.toml | *.allow)
|
COPYRIGHT | */COPYRIGHT | .editorconfig | .gitattributes | .gitignore | *.go | *.mod | *.md | *.txt | *.sh | *.ps1 | *.yml | *.yaml | *.html | *.css | *.js | *.toml | *.plist | *.allow)
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
@@ -67,8 +67,10 @@ list_project_files() {
|
|||||||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
git ls-files --cached --others --exclude-standard -z
|
git ls-files --cached --others --exclude-standard -z
|
||||||
else
|
else
|
||||||
find . -type d \( -name .git -o -name vendor -o -name bin -o -name dist -o -name coverage \) -prune -o \
|
while IFS= read -r -d '' path; do
|
||||||
-type f -print0 | sed -z 's#^\./##'
|
printf '%s\0' "${path#./}"
|
||||||
|
done < <(find . -type d \( -name .git -o -name vendor -o -name bin -o -name dist -o -name coverage \) -prune -o \
|
||||||
|
-type f -print0)
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +154,8 @@ while IFS= read -r -d '' path; do
|
|||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
*.json)
|
*.json)
|
||||||
fail "$path cannot carry a comment and needs an explicit license-map entry"
|
grep -Fq "\`$path\`" LICENSES.md || \
|
||||||
|
fail "$path cannot carry a comment and needs an exact license-map entry"
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
Executable
+258
@@ -0,0 +1,258 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
script_root=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
project_root=$(CDPATH= cd -- "$script_root/.." && pwd)
|
||||||
|
# shellcheck source=public-snapshot-lib.sh
|
||||||
|
source "$script_root/public-snapshot-lib.sh"
|
||||||
|
# Release safety limits are policy, not caller-tunable settings.
|
||||||
|
SNAPSHOT_MAX_FILE_BYTES=1048576
|
||||||
|
SNAPSHOT_MAX_TOTAL_BYTES=16777216
|
||||||
|
SNAPSHOT_MAX_FILES=2000
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat >&2 <<'USAGE'
|
||||||
|
Usage: export-public-snapshot.sh --destination PATH [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--source PATH Git worktree root (default: repository containing script)
|
||||||
|
--ref REF Committed source ref (default: HEAD)
|
||||||
|
--mode MODE release (default) or review
|
||||||
|
--allowlist PATH Review mode only: audited external exact-file policy
|
||||||
|
|
||||||
|
The destination must not exist and must be outside the source worktree. The
|
||||||
|
exporter creates it atomically and never initializes Git, configures a remote,
|
||||||
|
commits, tags, pushes, or copies .git.
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
source_path=$project_root
|
||||||
|
source_ref=HEAD
|
||||||
|
destination=""
|
||||||
|
mode=release
|
||||||
|
allowlist_override=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--source)
|
||||||
|
[[ $# -ge 2 ]] || { usage; exit 2; }
|
||||||
|
source_path=$2
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--ref)
|
||||||
|
[[ $# -ge 2 ]] || { usage; exit 2; }
|
||||||
|
source_ref=$2
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--destination)
|
||||||
|
[[ $# -ge 2 ]] || { usage; exit 2; }
|
||||||
|
destination=$2
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--mode)
|
||||||
|
[[ $# -ge 2 ]] || { usage; exit 2; }
|
||||||
|
mode=$2
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--allowlist)
|
||||||
|
[[ $# -ge 2 ]] || { usage; exit 2; }
|
||||||
|
allowlist_override=$2
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h | --help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "public snapshot: unknown argument: $1" >&2
|
||||||
|
usage
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n $destination ]] || { usage; exit 2; }
|
||||||
|
[[ $mode == release || $mode == review ]] || { echo "public snapshot: mode must be release or review" >&2; exit 2; }
|
||||||
|
[[ $source_ref != -* && $source_ref != *$'\n'* && $source_ref != *$'\r'* ]] || {
|
||||||
|
echo "public snapshot: invalid source ref" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
if [[ $mode == release && -n $allowlist_override ]]; then
|
||||||
|
echo "public snapshot: release mode requires the allowlist committed in the source ref" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
source_path=$(snapshot_realpath_existing "$source_path")
|
||||||
|
git_root=$(git -C "$source_path" rev-parse --show-toplevel 2>/dev/null) || {
|
||||||
|
echo "public snapshot: source is not a Git worktree" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
git_root=$(snapshot_realpath_existing "$git_root")
|
||||||
|
[[ $source_path == "$git_root" ]] || { echo "public snapshot: --source must name the worktree root" >&2; exit 1; }
|
||||||
|
git_dir=$(git -C "$git_root" rev-parse --absolute-git-dir 2>/dev/null) || {
|
||||||
|
echo "public snapshot: cannot resolve source Git metadata directory" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
git_common_dir=$(git -C "$git_root" rev-parse --git-common-dir 2>/dev/null) || {
|
||||||
|
echo "public snapshot: cannot resolve source Git common directory" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
git_dir=$(snapshot_realpath_existing "$git_dir")
|
||||||
|
if [[ $git_common_dir != /* ]]; then
|
||||||
|
git_common_dir=$git_root/$git_common_dir
|
||||||
|
fi
|
||||||
|
git_common_dir=$(snapshot_realpath_existing "$git_common_dir")
|
||||||
|
commit=$(git -C "$git_root" rev-parse --verify "${source_ref}^{commit}" 2>/dev/null) || {
|
||||||
|
echo "public snapshot: source ref does not resolve to a commit" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ $commit =~ ^[0-9a-f]{40}$ || $commit =~ ^[0-9a-f]{64}$ ]] || {
|
||||||
|
echo "public snapshot: source commit is not a full object ID" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ $mode == release ]]; then
|
||||||
|
head_commit=$(git -C "$git_root" rev-parse --verify HEAD^{commit})
|
||||||
|
[[ $commit == "$head_commit" ]] || { echo "public snapshot: release ref must resolve to HEAD" >&2; exit 1; }
|
||||||
|
status=$(git -C "$git_root" status --porcelain=v1 --untracked-files=all)
|
||||||
|
snapshot_status_is_clean "$status" || {
|
||||||
|
echo "public snapshot: release source worktree is dirty" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
destination_parent=$(dirname -- "$destination")
|
||||||
|
destination_name=$(basename -- "$destination")
|
||||||
|
[[ $destination_name != . && $destination_name != .. && -n $destination_name ]] || {
|
||||||
|
echo "public snapshot: invalid destination name" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
destination_parent=$(snapshot_realpath_existing "$destination_parent")
|
||||||
|
[[ -d $destination_parent && ! -L $destination_parent ]] || {
|
||||||
|
echo "public snapshot: destination parent must be an existing non-symlink directory" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
for protected_root in "$git_root" "$git_dir" "$git_common_dir"; do
|
||||||
|
if snapshot_path_is_at_or_below "$destination_parent" "$protected_root"; then
|
||||||
|
echo "public snapshot: destination must be outside the source worktree and Git metadata" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
destination=$destination_parent/$destination_name
|
||||||
|
[[ ! -e $destination && ! -L $destination ]] || {
|
||||||
|
echo "public snapshot: destination already exists; refusing to alter it" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
staging=$(mktemp -d "$destination_parent/.${destination_name}.himesan-public-export.XXXXXX")
|
||||||
|
marker_name=.himesan-public-export-owned
|
||||||
|
marker=$staging/$marker_name
|
||||||
|
printf 'owned temporary public snapshot staging directory\n' >"$marker"
|
||||||
|
cleanup() {
|
||||||
|
local status=$?
|
||||||
|
if [[ -n ${staging:-} && -d $staging && -f $marker ]]; then
|
||||||
|
case $staging in
|
||||||
|
"$destination_parent"/."$destination_name".himesan-public-export.*)
|
||||||
|
rm -rf -- "$staging"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
exit "$status"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
policy_file=$staging/.himesan-policy-input
|
||||||
|
if [[ -n $allowlist_override ]]; then
|
||||||
|
[[ $mode == review ]] || { echo "public snapshot: external policy is review-only" >&2; exit 2; }
|
||||||
|
[[ -f $allowlist_override && ! -L $allowlist_override ]] || {
|
||||||
|
echo "public snapshot: external allowlist must be a regular non-symlink file" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
cp -- "$allowlist_override" "$policy_file"
|
||||||
|
else
|
||||||
|
policy_path=scripts/public-snapshot.allow
|
||||||
|
git -C "$git_root" cat-file -e "$commit:$policy_path" 2>/dev/null || {
|
||||||
|
echo "public snapshot: committed ref lacks $policy_path" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
git -C "$git_root" show "$commit:$policy_path" >"$policy_file"
|
||||||
|
fi
|
||||||
|
policy_sha256=$(sha256sum "$policy_file" | awk '{print $1}')
|
||||||
|
|
||||||
|
seen_lines=$'\n'
|
||||||
|
paths=()
|
||||||
|
while IFS= read -r line || [[ -n $line ]]; do
|
||||||
|
[[ -n $line && ${line:0:1} != '#' ]] || continue
|
||||||
|
if [[ $line == *[[:space:]]* ]] || snapshot_forbidden_path "$line"; then
|
||||||
|
echo "public snapshot: invalid or forbidden allowlist entry: $line" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
case $seen_lines in
|
||||||
|
*$'\n'"$line"$'\n'*)
|
||||||
|
echo "public snapshot: duplicate allowlist entry: $line" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
seen_lines=$seen_lines$line$'\n'
|
||||||
|
record=$(git -C "$git_root" ls-tree "$commit" -- "$line")
|
||||||
|
[[ -n $record && ${record#*$'\t'} == "$line" && $record != *$'\n'* ]] || {
|
||||||
|
echo "public snapshot: allowlisted path is absent or ambiguous in source ref: $line" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
read -r object_mode object_type object_id <<<"${record%%$'\t'*}"
|
||||||
|
[[ $object_type == blob && ($object_mode == 100644 || $object_mode == 100755) ]] || {
|
||||||
|
echo "public snapshot: allowlisted path is not a regular file: $line" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
blob_size=$(git -C "$git_root" cat-file -s "$object_id")
|
||||||
|
((blob_size <= SNAPSHOT_MAX_FILE_BYTES)) || {
|
||||||
|
echo "public snapshot: allowlisted blob is oversized: $line" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
paths+=("$line")
|
||||||
|
done <"$policy_file"
|
||||||
|
[[ ${#paths[@]} -gt 0 ]] || { echo "public snapshot: allowlist selected no files" >&2; exit 1; }
|
||||||
|
|
||||||
|
sorted_paths=()
|
||||||
|
while IFS= read -r -d '' path; do
|
||||||
|
sorted_paths[${#sorted_paths[@]}]=$path
|
||||||
|
done < <(printf '%s\0' "${paths[@]}" | LC_ALL=C sort -z)
|
||||||
|
git -C "$git_root" archive --format=tar "$commit" -- "${sorted_paths[@]}" | tar -xf - -C "$staging"
|
||||||
|
unlink "$policy_file"
|
||||||
|
|
||||||
|
exported_count=$(find "$staging" -type f ! -name "$marker_name" | wc -l)
|
||||||
|
exported_count=${exported_count//[[:space:]]/}
|
||||||
|
[[ $exported_count -eq ${#sorted_paths[@]} ]] || {
|
||||||
|
echo "public snapshot: extracted file count does not match allowlist" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
snapshot_validate_export_tree "$staging" "$marker_name"
|
||||||
|
|
||||||
|
manifest=$staging/PUBLIC-SNAPSHOT.sha256
|
||||||
|
manifest_input=$staging/.himesan-manifest-input
|
||||||
|
(cd "$staging" && find . -type f ! -name "$marker_name" ! -name .himesan-manifest-input -print0 | LC_ALL=C sort -z | xargs -0 sha256sum) >"$manifest_input"
|
||||||
|
mv "$manifest_input" "$manifest"
|
||||||
|
manifest_sha256=$(sha256sum "$manifest" | awk '{print $1}')
|
||||||
|
provenance=$staging/PUBLIC-SNAPSHOT.json
|
||||||
|
printf '{"schema_version":2,"project":"sandwich-hime","export_policy":"exact-allowlist-v1","export_mode":"%s","file_count":%s,"allowlist_sha256":"%s","manifest_sha256":"%s"}\n' \
|
||||||
|
"$mode" "$exported_count" "$policy_sha256" "$manifest_sha256" >"$provenance"
|
||||||
|
|
||||||
|
# Normalize filesystem metadata to a public constant as well as normalizing
|
||||||
|
# content. Private commit IDs, tree IDs, timestamps, identities, refs, remote
|
||||||
|
# URLs, and checkout paths do not enter the exported tree.
|
||||||
|
TZ=UTC find "$staging" -exec touch -t 197001010000 {} +
|
||||||
|
unlink "$marker"
|
||||||
|
if mv --help 2>&1 | grep -q -- '-T'; then
|
||||||
|
mv -nT -- "$staging" "$destination"
|
||||||
|
else
|
||||||
|
# BSD mv has no -T. The existing-destination preflight above preserves the
|
||||||
|
# same no-overwrite policy for the local macOS review lane.
|
||||||
|
mv -n "$staging" "$destination"
|
||||||
|
fi
|
||||||
|
if [[ -e $staging || ! -d $destination ]]; then
|
||||||
|
echo "public snapshot: destination appeared during activation; staging was not published" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
staging=""
|
||||||
|
printf 'public_snapshot=%s\ncommit=%s\nfiles=%s\n' "$destination" "$commit" "$exported_count"
|
||||||
Executable
+76
@@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
cd "$repo_root"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
printf 'Usage: scripts/native-gate.sh --repository OWNER/REPOSITORY --go-version goX.Y.Z --runner-version VERSION --runner-name NAME --output DIR\n' >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
repository=''
|
||||||
|
expected_go=''
|
||||||
|
runner_version=''
|
||||||
|
runner_name=''
|
||||||
|
output=''
|
||||||
|
while (( $# > 0 )); do
|
||||||
|
case "$1" in
|
||||||
|
--repository) repository=$2; shift 2 ;;
|
||||||
|
--go-version) expected_go=$2; shift 2 ;;
|
||||||
|
--runner-version) runner_version=$2; shift 2 ;;
|
||||||
|
--runner-name) runner_name=$2; shift 2 ;;
|
||||||
|
--output) output=$2; shift 2 ;;
|
||||||
|
*) usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [[ ! "$repository" =~ ^[a-z0-9][a-z0-9._-]*/[a-z0-9][a-z0-9._-]*$ || -z "$expected_go" || -z "$runner_version" || -z "$runner_name" || -z "$output" ]]; then
|
||||||
|
usage
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ "$(go env GOVERSION)" != "$expected_go" ]]; then
|
||||||
|
printf 'error: expected %s, found %s\n' "$expected_go" "$(go env GOVERSION)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p -- "$output"
|
||||||
|
./scripts/check-licenses.sh
|
||||||
|
./scripts/test-public-snapshot.sh
|
||||||
|
HIMESAN_RACE=1 ./scripts/verify.sh
|
||||||
|
(
|
||||||
|
fuzz_cache=$(mktemp -d "${TMPDIR:-/tmp}/himesan-fuzz-cache.XXXXXXXX")
|
||||||
|
trap 'rm -rf -- "$fuzz_cache"' EXIT HUP INT TERM
|
||||||
|
export GOCACHE="$fuzz_cache"
|
||||||
|
go test ./internal/compiler -run '^$' -fuzz '^FuzzCompileNeverPanics$' -fuzztime=15s -parallel=1
|
||||||
|
go test ./internal/compiler -run '^$' -fuzz '^FuzzGoDelimiterNeverPanics$' -fuzztime=15s -parallel=1
|
||||||
|
go test ./internal/lsp -run '^$' -fuzz '^FuzzFrameReaderNeverPanics$' -fuzztime=15s -parallel=1
|
||||||
|
go test ./internal/lsp -run '^$' -fuzz '^FuzzDocumentPositionNeverPanics$' -fuzztime=15s -parallel=1
|
||||||
|
(
|
||||||
|
cd sando
|
||||||
|
go test -run '^$' -fuzz '^FuzzWriteURLPolicy$' -fuzztime=15s -parallel=1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
|
||||||
|
(
|
||||||
|
cd sando
|
||||||
|
go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
|
||||||
|
)
|
||||||
|
./scripts/verify-consumer.sh
|
||||||
|
|
||||||
|
./scripts/package-native.sh --version v0.0.0-verification.1 --output "$output"
|
||||||
|
artifact=$(find "$output" -maxdepth 1 -type f -name '*.tar.gz' -print -quit)
|
||||||
|
artifact_sha=$(awk '{print $1}' "$artifact.sha256")
|
||||||
|
completed_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
go run ./cmd/himesan-release receipt \
|
||||||
|
--output "$output/TEND-CI-VERIFICATION.json" \
|
||||||
|
--repository "$repository" \
|
||||||
|
--commit "$(git rev-parse HEAD)" \
|
||||||
|
--tree "$(git rev-parse 'HEAD^{tree}')" \
|
||||||
|
--goos "$(go env GOOS)" --goarch "$(go env GOARCH)" \
|
||||||
|
--go-version "$expected_go" --runner-version "$runner_version" \
|
||||||
|
--runner-name "$runner_name" --artifact-sha256 "$artifact_sha" \
|
||||||
|
--completed-at "$completed_at" \
|
||||||
|
--gates test,vet,build,race,generation,contracts,public-snapshot,fuzz,vulnerability,consumer,package \
|
||||||
|
--generated-files internal/compiler/testdata/golden/basic.sando.go
|
||||||
|
printf 'Native verification evidence: %s\n' "$output"
|
||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
if [[ "$(uname -s)/$(uname -m)" != Darwin/arm64 ]]; then
|
||||||
|
printf 'error: the maintained macOS artifact must be built natively on darwin/arm64\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exec "$(dirname -- "${BASH_SOURCE[0]}")/package-native.sh" "$@"
|
||||||
Executable
+81
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
cd "$repo_root"
|
||||||
|
|
||||||
|
usage() { printf 'Usage: scripts/package-native.sh --version vX.Y.Z --output DIR\n' >&2; }
|
||||||
|
version=''
|
||||||
|
output=''
|
||||||
|
while (( $# > 0 )); do
|
||||||
|
case "$1" in
|
||||||
|
--version) [[ $# -ge 2 ]] || { usage; exit 2; }; version=$2; shift 2 ;;
|
||||||
|
--output) [[ $# -ge 2 ]] || { usage; exit 2; }; output=$2; shift 2 ;;
|
||||||
|
*) usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! "$version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
|
||||||
|
printf 'error: a canonical v-prefixed release version is required\n' >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
[[ -n "$output" ]] || { usage; exit 2; }
|
||||||
|
if ! git diff --quiet -- || ! git diff --cached --quiet --; then
|
||||||
|
printf 'error: native release packaging requires a clean tracked worktree\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
untracked_sources=$(git ls-files --others --exclude-standard -- \
|
||||||
|
'*.go' '*.sando' 'go.mod' 'go.sum' 'vendor/**' || true)
|
||||||
|
if [[ -n "$untracked_sources" ]]; then
|
||||||
|
printf 'error: untracked build inputs prevent trustworthy release provenance:\n%s\n' "$untracked_sources" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
target="$(go env GOOS)/$(go env GOARCH)"
|
||||||
|
case "$target" in
|
||||||
|
darwin/arm64 | linux/amd64) ;;
|
||||||
|
*) printf 'error: unsupported maintained native target: %s\n' "$target" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
if [[ "$(go env GOVERSION)" != go1.26.7 && "$(go env GOVERSION)" != go1.27.0 ]]; then
|
||||||
|
printf 'error: unsupported release toolchain: %s\n' "$(go env GOVERSION)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
temporary=$(mktemp -d "${TMPDIR:-/tmp}/himesan-native-package.XXXXXXXX")
|
||||||
|
temporary=$(CDPATH= cd -- "$temporary" && pwd -P)
|
||||||
|
cleanup() { rm -rf -- "$temporary"; }
|
||||||
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
|
||||||
|
commit=$(git rev-parse HEAD)
|
||||||
|
tree=$(git rev-parse 'HEAD^{tree}')
|
||||||
|
source_date_epoch=$(git show -s --format=%ct HEAD)
|
||||||
|
go_version=$(go env GOVERSION)
|
||||||
|
target_os=${target%/*}
|
||||||
|
target_arch=${target#*/}
|
||||||
|
linker_flags="-buildid= -X gamertan.com/sandwich-hime/internal/version.Compiler=$version"
|
||||||
|
for pass in one two; do
|
||||||
|
CGO_ENABLED=0 GOTOOLCHAIN=local go build -buildvcs=false -trimpath \
|
||||||
|
-ldflags "$linker_flags" -o "$temporary/himesan-$pass" ./cmd/himesan
|
||||||
|
done
|
||||||
|
if ! cmp -s "$temporary/himesan-one" "$temporary/himesan-two"; then
|
||||||
|
printf 'error: repeated native builds were not byte-identical\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
case "$target" in
|
||||||
|
darwin/arm64) expected='Mach-O 64-bit executable arm64' ;;
|
||||||
|
linux/amd64) expected='ELF 64-bit LSB executable, x86-64' ;;
|
||||||
|
esac
|
||||||
|
if ! file "$temporary/himesan-one" | grep -Fq "$expected"; then
|
||||||
|
printf 'error: candidate has the wrong native executable format\n' >&2
|
||||||
|
file "$temporary/himesan-one" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p -- "$output"
|
||||||
|
go run ./cmd/himesan-release package \
|
||||||
|
--version "$version" --commit "$commit" --tree "$tree" \
|
||||||
|
--go-version "$go_version" --goos "$target_os" --goarch "$target_arch" \
|
||||||
|
--binary "$temporary/himesan-one" --output "$output" \
|
||||||
|
--source-date-epoch "$source_date_epoch"
|
||||||
|
printf 'Unsigned native package created. Signing and notarization were not performed.\n'
|
||||||
Executable
+132
@@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
# Sourced validation helpers for export-public-snapshot.sh and its focused
|
||||||
|
# tests. This file deliberately leaves the caller's shell options unchanged.
|
||||||
|
|
||||||
|
snapshot_status_is_clean() {
|
||||||
|
[[ -z $1 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot_path_is_at_or_below() {
|
||||||
|
local candidate=$1
|
||||||
|
local boundary=$2
|
||||||
|
|
||||||
|
[[ $boundary == / || $candidate == "$boundary" || $candidate == "$boundary/"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot_realpath_existing() {
|
||||||
|
local path=$1
|
||||||
|
local directory base
|
||||||
|
if [[ -d $path ]]; then
|
||||||
|
(CDPATH= cd -- "$path" && pwd -P)
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ -f $path ]]; then
|
||||||
|
directory=$(dirname -- "$path")
|
||||||
|
base=$(basename -- "$path")
|
||||||
|
directory=$(CDPATH= cd -- "$directory" && pwd -P) || return 1
|
||||||
|
printf '%s/%s\n' "$directory" "$base"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot_forbidden_path() {
|
||||||
|
local path=$1
|
||||||
|
local lower
|
||||||
|
lower=$(printf '%s' "$path" | LC_ALL=C tr '[:upper:]' '[:lower:]')
|
||||||
|
|
||||||
|
[[ $path != /* && $path != *\\* && $path != *//* ]] || return 0
|
||||||
|
[[ $path != . && $path != .. && $path != ../* && $path != */../* && $path != */.. ]] || return 0
|
||||||
|
[[ $path != *$'\n'* && $path != *$'\r'* && $path != *$'\t'* ]] || return 0
|
||||||
|
|
||||||
|
case "/$lower/" in
|
||||||
|
*/.git/* | */.gitea/* | */.github/* | */private/* | */prototype/* | */prototypes/* | */history/* | */legacy/* | */vendor/* | */bin/* | */dist/* | */coverage/* | */cmd/himetest/* | */cmd/himework/* | */internal/himesan/* | */internal/integration/* | */templates/*)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
case $lower in
|
||||||
|
go.work | go.work.sum | .env | .env.* | */.env | */.env.* | *.db | *.db-* | *.sqlite | *.sqlite3 | *.pem | *.key | *.p12 | *.pfx | */id_rsa | */id_ed25519 | *credentials* | *.exe | *.dll | *.dylib | *.so | *.a | *.o | *.test | *.prof | *.cover | *.zip | *.tar | *.tar.gz | *.tgz)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot_validate_export_tree() {
|
||||||
|
local root=$1
|
||||||
|
local marker=${2:-}
|
||||||
|
local max_file_bytes=${SNAPSHOT_MAX_FILE_BYTES:-1048576}
|
||||||
|
local max_total_bytes=${SNAPSHOT_MAX_TOTAL_BYTES:-16777216}
|
||||||
|
local max_files=${SNAPSHOT_MAX_FILES:-2000}
|
||||||
|
local total=0
|
||||||
|
local count=0
|
||||||
|
local file rel size
|
||||||
|
local users_word=Users
|
||||||
|
local private_unix="/(home|${users_word})/[^/[:space:]]+"
|
||||||
|
local private_windows='[A-Za-z]:[\\/]+Users[\\/]'
|
||||||
|
local private_wsl="/mnt/[a-zA-Z]/${users_word}/"
|
||||||
|
local pem_begin='-----BEGIN '
|
||||||
|
local private_key="${pem_begin}([A-Z0-9]+ )?PRIVATE KEY-----|${pem_begin}PGP PRIVATE KEY BLOCK-----"
|
||||||
|
local provider_token='AKIA[0-9A-Z]{16}|(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}|glpat-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}'
|
||||||
|
local private_commit_id='(private development (source|baseline)|private (development )?(commit|source))[^[:cntrl:]]*[0-9a-f]{12,64}'
|
||||||
|
local private_repository='sandwich-hime-''dev'
|
||||||
|
# Known excluded documentation roots, not ordinary prose about private work.
|
||||||
|
# Scope this to Markdown: exported scanner code legitimately names its policy.
|
||||||
|
local private_markdown_ref='(^|[^[:alnum:]_./%-])([.]{1,2}/)*(private|history)/'
|
||||||
|
|
||||||
|
if find "$root" -type l -print -quit | grep -q .; then
|
||||||
|
echo "public snapshot: symbolic links are forbidden" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
rel=${file#"$root"/}
|
||||||
|
[[ -z $marker || $rel != "$marker" ]] || continue
|
||||||
|
if snapshot_forbidden_path "$rel"; then
|
||||||
|
echo "public snapshot: forbidden path: $rel" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
size=$(wc -c <"$file")
|
||||||
|
size=${size//[[:space:]]/}
|
||||||
|
if ((size > max_file_bytes)); then
|
||||||
|
echo "public snapshot: oversized file: $rel ($size bytes)" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
total=$((total + size))
|
||||||
|
count=$((count + 1))
|
||||||
|
if ((total > max_total_bytes || count > max_files)); then
|
||||||
|
echo "public snapshot: export exceeds aggregate size/count limits" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ -s $file ]] && ! LC_ALL=C grep -Iq . "$file"; then
|
||||||
|
echo "public snapshot: binary file rejected: $rel" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if LC_ALL=C grep -Eq "$private_unix|$private_windows|$private_wsl" "$file"; then
|
||||||
|
echo "public snapshot: private filesystem path indicator in $rel" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if LC_ALL=C grep -Eq -- "$private_key|$provider_token" "$file"; then
|
||||||
|
echo "public snapshot: key or credential indicator in $rel" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if LC_ALL=C grep -Eiq -- "$private_commit_id" "$file"; then
|
||||||
|
echo "public snapshot: private commit identifier in $rel" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if LC_ALL=C grep -Fq -- "$private_repository" "$file"; then
|
||||||
|
echo "public snapshot: private repository indicator in $rel" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
case $rel in
|
||||||
|
*.[mM][dD] | *.[mM][aA][rR][kK][dD][oO][wW][nN])
|
||||||
|
if LC_ALL=C grep -Eiq -- "$private_markdown_ref" "$file"; then
|
||||||
|
echo "public snapshot: excluded documentation reference in $rel" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done < <(find "$root" -type f -print0 | LC_ALL=C sort -z)
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
# Exact reviewed file allowlist for a host-neutral public source snapshot.
|
||||||
|
# Directories, globs, and implicit descendants are intentionally unsupported.
|
||||||
|
|
||||||
|
.editorconfig
|
||||||
|
.gitattributes
|
||||||
|
.gitignore
|
||||||
|
AI_CONTRIBUTIONS.md
|
||||||
|
CHANGELOG.md
|
||||||
|
CLA.md
|
||||||
|
CONTRIBUTING.md
|
||||||
|
COPYRIGHT
|
||||||
|
DCO.txt
|
||||||
|
GENERATED_CODE.md
|
||||||
|
GOVERNANCE.md
|
||||||
|
LICENSE
|
||||||
|
LICENSES.md
|
||||||
|
MAINTAINERS.md
|
||||||
|
OUTPUT_EXCEPTION.md
|
||||||
|
README.md
|
||||||
|
RELEASE.md
|
||||||
|
ROADMAP.md
|
||||||
|
SECURITY.md
|
||||||
|
SPEC.md
|
||||||
|
TRADEMARKS.md
|
||||||
|
cmd/himesan/contract_test.go
|
||||||
|
cmd/himesan/doc.go
|
||||||
|
cmd/himesan/main.go
|
||||||
|
cmd/himesan/main_test.go
|
||||||
|
cmd/himesan-release/main.go
|
||||||
|
contracts/diagnostic-codes-v1.txt
|
||||||
|
contracts/generated-provenance-v1.txt
|
||||||
|
contracts/himesan-cli-help-v1.txt
|
||||||
|
contracts/himesan-config-v1.schema.json
|
||||||
|
contracts/himesan-operation-output-v1.schema.json
|
||||||
|
contracts/himesan-version-output-v1.schema.json
|
||||||
|
docs/ARCHITECTURE.md
|
||||||
|
docs/BENCHMARKS.md
|
||||||
|
docs/BENCHMARK_RESULTS.md
|
||||||
|
docs/BRAND.md
|
||||||
|
docs/COMPATIBILITY.md
|
||||||
|
docs/DEVELOPMENT_SERVER.md
|
||||||
|
docs/DIAGNOSTICS.md
|
||||||
|
docs/LANGUAGE_SERVER.md
|
||||||
|
docs/LICENSING_INTENT.md
|
||||||
|
docs/SECURITY_EVIDENCE.md
|
||||||
|
docs/THREAT_MODEL.md
|
||||||
|
docs/V1_RELEASE_PLAN.md
|
||||||
|
go.mod
|
||||||
|
internal/compiler/abi_test.go
|
||||||
|
internal/compiler/analysis.go
|
||||||
|
internal/compiler/analysis_test.go
|
||||||
|
internal/compiler/backend.go
|
||||||
|
internal/compiler/compiler_test.go
|
||||||
|
internal/compiler/contract_test.go
|
||||||
|
internal/compiler/context.go
|
||||||
|
internal/compiler/diagnostic.go
|
||||||
|
internal/compiler/discover.go
|
||||||
|
internal/compiler/e2e_test.go
|
||||||
|
internal/compiler/fuzz_test.go
|
||||||
|
internal/compiler/model.go
|
||||||
|
internal/compiler/operation.go
|
||||||
|
internal/compiler/parse.go
|
||||||
|
internal/compiler/replace_unix.go
|
||||||
|
internal/compiler/replace_windows.go
|
||||||
|
internal/compiler/testdata/golden/basic.sando
|
||||||
|
internal/compiler/testdata/golden/basic.sando.go
|
||||||
|
internal/devserver/browser_evidence_test.go
|
||||||
|
internal/devserver/config.go
|
||||||
|
internal/devserver/config_test.go
|
||||||
|
internal/devserver/contract_test.go
|
||||||
|
internal/devserver/events.go
|
||||||
|
internal/devserver/process.go
|
||||||
|
internal/devserver/process_test.go
|
||||||
|
internal/devserver/process_unix.go
|
||||||
|
internal/devserver/process_windows.go
|
||||||
|
internal/devserver/process_windows_32.go
|
||||||
|
internal/devserver/process_windows_64.go
|
||||||
|
internal/devserver/proxy.go
|
||||||
|
internal/devserver/proxy_test.go
|
||||||
|
internal/devserver/supervisor.go
|
||||||
|
internal/devserver/supervisor_test.go
|
||||||
|
internal/devserver/watch.go
|
||||||
|
internal/devserver/watch_test.go
|
||||||
|
internal/lsp/features.go
|
||||||
|
internal/lsp/protocol.go
|
||||||
|
internal/lsp/protocol_test.go
|
||||||
|
internal/lsp/security_test.go
|
||||||
|
internal/lsp/server.go
|
||||||
|
internal/lsp/server_test.go
|
||||||
|
internal/lsp/workspace.go
|
||||||
|
internal/releaseartifact/artifact.go
|
||||||
|
internal/releaseartifact/artifact_test.go
|
||||||
|
internal/releaseartifact/evidence.go
|
||||||
|
internal/releaseartifact/evidence_test.go
|
||||||
|
internal/releaseartifact/macos.go
|
||||||
|
internal/releaseartifact/macos_test.go
|
||||||
|
internal/releaseartifact/native.go
|
||||||
|
internal/releaseartifact/native_test.go
|
||||||
|
internal/testpath/tempdir.go
|
||||||
|
internal/testpath/tempdir_test.go
|
||||||
|
internal/version/version.go
|
||||||
|
internal/version/version_test.go
|
||||||
|
sando/COPYRIGHT
|
||||||
|
sando/LICENSE
|
||||||
|
sando/README.md
|
||||||
|
sando/api_contract_test.go
|
||||||
|
sando/benchmark_test.go
|
||||||
|
sando/component.go
|
||||||
|
sando/component_test.go
|
||||||
|
sando/differential_test.go
|
||||||
|
sando/go.mod
|
||||||
|
sando/trust.go
|
||||||
|
sando/write.go
|
||||||
|
sando/write_test.go
|
||||||
|
sando/testdata/public-api-v1.txt
|
||||||
|
scripts/README.md
|
||||||
|
scripts/PUBLIC_SNAPSHOT.md
|
||||||
|
scripts/check-licenses.sh
|
||||||
|
scripts/export-public-snapshot.sh
|
||||||
|
scripts/native-gate.sh
|
||||||
|
scripts/package-macos.sh
|
||||||
|
scripts/package-native.sh
|
||||||
|
scripts/public-snapshot-lib.sh
|
||||||
|
scripts/public-snapshot.allow
|
||||||
|
scripts/release-check.sh
|
||||||
|
scripts/sign-notarize-macos.sh
|
||||||
|
scripts/test-public-snapshot.sh
|
||||||
|
scripts/verify-consumer.sh
|
||||||
|
scripts/verify-public-install.sh
|
||||||
|
scripts/verify-real-browser.sh
|
||||||
|
scripts/verify.sh
|
||||||
+45
-38
@@ -16,9 +16,11 @@ artifacts in the repository, pushes, or deploys.
|
|||||||
--version Candidate compiler version.
|
--version Candidate compiler version.
|
||||||
--runtime-tag Existing runtime tag retained by a compiler-only release.
|
--runtime-tag Existing runtime tag retained by a compiler-only release.
|
||||||
Omit only when publishing a matching new runtime tag.
|
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
|
||||||
by HIMESAN_RELEASE_EVIDENCE_DIR. Canonical beta prereleases may
|
named by HIMESAN_RELEASE_EVIDENCE_DIR and the four native
|
||||||
run their narrower publication preflight without this flag.
|
runner receipts named by HIMESAN_NATIVE_EVIDENCE_DIR.
|
||||||
|
Canonical beta prereleases may run their narrower publication
|
||||||
|
preflight without this flag.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +173,7 @@ if (( compiler_only == 1 )); then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
artifact_dir=$(mktemp -d "${TMPDIR:-/tmp}/himesan-release-check.XXXXXXXX")
|
artifact_dir=$(mktemp -d "${TMPDIR:-/tmp}/himesan-release-check.XXXXXXXX")
|
||||||
|
artifact_dir=$(CDPATH= cd -- "$artifact_dir" && pwd -P)
|
||||||
cleanup() {
|
cleanup() {
|
||||||
if [[ -n "${artifact_dir:-}" && -d "$artifact_dir" ]]; then
|
if [[ -n "${artifact_dir:-}" && -d "$artifact_dir" ]]; then
|
||||||
rm -rf -- "$artifact_dir"
|
rm -rf -- "$artifact_dir"
|
||||||
@@ -238,13 +241,22 @@ for pass in 1 2; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
./scripts/check-licenses.sh
|
./scripts/check-licenses.sh
|
||||||
|
./scripts/test-public-snapshot.sh
|
||||||
HIMESAN_RACE=1 ./scripts/verify.sh
|
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 '^FuzzGoDelimiterNeverPanics$' -fuzztime=20s
|
export GOCACHE="$artifact_dir/fuzz-go-build-cache"
|
||||||
go test ./internal/lsp -run '^$' -fuzz '^FuzzFrameReaderNeverPanics$' -fuzztime=20s
|
mkdir -p "$GOCACHE"
|
||||||
go test ./internal/lsp -run '^$' -fuzz '^FuzzDocumentPositionNeverPanics$' -fuzztime=20s
|
go test ./internal/compiler -run '^$' -fuzz '^FuzzCompileNeverPanics$' -fuzztime=20s -parallel=1
|
||||||
|
go test ./internal/compiler -run '^$' -fuzz '^FuzzGoDelimiterNeverPanics$' -fuzztime=20s -parallel=1
|
||||||
|
go test ./internal/lsp -run '^$' -fuzz '^FuzzFrameReaderNeverPanics$' -fuzztime=20s -parallel=1
|
||||||
|
go test ./internal/lsp -run '^$' -fuzz '^FuzzDocumentPositionNeverPanics$' -fuzztime=20s -parallel=1
|
||||||
|
(
|
||||||
|
cd sando
|
||||||
|
go test -run '^$' -fuzz '^FuzzWriteURLPolicy$' -fuzztime=20s -parallel=1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
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 ./...
|
||||||
@@ -253,24 +265,16 @@ go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
|
|||||||
go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
|
go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
|
||||||
)
|
)
|
||||||
|
|
||||||
printf '\n==> cross-compiling release binary smoke set\n'
|
target="$(go env GOOS)/$(go env GOARCH)"
|
||||||
for target in \
|
case "$target" in
|
||||||
linux/amd64 \
|
linux/amd64 | darwin/arm64) ;;
|
||||||
linux/arm64 \
|
*) printf 'error: release preflight requires a maintained native target, found %s\n' "$target" >&2; exit 1 ;;
|
||||||
darwin/amd64 \
|
esac
|
||||||
darwin/arm64 \
|
target_os=${target%/*}
|
||||||
windows/amd64 \
|
target_arch=${target#*/}
|
||||||
windows/arm64; do
|
printf '\n==> building supported native %s release binary\n' "$target"
|
||||||
target_os=${target%/*}
|
CGO_ENABLED=0 go build -trimpath -ldflags "$compiler_linker_flags" \
|
||||||
target_arch=${target#*/}
|
-o "$artifact_dir/himesan-$target_os-$target_arch" ./cmd/himesan
|
||||||
extension=''
|
|
||||||
if [[ "$target_os" == windows ]]; then
|
|
||||||
extension='.exe'
|
|
||||||
fi
|
|
||||||
CGO_ENABLED=0 GOOS="$target_os" GOARCH="$target_arch" \
|
|
||||||
go build -trimpath -ldflags "$compiler_linker_flags" \
|
|
||||||
-o "$artifact_dir/himesan-$target_os-$target_arch$extension" ./cmd/himesan
|
|
||||||
done
|
|
||||||
|
|
||||||
for required in \
|
for required in \
|
||||||
scripts/verify-public-install.sh \
|
scripts/verify-public-install.sh \
|
||||||
@@ -283,23 +287,26 @@ done
|
|||||||
|
|
||||||
if (( public_release == 1 )); then
|
if (( public_release == 1 )); then
|
||||||
evidence_dir=${HIMESAN_RELEASE_EVIDENCE_DIR:-}
|
evidence_dir=${HIMESAN_RELEASE_EVIDENCE_DIR:-}
|
||||||
|
native_evidence_dir=${HIMESAN_NATIVE_EVIDENCE_DIR:-}
|
||||||
if [[ -z "$evidence_dir" || ! -d "$evidence_dir" ]]; then
|
if [[ -z "$evidence_dir" || ! -d "$evidence_dir" ]]; then
|
||||||
printf 'error: --public requires HIMESAN_RELEASE_EVIDENCE_DIR\n' >&2
|
printf 'error: --public requires HIMESAN_RELEASE_EVIDENCE_DIR\n' >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
for evidence in \
|
if [[ -z "$native_evidence_dir" || ! -d "$native_evidence_dir" ]]; then
|
||||||
legal-review.md \
|
printf 'error: --public requires HIMESAN_NATIVE_EVIDENCE_DIR\n' >&2
|
||||||
cross-platform.md \
|
exit 1
|
||||||
security.md \
|
fi
|
||||||
development-supervisor.md \
|
go run ./cmd/himesan-release verify-native \
|
||||||
benchmark-methodology.md \
|
--directory "$native_evidence_dir" \
|
||||||
vanity-imports.md \
|
--repository gamertan/sandwich-hime \
|
||||||
signing-and-recovery.md; do
|
--commit "$(git rev-parse HEAD)" \
|
||||||
if [[ ! -s "$evidence_dir/$evidence" ]]; then
|
--tree "$(git rev-parse 'HEAD^{tree}')"
|
||||||
printf 'error: public release evidence is missing or empty: %s\n' "$evidence_dir/$evidence" >&2
|
go run ./cmd/himesan-release verify-evidence \
|
||||||
exit 1
|
--directory "$evidence_dir" \
|
||||||
fi
|
--repository gamertan/sandwich-hime \
|
||||||
done
|
--version "$version" \
|
||||||
|
--commit "$(git rev-parse HEAD)" \
|
||||||
|
--tree "$(git rev-parse 'HEAD^{tree}')"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "$(git status --porcelain=v1 --untracked-files=all)" ]]; then
|
if [[ -n "$(git status --porcelain=v1 --untracked-files=all)" ]]; then
|
||||||
|
|||||||
Executable
+80
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
Usage: scripts/sign-notarize-macos.sh --archive FILE --sha256 DIGEST --output DIR --keychain-profile NAME
|
||||||
|
|
||||||
|
Run this manually from Cole's signed-in macOS account. It never runs in CI.
|
||||||
|
It signs the native CLI, creates and signs a DMG, submits that DMG to Apple's
|
||||||
|
notary service, staples its ticket, and validates the distribution.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
archive=''
|
||||||
|
archive_sha256=''
|
||||||
|
output=''
|
||||||
|
profile=''
|
||||||
|
identity='Developer ID Application: Cole Speelman (5BXR9JCUBL)'
|
||||||
|
identifier='com.gamertan.sandwich-hime.himesan'
|
||||||
|
while (( $# > 0 )); do
|
||||||
|
case "$1" in
|
||||||
|
--archive) archive=$2; shift 2 ;;
|
||||||
|
--sha256) archive_sha256=$2; shift 2 ;;
|
||||||
|
--output) output=$2; shift 2 ;;
|
||||||
|
--keychain-profile) profile=$2; shift 2 ;;
|
||||||
|
*) usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [[ -z "$archive" || -z "$archive_sha256" || -z "$output" || -z "$profile" ]]; then usage; exit 2; fi
|
||||||
|
if [[ ! "$archive_sha256" =~ ^[0-9a-f]{64}$ ]]; then
|
||||||
|
printf 'error: --sha256 must be the approved lowercase archive digest\n' >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ "$(uname -s)/$(uname -m)" != Darwin/arm64 ]]; then
|
||||||
|
printf 'error: signing must run natively on Apple Silicon macOS\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
temporary=$(mktemp -d "${TMPDIR:-/tmp}/himesan-notarize.XXXXXXXX")
|
||||||
|
temporary=$(CDPATH= cd -- "$temporary" && pwd -P)
|
||||||
|
cleanup() { rm -rf -- "$temporary"; }
|
||||||
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
|
||||||
|
go run ./cmd/himesan-release extract-macos \
|
||||||
|
--archive "$archive" --sha256 "$archive_sha256" --output "$temporary"
|
||||||
|
root=$(find "$temporary" -mindepth 1 -maxdepth 1 -type d -print -quit)
|
||||||
|
binary="$root/himesan"
|
||||||
|
[[ -x "$binary" ]] || { printf 'error: archive does not contain executable himesan\n' >&2; exit 1; }
|
||||||
|
|
||||||
|
codesign --force --options runtime --timestamp \
|
||||||
|
--identifier "$identifier" --sign "$identity" "$binary"
|
||||||
|
codesign --verify --strict --verbose=2 "$binary"
|
||||||
|
go run ./cmd/himesan-release finalize-macos \
|
||||||
|
--directory "$root" \
|
||||||
|
--unsigned-archive-sha256 "$archive_sha256" \
|
||||||
|
--identity "$identity" --identifier "$identifier" \
|
||||||
|
--finalized-at "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||||
|
codesign --verify --strict --verbose=2 "$binary"
|
||||||
|
|
||||||
|
mkdir -p -- "$output"
|
||||||
|
version=$(basename "$root")
|
||||||
|
dmg="$output/$version.dmg"
|
||||||
|
if [[ -e "$dmg" || -e "$dmg.sha256" ]]; then
|
||||||
|
printf 'error: signed distribution output already exists: %s\n' "$dmg" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
hdiutil create -quiet -fs HFS+ -format UDZO -volname "$version" -srcfolder "$root" "$dmg"
|
||||||
|
codesign --force --timestamp --sign "$identity" "$dmg"
|
||||||
|
codesign --verify --strict --verbose=2 "$dmg"
|
||||||
|
xcrun notarytool submit "$dmg" --keychain-profile "$profile" --wait
|
||||||
|
xcrun stapler staple "$dmg"
|
||||||
|
xcrun stapler validate "$dmg"
|
||||||
|
codesign --verify --strict --verbose=2 "$dmg"
|
||||||
|
spctl --assess --type open --context context:primary-signature --verbose=2 "$dmg"
|
||||||
|
dmg_sha256=$(shasum -a 256 "$dmg" | awk '{print $1}')
|
||||||
|
printf '%s %s\n' "$dmg_sha256" "$(basename "$dmg")" >"$dmg.sha256"
|
||||||
|
chmod 0444 "$dmg" "$dmg.sha256"
|
||||||
|
printf 'Signed, notarized, and stapled distribution: %s\n' "$dmg"
|
||||||
Executable
+305
@@ -0,0 +1,305 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
project_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
# shellcheck source=public-snapshot-lib.sh
|
||||||
|
source "$project_root/scripts/public-snapshot-lib.sh"
|
||||||
|
exporter=$project_root/scripts/export-public-snapshot.sh
|
||||||
|
temporary=$(mktemp -d)
|
||||||
|
cleanup() {
|
||||||
|
rm -rf -- "$temporary"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
bash -n "$project_root/scripts/public-snapshot-lib.sh" "$exporter"
|
||||||
|
if grep -Eq 'git[[:space:]]+(init|commit|tag|push|remote)([[:space:]]|$)' "$exporter"; then
|
||||||
|
echo "exporter contains a forbidden Git mutation command" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for required in COPYRIGHT OUTPUT_EXCEPTION.md sando/COPYRIGHT; do
|
||||||
|
grep -Fxq "$required" "$project_root/scripts/public-snapshot.allow" || {
|
||||||
|
echo "required legal boundary is absent from public allowlist: $required" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
snapshot_status_is_clean ""
|
||||||
|
if snapshot_status_is_clean " M reviewed.go"; then
|
||||||
|
echo "dirty status was accepted" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
snapshot_path_is_at_or_below /var/tmp / || {
|
||||||
|
echo "filesystem-root boundary did not contain an absolute path" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if snapshot_path_is_at_or_below /safe-ish /safe; then
|
||||||
|
echo "path boundary accepted a sibling prefix" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
for path in .gitea/workflows/verify.yml .github/workflows/verify.yml private/notes.md \
|
||||||
|
internal/integration/product/test.go go.work build/output.exe data/private.db history/prototype.go; do
|
||||||
|
if ! snapshot_forbidden_path "$path"; then
|
||||||
|
echo "private/build path was not rejected: $path" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
snapshot_forbidden_path README.md && { echo "safe path was rejected" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Exercise the scanner against the complete proposed policy, including these
|
||||||
|
# uncommitted exporter files, so the next clean commit cannot reveal a
|
||||||
|
# self-triggering detector or a missing reviewed path.
|
||||||
|
proposed_tree=$temporary/proposed
|
||||||
|
mkdir -p "$proposed_tree"
|
||||||
|
while IFS= read -r line || [[ -n $line ]]; do
|
||||||
|
[[ -n $line && ${line:0:1} != '#' ]] || continue
|
||||||
|
[[ -f $project_root/$line && ! -L $project_root/$line ]] || {
|
||||||
|
echo "reviewed allowlist path is missing or not regular: $line" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
mkdir -p "$proposed_tree/$(dirname -- "$line")"
|
||||||
|
cp -p -- "$project_root/$line" "$proposed_tree/$line"
|
||||||
|
done <"$project_root/scripts/public-snapshot.allow"
|
||||||
|
snapshot_validate_export_tree "$proposed_tree"
|
||||||
|
(cd "$proposed_tree" && bash scripts/check-licenses.sh)
|
||||||
|
private_home_pattern='/'home'/'cole
|
||||||
|
private_commit_pattern='80bed136''75e8'
|
||||||
|
private_tag_pattern='prototype-''2025'
|
||||||
|
if LC_ALL=C grep -IRq -e "$private_home_pattern" -e "$private_commit_pattern" -e "$private_tag_pattern" "$proposed_tree"; then
|
||||||
|
echo "public allowlist contains a private path or history identifier" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
safe_tree=$temporary/safe
|
||||||
|
mkdir -p "$safe_tree"
|
||||||
|
printf 'ordinary reviewed source\n' >"$safe_tree/source.go"
|
||||||
|
snapshot_validate_export_tree "$safe_tree"
|
||||||
|
|
||||||
|
# Public prose and ordinary public links are allowed. Source-code policy
|
||||||
|
# fixtures may name excluded roots without becoming documentation links.
|
||||||
|
printf '%s\n' 'Private development records remain separate.' \
|
||||||
|
'[Releases](docs/releases.md)' '[History](https://example.invalid/history/releases)' \
|
||||||
|
'[Design](private-design.md)' >"$safe_tree/README.md"
|
||||||
|
printf '%s\n' '// Reject private/notes.md in an export policy.' >"$safe_tree/scanner_test.go"
|
||||||
|
snapshot_validate_export_tree "$safe_tree"
|
||||||
|
|
||||||
|
markdown_tree=$temporary/markdown
|
||||||
|
mkdir -p "$markdown_tree"
|
||||||
|
for reference in '[Audit](private/audit.md)' '[Audit](./private/audit.md#resume)' \
|
||||||
|
'[Audit](../../private/audit.md)' '[audit]: ../private/audit.md' '[audit]:private/audit.md' \
|
||||||
|
'`private/audit.md`' '<a href="private/audit.md">Audit</a>' \
|
||||||
|
'[Archive](history/story.md)' '[Audit](PRIVATE/AUDIT.md)'; do
|
||||||
|
printf '%s\n' "$reference" >"$markdown_tree/README.Md"
|
||||||
|
if snapshot_validate_export_tree "$markdown_tree" >"$temporary/markdown.log" 2>&1; then
|
||||||
|
echo "excluded documentation reference was accepted: $reference" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep -q 'excluded documentation reference in README.Md' "$temporary/markdown.log"
|
||||||
|
done
|
||||||
|
mv "$markdown_tree/README.Md" "$markdown_tree/notes.markdown"
|
||||||
|
if snapshot_validate_export_tree "$markdown_tree" >/dev/null 2>&1; then
|
||||||
|
echo "excluded reference in .markdown documentation was accepted" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
empty_tree=$temporary/empty
|
||||||
|
mkdir -p "$empty_tree"
|
||||||
|
: >"$empty_tree/empty.txt"
|
||||||
|
snapshot_validate_export_tree "$empty_tree"
|
||||||
|
|
||||||
|
symlink_tree=$temporary/symlink
|
||||||
|
mkdir -p "$symlink_tree"
|
||||||
|
printf 'target\n' >"$symlink_tree/target"
|
||||||
|
ln -s target "$symlink_tree/link"
|
||||||
|
if snapshot_validate_export_tree "$symlink_tree" >/dev/null 2>&1; then
|
||||||
|
echo "symlink tree was accepted" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
oversized_tree=$temporary/oversized
|
||||||
|
mkdir -p "$oversized_tree"
|
||||||
|
printf '123456789\n' >"$oversized_tree/large.txt"
|
||||||
|
if SNAPSHOT_MAX_FILE_BYTES=8 snapshot_validate_export_tree "$oversized_tree" >/dev/null 2>&1; then
|
||||||
|
echo "oversized file was accepted" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
binary_tree=$temporary/binary
|
||||||
|
mkdir -p "$binary_tree"
|
||||||
|
printf 'text\000binary\n' >"$binary_tree/blob.dat"
|
||||||
|
if snapshot_validate_export_tree "$binary_tree" >/dev/null 2>&1; then
|
||||||
|
echo "binary file was accepted" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
private_tree=$temporary/private
|
||||||
|
mkdir -p "$private_tree"
|
||||||
|
printf '/%s/%s/project/private.db\n' home developer >"$private_tree/path.txt"
|
||||||
|
if snapshot_validate_export_tree "$private_tree" >/dev/null 2>&1; then
|
||||||
|
echo "private filesystem path was accepted" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
private_commit_tree=$temporary/private-commit
|
||||||
|
mkdir -p "$private_commit_tree"
|
||||||
|
printf 'Private development source: %040d\n' 0 >"$private_commit_tree/ledger.txt"
|
||||||
|
if snapshot_validate_export_tree "$private_commit_tree" >/dev/null 2>&1; then
|
||||||
|
echo "private commit identifier was accepted" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
private_repository_tree=$temporary/private-repository
|
||||||
|
mkdir -p "$private_repository_tree"
|
||||||
|
printf 'gamertan/%s%s\n' 'sandwich-hime-' 'dev' >"$private_repository_tree/source.txt"
|
||||||
|
if snapshot_validate_export_tree "$private_repository_tree" >/dev/null 2>&1; then
|
||||||
|
echo "private repository identifier was accepted" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
credential_tree=$temporary/credential
|
||||||
|
mkdir -p "$credential_tree"
|
||||||
|
printf '%s%s\n' '-----BEGIN ' 'PRIVATE KEY-----' >"$credential_tree/secret.txt"
|
||||||
|
if snapshot_validate_export_tree "$credential_tree" >/dev/null 2>&1; then
|
||||||
|
echo "private key indicator was accepted" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Until these new exporter files themselves are committed, construct a
|
||||||
|
# review-only policy containing the intersection of the reviewed policy and
|
||||||
|
# the selected committed source ref. No repository or Git object is mutated.
|
||||||
|
review_policy=$temporary/review.allow
|
||||||
|
while IFS= read -r line || [[ -n $line ]]; do
|
||||||
|
[[ -n $line && ${line:0:1} != '#' ]] || continue
|
||||||
|
if git -C "$project_root" cat-file -e "HEAD:$line" 2>/dev/null &&
|
||||||
|
git -C "$project_root" diff --quiet HEAD -- "$line"; then
|
||||||
|
printf '%s\n' "$line" >>"$review_policy"
|
||||||
|
fi
|
||||||
|
done <"$project_root/scripts/public-snapshot.allow"
|
||||||
|
|
||||||
|
# A destination beneath the source worktree (including .git) must fail before
|
||||||
|
# staging creation. This test never removes anything from the source tree.
|
||||||
|
inside_name=himesan-export-must-not-exist-$$
|
||||||
|
inside_destination=$project_root/.git/$inside_name
|
||||||
|
[[ ! -e $inside_destination && ! -L $inside_destination ]] || {
|
||||||
|
echo "in-worktree destination unexpectedly exists before test" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if "$exporter" --source "$project_root" --ref HEAD --mode review \
|
||||||
|
--allowlist "$review_policy" --destination "$inside_destination" >/dev/null 2>&1; then
|
||||||
|
echo "exporter accepted a destination inside the source worktree" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
[[ ! -e $inside_destination && ! -L $inside_destination ]] || {
|
||||||
|
echo "failed in-worktree export created its destination" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if find "$project_root/.git" -maxdepth 1 -name ".${inside_name}.himesan-public-export.*" -print -quit | grep -q .; then
|
||||||
|
echo "failed in-worktree export created a staging directory" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A linked worktree stores its private Git directory and shared common Git
|
||||||
|
# directory outside that worktree root. Neither metadata location may become
|
||||||
|
# an export destination. The isolated repositories live entirely in $temporary.
|
||||||
|
linked_main=$temporary/linked-main
|
||||||
|
linked_worktree=$temporary/linked-worktree
|
||||||
|
git init -q "$linked_main"
|
||||||
|
printf 'reviewed linked-worktree source\n' >"$linked_main/source.go"
|
||||||
|
git -C "$linked_main" add source.go
|
||||||
|
git -C "$linked_main" -c user.name='Snapshot Test' -c user.email='snapshot@example.invalid' \
|
||||||
|
commit -qm 'seed isolated exporter test'
|
||||||
|
git -C "$linked_main" worktree add -q --detach "$linked_worktree" HEAD
|
||||||
|
linked_policy=$temporary/linked.allow
|
||||||
|
printf 'source.go\n' >"$linked_policy"
|
||||||
|
linked_git_dir=$(git -C "$linked_worktree" rev-parse --absolute-git-dir)
|
||||||
|
linked_common_dir=$(git -C "$linked_worktree" rev-parse --git-common-dir)
|
||||||
|
if [[ $linked_common_dir != /* ]]; then
|
||||||
|
linked_common_dir=$linked_worktree/$linked_common_dir
|
||||||
|
fi
|
||||||
|
linked_common_dir=$(snapshot_realpath_existing "$linked_common_dir")
|
||||||
|
|
||||||
|
assert_metadata_destination_rejected() {
|
||||||
|
local label=$1
|
||||||
|
local parent=$2
|
||||||
|
local name=$3
|
||||||
|
local rejected_destination=$parent/$name
|
||||||
|
[[ ! -e $rejected_destination && ! -L $rejected_destination ]] || {
|
||||||
|
echo "$label destination unexpectedly exists before test" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if "$exporter" --source "$linked_worktree" --ref HEAD --mode review \
|
||||||
|
--allowlist "$linked_policy" --destination "$rejected_destination" >/dev/null 2>&1; then
|
||||||
|
echo "exporter accepted destination inside $label" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
[[ ! -e $rejected_destination && ! -L $rejected_destination ]] || {
|
||||||
|
echo "failed $label export created its destination" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if find "$parent" -maxdepth 1 -name ".${name}.himesan-public-export.*" -print -quit | grep -q .; then
|
||||||
|
echo "failed $label export created a staging directory" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_metadata_destination_rejected 'linked-worktree Git directory' "$linked_git_dir" linked-private-destination
|
||||||
|
assert_metadata_destination_rejected 'shared Git common directory' "$linked_common_dir" linked-common-destination
|
||||||
|
|
||||||
|
first=$temporary/public-one
|
||||||
|
second=$temporary/public-two
|
||||||
|
"$exporter" --source "$project_root" --ref HEAD --mode review \
|
||||||
|
--allowlist "$review_policy" --destination "$first" >/dev/null
|
||||||
|
"$exporter" --source "$project_root" --ref HEAD --mode review \
|
||||||
|
--allowlist "$review_policy" --destination "$second" >/dev/null
|
||||||
|
diff -r --no-dereference "$first" "$second" >/dev/null
|
||||||
|
(cd "$first" && sha256sum -c PUBLIC-SNAPSHOT.sha256 >/dev/null)
|
||||||
|
|
||||||
|
for excluded in .git .gitea .github private history internal/integration go.work; do
|
||||||
|
[[ ! -e $first/$excluded && ! -L $first/$excluded ]] || {
|
||||||
|
echo "excluded path reached snapshot: $excluded" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
[[ -f $first/sando/component.go ]] || { echo "reviewed runtime source was not exported" >&2; exit 1; }
|
||||||
|
[[ -f $first/PUBLIC-SNAPSHOT.json && -f $first/PUBLIC-SNAPSHOT.sha256 ]] || {
|
||||||
|
echo "public provenance files are missing" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
grep -q '"schema_version":2' "$first/PUBLIC-SNAPSHOT.json"
|
||||||
|
grep -q '"export_mode":"review"' "$first/PUBLIC-SNAPSHOT.json"
|
||||||
|
if grep -Fq "$project_root" "$first/PUBLIC-SNAPSHOT.json" \
|
||||||
|
|| grep -Fq "$project_root" "$first/PUBLIC-SNAPSHOT.sha256" \
|
||||||
|
|| grep -q '@' "$first/PUBLIC-SNAPSHOT.json" \
|
||||||
|
|| grep -Eq '"(commit|tree|source_date_epoch)"' "$first/PUBLIC-SNAPSHOT.json"; then
|
||||||
|
echo "public provenance exposed a checkout path or email" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
occupied=$temporary/occupied
|
||||||
|
mkdir -p "$occupied"
|
||||||
|
printf 'do not delete\n' >"$occupied/owner-marker"
|
||||||
|
if "$exporter" --source "$project_root" --ref HEAD --mode review \
|
||||||
|
--allowlist "$review_policy" --destination "$occupied" >/dev/null 2>&1; then
|
||||||
|
echo "exporter accepted an existing destination" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep -q 'do not delete' "$occupied/owner-marker"
|
||||||
|
|
||||||
|
status=$(git -C "$project_root" status --porcelain=v1 --untracked-files=all)
|
||||||
|
release_destination=$temporary/release
|
||||||
|
if snapshot_status_is_clean "$status"; then
|
||||||
|
"$exporter" --source "$project_root" --ref HEAD --mode release \
|
||||||
|
--destination "$release_destination" >/dev/null
|
||||||
|
else
|
||||||
|
if "$exporter" --source "$project_root" --ref HEAD --mode release \
|
||||||
|
--destination "$release_destination" >"$temporary/release.log" 2>&1; then
|
||||||
|
echo "release export accepted a dirty source" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep -q 'release source worktree is dirty' "$temporary/release.log"
|
||||||
|
[[ ! -e $release_destination ]] || { echo "failed release created a destination" >&2; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "public snapshot export checks passed"
|
||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
temporary=$(mktemp -d "${TMPDIR:-/tmp}/himesan-consumer.XXXXXXXX")
|
||||||
|
temporary=$(CDPATH= cd -- "$temporary" && pwd -P)
|
||||||
|
cleanup() { rm -rf -- "$temporary"; }
|
||||||
|
trap cleanup EXIT HUP INT TERM
|
||||||
|
|
||||||
|
mkdir -p "$temporary/golden"
|
||||||
|
cp "$repo_root/internal/compiler/testdata/golden/basic.sando" "$temporary/golden/page.sando"
|
||||||
|
GOTOOLCHAIN=local go build -trimpath -o "$temporary/himesan" "$repo_root/cmd/himesan"
|
||||||
|
"$temporary/himesan" generate "$temporary/golden/page.sando"
|
||||||
|
(
|
||||||
|
cd "$temporary"
|
||||||
|
GOTOOLCHAIN=local go mod init example.test/himesan-consumer
|
||||||
|
GOTOOLCHAIN=local go mod edit -go=1.25
|
||||||
|
GOTOOLCHAIN=local go mod edit -replace=gamertan.com/sandwich-hime/sando="$repo_root/sando"
|
||||||
|
GOTOOLCHAIN=local go mod tidy
|
||||||
|
GOTOOLCHAIN=local go test ./...
|
||||||
|
)
|
||||||
|
|
||||||
|
printf 'Temporary Go 1.25 consumer compiled generated output successfully.\n'
|
||||||
@@ -101,6 +101,7 @@ if [[ "$browser_status" != 404 ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
scratch_dir=$(mktemp -d "${TMPDIR:-/tmp}/himesan-public-install.XXXXXXXX")
|
scratch_dir=$(mktemp -d "${TMPDIR:-/tmp}/himesan-public-install.XXXXXXXX")
|
||||||
|
scratch_dir=$(CDPATH= cd -- "$scratch_dir" && pwd -P)
|
||||||
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
|
chmod -R u+w -- "$scratch_dir" 2>/dev/null || true
|
||||||
|
|||||||
Executable
+35
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
cd "$repo_root"
|
||||||
|
|
||||||
|
chrome=${HIMESAN_CHROME:-}
|
||||||
|
if [[ -z "$chrome" ]]; then
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Darwin) chrome='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' ;;
|
||||||
|
Linux)
|
||||||
|
for candidate in google-chrome-stable google-chrome chromium chromium-browser; do
|
||||||
|
if command -v "$candidate" >/dev/null 2>&1; then
|
||||||
|
chrome=$(command -v "$candidate")
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
if [[ -z "$chrome" || ! -f "$chrome" || ! -x "$chrome" ]]; then
|
||||||
|
printf 'error: set HIMESAN_CHROME to a reviewed Chrome or Chromium executable\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'Real-browser executable: '
|
||||||
|
"$chrome" --version
|
||||||
|
printf 'Go toolchain: '
|
||||||
|
go version
|
||||||
|
HIMESAN_CHROME="$chrome" go test -count=1 -tags=himesan_browser_evidence \
|
||||||
|
./internal/devserver -run '^TestRealBrowserDevelopmentClient$' -v
|
||||||
|
go test -count=1 ./internal/devserver \
|
||||||
|
-run '^(TestSupervisorBuildsSwapsAndCleansUp|TestSupervisorClearsTargetWhenCurrentApplicationExits)$' -v
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[switch]$Race
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
|
||||||
Set-Location $RepoRoot
|
|
||||||
|
|
||||||
function Invoke-Checked {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$Label,
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[scriptblock]$Command
|
|
||||||
)
|
|
||||||
|
|
||||||
Write-Host "`n==> $Label"
|
|
||||||
& $Command
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "$Label failed with exit code $LASTEXITCODE"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-ModuleChecks {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$Directory,
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$Label
|
|
||||||
)
|
|
||||||
|
|
||||||
Push-Location $Directory
|
|
||||||
try {
|
|
||||||
Invoke-Checked "$Label`: go test" { go test ./... }
|
|
||||||
Invoke-Checked "$Label`: go vet" { go vet ./... }
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Pop-Location
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-SandoSources {
|
|
||||||
if (-not (Test-Path "internal/compiler/testdata/golden" -PathType Container)) {
|
|
||||||
return @()
|
|
||||||
}
|
|
||||||
|
|
||||||
return @(Get-ChildItem "internal/compiler/testdata/golden" -File -Filter "*.sando" |
|
|
||||||
Sort-Object FullName)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-GeneratedManifest {
|
|
||||||
$lines = foreach ($source in (Get-SandoSources)) {
|
|
||||||
$output = "$($source.FullName).go"
|
|
||||||
if (-not (Test-Path $output -PathType Leaf)) {
|
|
||||||
"missing $output"
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
$hash = (Get-FileHash -Algorithm SHA256 $output).Hash.ToLowerInvariant()
|
|
||||||
$modified = (Get-Item -LiteralPath $output).LastWriteTimeUtc.Ticks
|
|
||||||
"$hash $modified $output"
|
|
||||||
}
|
|
||||||
return ($lines -join "`n")
|
|
||||||
}
|
|
||||||
|
|
||||||
$TempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("himesan-verify-" + [guid]::NewGuid())
|
|
||||||
New-Item -ItemType Directory -Path $TempRoot | Out-Null
|
|
||||||
|
|
||||||
try {
|
|
||||||
Invoke-ModuleChecks "." "compiler module"
|
|
||||||
Invoke-Checked "compiler module: go build" {
|
|
||||||
go build -trimpath -o (Join-Path $TempRoot "himesan.exe") ./cmd/himesan
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path "sando/go.mod" -PathType Leaf)) {
|
|
||||||
throw "nested Apache runtime module sando/go.mod is missing"
|
|
||||||
}
|
|
||||||
Invoke-ModuleChecks "sando" "sando runtime module"
|
|
||||||
|
|
||||||
$Sources = @(Get-SandoSources)
|
|
||||||
if ($Sources.Count -eq 0) {
|
|
||||||
throw "compiler-owned golden .sando fixture is missing"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$SourcePaths = @($Sources | ForEach-Object { $_.FullName })
|
|
||||||
$CheckArgs = @("run", "./cmd/himesan", "check") + $SourcePaths
|
|
||||||
$GenerateArgs = @("run", "./cmd/himesan", "generate") + $SourcePaths
|
|
||||||
Invoke-Checked "golden generation: read-only freshness check" {
|
|
||||||
& go $CheckArgs
|
|
||||||
}
|
|
||||||
$Before = Get-GeneratedManifest
|
|
||||||
|
|
||||||
Invoke-Checked "golden generation: first deterministic pass" {
|
|
||||||
& go $GenerateArgs
|
|
||||||
}
|
|
||||||
$First = Get-GeneratedManifest
|
|
||||||
if ($Before -cne $First) {
|
|
||||||
throw "generation changed committed output after check declared it fresh"
|
|
||||||
}
|
|
||||||
|
|
||||||
Invoke-Checked "golden generation: second deterministic pass" {
|
|
||||||
& go $GenerateArgs
|
|
||||||
}
|
|
||||||
$Second = Get-GeneratedManifest
|
|
||||||
if ($First -cne $Second) {
|
|
||||||
throw "repeated generation changed output bytes or an unchanged timestamp"
|
|
||||||
}
|
|
||||||
|
|
||||||
Invoke-Checked "golden generation: final freshness check" {
|
|
||||||
& go $CheckArgs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($Race) {
|
|
||||||
Invoke-Checked "compiler module: race tests" { go test -race ./... }
|
|
||||||
Push-Location "sando"
|
|
||||||
try {
|
|
||||||
Invoke-Checked "sando runtime module: race tests" { go test -race ./... }
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Pop-Location
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "`n==> verification complete"
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
if (Test-Path $TempRoot -PathType Container) {
|
|
||||||
Remove-Item -LiteralPath $TempRoot -Recurse -Force
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -65,6 +65,7 @@ run_module_checks . "compiler module"
|
|||||||
|
|
||||||
log "compiler module: go build"
|
log "compiler module: go build"
|
||||||
build_dir=$(mktemp -d "${TMPDIR:-/tmp}/himesan-verify.XXXXXXXX")
|
build_dir=$(mktemp -d "${TMPDIR:-/tmp}/himesan-verify.XXXXXXXX")
|
||||||
|
build_dir=$(CDPATH= cd -- "$build_dir" && pwd -P)
|
||||||
cleanup() {
|
cleanup() {
|
||||||
if [[ -n "${build_dir:-}" && -d "$build_dir" ]]; then
|
if [[ -n "${build_dir:-}" && -d "$build_dir" ]]; then
|
||||||
rm -rf -- "$build_dir"
|
rm -rf -- "$build_dir"
|
||||||
|
|||||||
Reference in New Issue
Block a user