GuestCraft quality program
Questi contenuti non sono ancora disponibili nella tua lingua.
Security reviews use the deployment threat model to check trust boundaries, residual risks and the verification contract alongside these gates.
- Status: accepted for the “PMS v0.2” round; revised whenever a gate is added or changed
- Date: 2026-10-29
- Scope: code quality, test strength, supply chain, API contract, review process
- Related: ADR 0003 (toolchain), ADR 0005 (testing and gates), ADR 0016 (HTTP API), the earlier research
testing-quality-research.md, CONTRIBUTING
The owner’s first priority is code quality. This document compares the practices of mature open-source TypeScript projects and of the 2025–2026 standards (OpenSSF, SLSA, Google’s review guide) with what this repository enforces. It ranks the gaps and fixes the review protocol. Every tool in it is free and open source: no paid tier, no SaaS account, no runtime service.
Three rules apply throughout:
- A gate is evidence only if it can fail. Every new gate here proves that it rejects what it promises: Spectral lints a document of deliberate violations, and the coverage ratchet has its own tests. A green job that cannot go red is not counted.
- No number without its denominator. Coverage, mutation score and lint results are reported with their counts, and no number is presented as a measure of product completeness (AGENTS.md).
- Existing debt is listed, never hidden. A rule that the current code breaks is still turned on. The existing violations are listed one by one as warnings, so new code meets the full rule and the debt stays visible in every run.
1. What the repository already enforces
Section titled “1. What the repository already enforces”The repository starts from a high baseline. Each pull request goes through the required quality
job (.github/workflows/quality.yml), which runs:
- frozen install with
minimumReleaseAge(7 days, transitive dependencies included),trustPolicy: no-downgradeand build scripts allowed one by one; - Biome, with the recommended preset, extra rules and GritQL plugins against ambient I/O and clocks in the domain;
- dependency-cruiser boundaries, Knip dead code, and the license-direction check (AGPL → MIT only);
- REUSE lint, commitlint and actionlint on the workflows;
- TypeScript 7 in strict mode (
exactOptionalPropertyTypes,erasableSyntaxOnly); - one Vitest process with V8 coverage over every handwritten file, with per-package thresholds (core 95/90/95/95; money and identifiers 99/95/100/99 per file);
- fast-check property tests (1,000 runs) and real PostgreSQL 17 through Testcontainers.
Outside the quality job:
smoke(Docker Compose, required) builds the image and checks the empty instance.imagebuilds the release images natively on amd64 and arm64, checks their OCI labels and writes their SBOMs, without pushing. It runs on every push tomainand on pull requests that change the build inputs.releasekeeps the release pull request up to date. After a release pull request is merged, it publishes attested images to GHCR and the GitHub release (ADR 0039).browserruns Playwright on the production build, with axe checks in the roles suite. It is not a required check.- The nightly property run uses 10,000 cases in two time zones far from UTC.
- A weekly shifted-clock matrix runs.
- A nightly Channex drift check opens an issue when it fails.
- Actions are pinned to full commit SHAs. Renovate updates dependencies with its own 7-day delay.
mainis protected (requiredqualityandsmoke, an up-to-date branch, resolved conversations, administrators included, no force push). This was verified through the GitHub API on 2026-10-28.
2. Gap analysis
Section titled “2. Gap analysis”Effort: XS < 1 h, S ≈ half a day, M ≈ 1–3 days, L > 3 days. Priority:
- P0: cheap and high value, done in this change or due within days.
- P1: the next rounds.
- P2: when the trigger named in the row happens (public repository, first release, and so on).
| # | Practice (source) | Status in GuestCraft | Action | Priority | Effort |
|---|---|---|---|---|---|
| 1 | OpenAPI style linting enforcing the house API rules (Spectral, Zalando guidelines) | ADR 0016 planned it; nothing ran | Done. .spectral.yaml with 11 GuestCraft rules (snake_case, no RPC paths, RFC 9457 problems, Request-Id, If-Match and ETag, operationId form, security). It runs in the required quality job, with a self-test (section 4.1). |
P0 | done |
| 2 | Secret scanning of the whole history (gitleaks) | None; GitHub secret scanning is paid on private repositories (GitHub Secret Protection) | Done. gitleaks 8.30.1 over all commits in the required quality job, with an allowlist of synthetic values only (section 4.2) |
P0 | done |
| 3 | Known-vulnerability scanning of the lockfile (osv-scanner) | Renovate vulnerabilityAlerts is configured but depends on Dependabot alerts, which are off on the repository |
Done. The dependencies workflow fails a pull request only on vulnerabilities it adds, and runs a full nightly scan that opens an issue. 8 advisories on main were fixed (section 4.3). |
P0 | done |
| 4 | Coverage ratchet: coverage never falls (Vitest coverage) | Static per-package thresholds only; a package at 99% could fall to 95% unnoticed | Done. pnpm coverage:ratchet against tooling/quality/coverage-baseline.json (main’s values), in quality (section 4.4) |
P0 | done |
| 5 | Mutation testing of the domain (StrykerJS) | Planned in ADR 0005, not run | Done. StrykerJS on packages/core with a nightly incremental job and a failing overall score threshold. Measurements and remaining gaps are in section 4.5. |
P0 | done |
| 6 | Turn on Dependabot alerts (free on every plan, GitHub plans) | Done on 2026-10-28 | Renovate’s existing vulnerabilityAlerts now has alerts to act on. |
P0 | done |
| 7 | Remove the Stryker release-age exception | minimumReleaseAgeExclude lists @stryker-mutator/*@10.1.1, which turns 7 days old on 2026-10-30 |
Delete the block on or after 2026-10-31 | P0 | XS |
| 8 | Optimistic concurrency on every write (public API rule) | 8 PUTs have no If-Match and 3 item reads have no ETag (Spectral finding S3); concurrent edits overwrite each other | Add versions and If-Match to properties, room types, units, housekeeping, extras and the Channex mapping/connection; then remove the overrides | P1 | M |
| 9 | Changed-line coverage, 90% (100% for money, isolation, sync) (ADR 0005; diff-cover, Apache-2.0) | Not enforced | Run diff-cover on Vitest’s lcov.info against the base commit in quality |
P1 | S |
| 10 | Breaking-change detection on the contract (oasdiff, Apache-2.0) | Planned in ADR 0016 | oasdiff breaking between the base commit’s openapi/v1.json and the pull request’s, failing on errors unless the revision is a new major |
P1 | S |
| 11 | Mutation threshold and wider scope | An overall break threshold now fails the nightly core job (section 4.5); no per-module gate or pull-request mutation run exists. |
Add a pull-request run on changed core files (--incremental), consider per-module thresholds, and extend to countries and the pure parts of api. |
P1 | S |
| 12 | Workflow security linting (zizmor, MIT; actionlint, MIT) | actionlint 1.7.12 runs in quality (pnpm actions:lint), checksum-pinned in tooling/requirements/tools.json, since 2026-10-28. zizmor: manual review only. The Nx “s1ngularity” compromise started from a workflow injection (Nx post-mortem) |
Add zizmor the same way, in quality |
P1 | XS |
| 13 | Complexity limits (Biome noExcessiveCognitiveComplexity) |
Off; 159 functions exceed the default of 15 (46 in core, 53 in web) | Turn on as error at 15 for new code, with the 159 existing functions listed in an override; reduce the list with each refactor | P1 | M |
| 14 | Browser end-to-end as a required check | browser runs on every pull request but is not required |
Owner: add front-desk to the required checks once it has been stable for 2 weeks |
P1 | XS |
| 15 | Accessibility on every screen (WCAG 2.2 AA, axe-core, MPL-2.0) | axe runs in component tests and in the roles end-to-end suite | Run axe on each screen visited by every Playwright spec, tags up to wcag22aa (ADR 0005) |
P1 | S |
| 16 | Fix the other contract findings (S1, S2, S4, S5) | Listed as warnings | S2 and S4 now, with their owners. S1, a path rename, needs a contract decision because it changes /api/v1 URLs. |
P1 | S–M |
| 17 | Duplicate code (jscpd, MIT) | Not measured | Measure once. Gate only on new duplication above 50 tokens, with a baseline | P2 | S |
| 18 | API fuzzing with authorization fixtures (Schemathesis, MIT; ADR 0005) | Not run | Nightly against the server with two workspaces and two users | P2 | M |
| 19 | Performance budgets | Done. Deterministic gzip and brotli limits for initial JavaScript and CSS on every built web page, enforced by browser (section 4.8). |
Measure field performance separately when a stable synthetic environment is available; keep the asset gate. | P2 | done |
| 20 | Custom SAST rules (Semgrep CE engine LGPL-2.1; its registry rules are not OSS) | Biome + GritQL cover the domain’s import and I/O rules | Write GuestCraft’s own rules (raw SQL outside db, missing permission checks on routes) only where GritQL cannot express them. Never vendor registry rules. |
P2 | M |
| 21 | Type coverage (type-coverage, MIT) |
Not possible: it needs the TypeScript compiler API, which TS 7.0 does not ship (issue) | Revisit with TypeScript 7.1; noExplicitAny already holds the line |
P2 | S |
| 22 | OpenSSF Scorecard (checks) | Implemented: pinned official Action on main pushes, weekly and branch-protection changes; SARIF goes to code scanning and results are published. First CLI score: 6.6/10 (section 4.7). | Triage each low check below; target no unexplained critical red check. | P2 | done |
| 23 | CodeQL, GitHub secret scanning with push protection, dependency review | Implemented: CodeQL default setup, secret scanning with push protection and private vulnerability reporting are enabled; pinned dependency review blocks new high-severity vulnerabilities on pull requests, without comments. | Keep pnpm licenses:check as the license gate. Verify CodeQL’s coverage of new commits (row 22). |
P2 | done |
| 24 | SBOM, provenance and signed releases (SLSA 1.2, actions/attest, Syft, release-please) |
Automated, not yet exercised (2026-10-28, ADR 0039): release-please release pull requests and changelog. Each release gets amd64/arm64 images on GHCR built natively without cache, SLSA build provenance and SPDX SBOM attestations per platform image (provenance also per index), and SPDX and CycloneDX SBOMs attached to the release. The release stays a draft until all of that succeeds. The image dry run proves the build and the SBOMs on every push to main. Claimed level: SLSA Build L2. No version released yet. No separate cosign signature: the attestations are keyless Sigstore bundles. |
First release: the owner sets up the GitHub App, then the checklist proves the push, attestations and verification commands (releasing). Then: npm trusted publishing with provenance for the MIT SDK at its first publication; a cosign verification recipe tested on a real release; an N−1 → N upgrade test in CI; a reusable-workflow signer for Build L3. | P1 | M |
| 25 | OSPS Baseline and Best Practices Badge (baseline, criteria) | Self-assessment done for the passing level: openssf-best-practices.md answers all 67 criteria with evidence (60 Met, 3 Unmet, 4 N/A). The one unmet MUST is release_notes, which needs a first release. Not registered yet. |
Owner: cut v0.1.0 with release notes, then register at bestpractices.dev and paste the answers. Assess OSPS Baseline L1/L2 and the silver level afterwards. |
P1 | S |
| 26 | Documentation structure (Diátaxis) and link checking (lychee, MIT/Apache-2.0) | ADRs, READMEs, developer guides; no link check | Classify docs/developer pages as tutorial, how-to, reference or explanation; check links nightly |
P2 | S |
| 27 | Public API reports for the SDK (API Extractor, MIT; TSDoc) | SDK generated from OpenAPI, no API report | Commit an API Extractor report for @guestcraft/sdk before its first npm release |
P2 | S |
| 28 | Betterleaks, the successor of gitleaks | gitleaks receives security fixes only (README) | Compare both on the same history before the next tool update | P2 | XS |
| 29 | Fast core-level tests for the use-case modules (ADR 0005: “Unit … next to the code”) | Partial: fast use-case tests now exercise reservation projections and state transitions, rate quotes, folio policy postings, ARI safety and booking-import recovery using contract-checked memory ports and injected HTTP transports (section 4.5). | Continue independent tests for the remaining surviving and uncovered mutants, especially ARI, booking import, reservation policy and room-building branches. The mutation no-coverage count measures test reach, not product completion. | P1 | L |
| 30 | Deterministic coverage | The db ariProperties and dueChannex scans have direct integration tests (worker-scans.integration.test.ts). One booking-import branch still depends on a scheduler tick (section 4.4). |
Inject the clock into the scheduler tests for that branch, then lower the ratchet’s 3-item allowance. | P1 | S |
3. Review protocol
Section titled “3. Review protocol”3.1 Which pull requests need which reviews
Section titled “3.1 Which pull requests need which reviews”| Class | Examples | Independent reviews before merge |
|---|---|---|
| Critical | Money and folios, availability and allocation, reservation state, RLS, grants and migrations, authentication, permissions, Channex writes, secrets handling, public contract removals | The owner, plus two independent reviews from reviewers who did not write the change, at least one of them adversarial (it tries to break the claims). A review’s findings and their answers are recorded in the pull request. |
| Substantive | A new feature across layers, a new public operation, a new dependency, a workflow change, a new gate | The owner, plus one independent review |
| Routine | Copy and translations, documentation, refactors without behaviour change, test-only changes, dependency patches from Renovate that pass every gate | The owner |
Code quality comes first. Every critical and substantive pull request also gets a dedicated code-quality review from a fresh-context reviewer who did not write the change, separate from the correctness reviews: it judges whether the code is simple, clear, consistent with the code around it and maintainable, and whether its tests are good tests (question 1 below). Correct code that is convoluted does not pass. A pull request that went through several fix rounds is reviewed again as a whole, because fixes added one at a time tend to pile special cases on top of each other.
An independent review can come from a person or from an AI reviewer started from a fresh context. Give the reviewer the diff and the claims, not the author’s conclusions, so that the review is not anchored on them. Self-review never counts as independent. GitHub cannot enforce two approvals with a single maintainer, so this is a process rule until there are enough reviewers (OpenSSF Scorecard, Code-Review).
3.2 Checklist
Section titled “3.2 Checklist”Reviewers follow Google’s guide: approve when the change definitely improves the health of the code, even if it is not perfect. Mark optional remarks as “Nit:”. Answer within one working day. Prefer changes of about 100 lines, and keep refactors separate from behaviour changes. For GuestCraft, every review answers these questions:
- Code quality. Is this the simplest design that meets the requirement, without speculative
abstractions, needless indirection or mode flags? Are functions small and cohesive, names
precise (the glossary’s domain terms), comments about why and true to the code, and is there no
duplicated logic and no dead code? Does it use the repository’s idioms: the
Resultand error model of ADR 0008, injected time and identifiers, the existing helpers? Are the types honest, with noanyand no casts that hide errors? Each finding names the file and line and proposes a specific change. - Correctness. Does the code do what the description claims, including edge cases: empty
inputs, time zones and DST, half-open date ranges
[arrival, departure), currency exponents, concurrency and retries? - Isolation and security. Can any path read or write another workspace’s or organization’s data? Are RLS, composite foreign keys, permission checks and audit records present on every new write? Do logs, errors, fixtures or screenshots carry secrets or personal data?
- API design. Are the endpoints resource-oriented, with no RPC verbs and with actions only
for real state transitions? Do they use snake_case and RFC 9457 problems with documented
codes? Do writes take a strong ETag in
If-Match? Is each change an addition only, or does the revision say otherwise? Are the OpenAPI document and the SDK regenerated?pnpm openapi:lintenforces part of this. - Tests that prove the claims. Does each claim in the description have a test at the level where it could fail: unit, property, real PostgreSQL, browser? Do the expectations come from the specification rather than mirror the code? Is there a regression test for each fix, and does denied access fail as it should? A mutant that survives in the changed code is a question for the author.
- Domain boundaries. Is the core free of infrastructure, with time, randomness and identifiers injected? Is country-specific behaviour in a country pack? Are licences in the right direction?
- UX, for UI changes: is every new text in the English catalog, complete and natural? Do the loading, empty, error and permission states exist? Are there keyboard paths, field-linked errors and semantic tokens? Are there screenshots of the result?
- Completeness report. Does the description separate implemented, partial and planned behaviour, and name the remaining gaps (CONTRIBUTING, “Functional completeness”)?
3.3 Gates that must be green
Section titled “3.3 Gates that must be green”Required by branch protection:
quality, which runs:- lint, boundaries, dead code and licences;
- REUSE and commitlint;
- gitleaks and Spectral;
- types;
- every test with coverage;
- the coverage ratchet.
smoke: the Docker image starts and answers.
Must be green before merge, though not yet required by GitHub:
browser: Playwright on the production build.dependencies: no new known vulnerability.dependency review: no newly introduced high-severity vulnerability (license policy stays inpnpm licenses:check).
Not required before merge:
mutation: the nightly score has a failing threshold, but the job is not a pull-request gate.nightly: properties at 10,000 runs.clock guard.channex-drift.
A red nightly job is triaged within a working day.
Merging with a failing or pending check, or claiming a check passed when it did not run, is never allowed (AGENTS.md).
4. What this change implemented
Section titled “4. What this change implemented”All binaries are pinned, by version and SHA-256, in
tooling/requirements/tools.json. They are downloaded on
first use into .cache/tools/ by tooling/scripts/src/tool.ts,
which refuses a file whose hash differs. npm tools go through the pnpm catalog. Renovate does not
update the binary pins, because it cannot recompute their hashes. Update them by hand, with the
version, every URL and every hash together.
| Tool | Version | Licence | Cost | How it is pinned | Command |
|---|---|---|---|---|---|
| Spectral CLI | 6.17.0 | Apache-2.0 | free | release binary + SHA-256 (computed: the project publishes no checksums) | pnpm openapi:lint |
| gitleaks | 8.30.1 | MIT | free; the CLI, not the gitleaks Action, whose licence is a proprietary EULA | release archive + the project’s SHA-256 | pnpm secrets:scan |
| osv-scanner | 2.7.0 | Apache-2.0 | free; queries the public osv.dev API | release binary + the project’s SHA-256 | pnpm deps:scan |
| StrykerJS core + Vitest runner | 10.1.1 | Apache-2.0 | free; the dashboard reporter is not used | pnpm catalog | pnpm mutation:core |
Why Spectral is a binary. The npm package @stoplight/spectral-cli cannot be installed under
this workspace’s policy. One of its dependencies, rollup@2.80.0, comes from its ruleset
bundler, and pnpm’s trustPolicy rejects it: its publishing evidence is weaker than that of
earlier rollup releases. The npm package also depends on @scarf/scarf, an install-time
telemetry package. The standalone binary avoids both, and the workspace policy stays as it is.
4.1 Spectral: the OpenAPI rules and their findings
Section titled “4.1 Spectral: the OpenAPI rules and their findings”.spectral.yaml extends spectral:oas (recommended) and adds 11 rules,
all errors:
gc-path-snake-case,gc-no-rpc-paths,gc-property-snake-case,gc-query-parameter-snake-case,gc-operation-id;gc-default-problem,gc-errors-are-problems,gc-request-id;gc-writes-need-if-match,gc-item-get-etag,gc-operation-security.
Before linting the contract, pnpm openapi:lint lints
tooling/spectral/violations.json. It fails unless
every gc- rule reports an error there. Spectral reads JSON with its YAML parser, which rejects
the tab indentation of openapi/v1.json line by line and then overflows its stack. The script
therefore lints copies indented with spaces, under reports/openapi/.
Results on openapi/v1.json (revision 2.0.0, 85 paths, 108 operations). 0 errors. These hold
everywhere:
- every JSON member and query parameter is snake_case;
- every operationId has the namespace form and is unique;
- every operation has a
defaultproblem answer; - every 4xx/5xx answer is
application/problem+jsononly.
The findings below are existing debt. Each is an override with a warning, path by path, so new paths get the full rule:
| Id | Rule | Finding | Assessment |
|---|---|---|---|
| S1 | gc-path-snake-case |
12 paths use kebab-case segments (room-types, rate-plans, front-desk, tax-categories, credits-due, cancellation-simulation) or a file suffix (reports/performance.csv). Other paths use snake_case (/channex/room_types, undo_check_in). |
A real inconsistency with ADR 0016. Renaming changes /api/v1 URLs that installed clients use, so it needs a contract decision: aliases, or the next major. The CSV export could use Accept: text/csv instead. |
| S2 | gc-no-rpc-paths |
POST /members/remove |
An RPC endpoint, against the public API rule. It should be DELETE /members/{member_id}. /members/role (POST), /invitations/resend and /invitations/revoke (POST on the collection, identifier in the body) break the same rule in a form no path rule can recognize reliably: they are review findings, to become PUT /members/{id}/role and /invitations/{id}/… actions. |
| S3 | gc-writes-need-if-match, gc-item-get-etag |
8 PUTs require no If-Match: /properties/{id}, room types, units, unit housekeeping, bulk housekeeping, extras, /channex/mapping and /channex/connection. 3 item GETs return no ETag: property, room type and unit. |
The most serious finding. The code confirms the gap is behavioural, not just documentation: these routes neither return nor check a version, so two people editing the same room type silently overwrite each other. Gap row 8. |
| S4 | gc-operation-security, gc-request-id |
GET /roles declares no security. It and DELETE /roles/custom/{role_id} (204) do not document Request-Id. |
The role matrix may be public on purpose, but nothing says so; document the choice, or authenticate the route. The missing header is documentation only, since the middleware sets it on every answer. |
| S5 | info-contact (Spectral built-in) |
info.contact is missing |
Add the project’s issue tracker or SECURITY.md URL |
| S6 | oas3-unused-component (turned off) |
ContentTooLarge, MethodNotAllowed and UnsupportedMediaType are not referenced |
Intended: ADR 0016 describes these shared answers once under components.responses |
| S7 | operation-tag-defined (turned off) |
The 108 operations use tags without a global tags list |
Add the list with descriptions; Scalar uses them to group the reference |
4.2 gitleaks
Section titled “4.2 gitleaks”pnpm secrets:scan scans every commit of the checked-out history (--log-opts=HEAD): in a pull
request that is main plus the pull request’s commits. Other branches are scanned by their own pull
requests’ checks, under their own allowlist (a branch without a pull request is not scanned on its own); scanning every ref made unrelated pull requests fail on another
branch’s synthetic fixtures. At first it scanned 390 commits, about 60 MB, in about 25
seconds. The first run reported 63 findings at 13 distinct locations, all false positives:
- synthetic test credentials built on
0123456789abcdef(database owner passwords, cron secrets, integration keys); - example
Idempotency-KeyUUIDs in an API test and in the analysis.
.gitleaks.toml allowlists those values, never files or directories, so a
real secret pasted into a test is still reported. After that, no leaks are found.
4.3 osv-scanner
Section titled “4.3 osv-scanner”The first scan of main’s pnpm-lock.yaml (887 packages) found 7 advisories in 2 packages. The
Stryker tree would have added 4 more in qs.
| Package | Advisories | Pulled in by | Fix |
|---|---|---|---|
| fast-uri 3.1.8 | GHSA-w74x-v6f9-j3xg, GHSA-wjr9-j795-jxcr, GHSA-333x-w4r6-6crp, GHSA-xhq7-xmqj-769h | ajv (commitlint, Stryker) | lockfile update to 3.1.9, inside the declared range |
| js-yaml 4.2.0 | GHSA-2883-xcg3-v3hh, GHSA-52cp-r559-cp3m, GHSA-5p4m-2wfm-xmqj (CPU exhaustion) | @hey-api/json-schema-ref-parser, which pins 4.2.0 exactly |
scoped override to 4.3.2; generate:check confirms the SDK output is unchanged |
| qs 6.15.1 | GHSA-4mjr-xmp4-gh2g, GHSA-6pq8-jhm2-2vcw, GHSA-q8mj-m7cp-5q26, GHSA-x5fp-wj9c-mxmx | typed-rest-client (Stryker’s dashboard reporter, unused), which pins 6.15.1 exactly |
scoped override to 6.16.1 |
All of these were in development tooling only, and none in the runtime image. The overrides in
pnpm-workspace.yaml are scoped to their parent and are removed when the parent ships the fix.
Accepted exceptions belong in osv-scanner.toml, with a reason and an
expiry of at most 30 days. The mobile app’s Expo toolchain (ADR 0041) brought three that had no
compatible fix. node-forge’s arrived in 1.4.2, inside the declared ^1.4.0, and a lockfile update
replaced its exception and cleared 11 later advisories. The other two remain; each is reached only
from @guestcraft/mobile, by tools that run on a developer’s machine, and neither the Docker
images nor the web build install it (Next ships its own compiled zod 3 in next/dist/compiled,
outside the lockfile, already on main and unchanged by the app):
| Package | Advisory | Pulled in by | Expiry, and the way out |
|---|---|---|---|
| deepmerge 4.3.1 | GHSA-gcqx-w48m-w2h4 | @expo/config, jest-config |
2026-11-28; no fixed release yet |
| zod 3.25.76 | GHSA-mrhx-c7xc-m3w5 | @expo/cli (3.x line) |
2026-11-28; fixed only in 4.6.6 |
Two more are fixed only in new majors that every parent’s range excludes, and that remove or change APIs and matching rules, so an override would change what the tools match. Both are reached only by development tools matching the repository’s own patterns, and no Docker image installs them:
| Package | Advisory | Pulled in by | Expiry, and the way out |
|---|---|---|---|
| glob 10.5.0, 13.0.6 | GHSA-3g3m-2fjr-6pw4 | Jest, testcontainers’ archiver, the Expo CLI and config | 2026-11-17; fixed only in glob 14.0.2 |
| minimatch 3.1.5, 5.1.9, 9.0.9, 10.2.6 | GHSA-g3hv-75wg-m249 | glob, Stryker, @expo/fingerprint |
2026-11-17; fixed only in minimatch 11.0.1; 3.1.5 and 5.1.9 lack the vulnerable code |
4.4 Coverage ratchet
Section titled “4.4 Coverage ratchet”pnpm coverage:ratchet sums coverage/coverage-summary.json by package (packages/<name>,
apps/<name>) and compares it with
tooling/quality/coverage-baseline.json, main’s
values. It fails when any metric of any package falls below its baseline by more than 0.1 point
and more than 3 items (statements, branches or functions), or when a package of the baseline
has no coverage at all.
The item allowance exists because identical runs differ. The first CI run of this change covered
2 functions fewer in packages/db (−0.5 point) and 1 branch fewer in packages/server than the
local run. The paths involved are ariProperties and dueChannex in db/src/index.ts, and one
branch of server/src/http/booking-import.ts. They ran only when a scheduler tick fired during a
server test. The two db functions now have direct integration tests; the server branch still
depends on the tick (gap row 30). The baseline holds the higher of the local and CI values.
--updateraises values and adds new packages; it never lowers one.- Lowering a value is a hand edit, justified in the pull request.
The vitest thresholds stay as the absolute floor. Tests:
coverage-ratchet.integration.test.ts.
4.5 Mutation testing baseline
Section titled “4.5 Mutation testing baseline”StrykerJS mutates packages/core/src and runs the core’s unit and property tests
against each mutant (stryker.config.mjs,
packages/core/vitest.mutation.config.ts). The
property tests use FC_RUNS=100 and a fixed seed.
Stryker 10.0.0 does not support Vitest 5. It reported 19.8% on money.ts: in that run, mutants
such as + → - in add “survived” against tests that check addition. Support arrived in
10.1.0 (release notes), re-released as 10.1.1 on 2026-10-23. That is less than
7 days ago, so 10.1.1 is admitted through an exact-version minimumReleaseAgeExclude that
expires on 2026-10-31 (ADR 0003’s exception rule; gap row 7).
Baseline, 2026-10-28, on this change (FC_RUNS=100, FC_SEED=20261028, Stryker 10.1.1,
Vitest 5.0.5). Each nightly run keeps the full HTML and JSON report as an artifact.
9,881 mutants in 34 files with mutable code; 9 runtime errors are excluded from the scores. The run took 21 min 42 s with 8 workers.
| Area | Score | Covered code | Killed | Timeout | Survived | NoCoverage | CompileError | RuntimeError | Ignored |
|---|---|---|---|---|---|---|---|---|---|
| audit | 100.00% | 100.00% | 10 | 0 | 0 | 0 | 0 | 0 | 0 |
| blocks | 79.86% | 81.56% | 345 | 0 | 78 | 9 | 0 | 0 | 0 |
| channex | 36.95% | 70.10% | 898 | 0 | 383 | 1149 | 0 | 0 | 0 |
| countries | 96.57% | 97.69% | 169 | 0 | 4 | 2 | 0 | 0 | 0 |
| folios | 62.67% | 77.66% | 685 | 0 | 197 | 211 | 0 | 0 | 0 |
| properties | 81.48% | 83.02% | 132 | 0 | 27 | 3 | 0 | 0 | 0 |
| reports | 93.55% | 93.55% | 87 | 0 | 6 | 0 | 0 | 0 | 0 |
| reservations | 14.42% | 80.82% | 653 | 0 | 155 | 3721 | 0 | 0 | 0 |
| shared | 89.30% | 89.30% | 687 | 14 | 84 | 0 | 0 | 9 | 0 |
| tenancy | 77.91% | 78.40% | 127 | 0 | 35 | 1 | 0 | 0 | 0 |
| All | 38.56% | 79.71% | 3793 | 14 | 969 | 5096 | 0 | 9 | 0 |
Two scores, and why they differ.
- 38.56% is the score over every mutant, which is Stryker’s headline number.
- 79.71% is the score over the mutants that some core test reaches.
The gap is 5,096 “no coverage” mutants, almost all of them in four use-case modules:
| Module | Lines | Mutants with no core test |
|---|---|---|
reservations/index.ts |
2,013 | 2,865 |
reservations/rate-management.ts |
564 | 817 |
channex/import.ts |
733 | 775 |
folios/index.ts |
289 | 201 |
No test in packages/core exercises these modules. Their only tests run through the PostgreSQL
integration tests of db and the route tests of api and server, which the mutation run does
not execute. That is also how packages/core passes its 95% coverage threshold: the root
coverage run counts every package’s tests.
This is a real finding (gap row 29). The orchestration of reservations, rates, imports and folios has no fast, isolated tests of its own, so its logic is proven only end to end. Among the covered code, the weakest areas are:
channex: 383 survivors, 258 of them inari.ts;folios/ledger.ts: 195 survivors;blocks: 78 survivors;shared: 84 survivors, 22 of them inmoney.ts, mostly error-message strings and equivalent guards.
At this measurement, no threshold failed the run. Round 2 below sets a nightly threshold. Surviving mutants in money, allocation, availability and reservation state are the first places to add tests: they are where a weak test hides a wrong amount or an overbooking.
The job is incremental, so a night without core changes takes minutes. A full run
(workflow_dispatch with full) costs about two to three hours of runner time;
pnpm mutation:core runs the same locally.
Core use-case test change, measured 2026-10-28. The first run used the unmodified main test base
(43cfecd) and the second used the test branch with fast use-case tests, both with
FC_RUNS=100 and FC_SEED=20261028. They mutated the same 10,319 candidates. Stryker excludes
runtime errors from the score; 3 occurred before and 17 after. The latter were concentrated in
shared/exchange.ts and should be investigated before interpreting small movements in that
area. A further named-permission test was added after the second run copied its sandbox, so the
reported after score is a conservative measurement of this branch.
| Module | Before | After | NoCoverage before → after | Next target |
|---|---|---|---|---|
reservations/index.ts |
0.35% | 22.88% | 2,867 → 1,716 | 35% |
reservations/rate-management.ts |
0.12% | 27.87% | 817 → 330 | 40% |
channex/import.ts |
9.04% | 17.21% | 936 → 720 | 30% |
folios/index.ts |
9.46% | 39.19% | 201 → 90 | 55% |
shared/money.ts |
87.21% | 88.37% | 0 → 0 | 90% |
| Core, all files | 38.54% | 48.68% | 5,271 → 3,287 | 55% |
The first four modules gained fast use-case coverage, but still have 2,856 mutants no core test
reaches. These targets are for follow-up tests, not completion percentages or current gates.
The overall score increased by 10.14 points and 1,984 fewer mutants have no coverage. The
covered-code score moved from 78.81% to 71.49% as tests reached orchestration paths with 931
additional surviving mutants. Those paths need stronger assertions. A proposed CI break
threshold was 46% after four stable nightly runs, leaving 2.68 points of
room for known run variability. Round 2 supersedes that proposal with a measured nightly gate.
Core mutation score, round 2, measured 2026-10-28. With FC_RUNS=100 and
FC_SEED=20261028, a forced full retest has the same 10,319 candidates as round 1. Stryker excluded
2 runtime errors from the denominator. No handwritten source was excluded or ignored.
| Module | Round 1 | Round 2 | NoCoverage round 1 → 2 | 70% target |
|---|---|---|---|---|
reservations/index.ts |
22.88% | 32.04% | 1,716 → 1,269 | Open: 37.96 points |
reservations/rate-management.ts |
27.87% | 60.88% | 330 → 60 | Open: 9.12 points |
folios/index.ts |
39.19% | 81.53% | 90 → 6 | Met |
channex/import.ts |
17.21% | 43.72% | 720 → 249 | Open: 26.28 points |
| Core, all files | 48.68% | 58.19% | 3,287 → 2,012 | Open: 11.81 points |
The round 2 total is 5,945 killed, 58 timed out, 2,302 survived and 2,012 with no coverage.
The nightly job now fails below 55% (thresholds.break), 3.19 points under this measured
score. It is an overall floor; it does not imply that each module has met 70%. Reservation
calendar and front-desk projections still have large no-coverage clusters because the shared
memory port deliberately does not implement stays, list or older overdue arrivals. Booking
import mapping and recovery have both surviving and uncovered branches. These need independent
behavior tests and, where persistence or isolation matters, the PostgreSQL integration suite.
No surviving mutant was classified as equivalent without proof. In particular, apparently
redundant workspace guards defend against malformed JavaScript principals and must retain
fail-closed behavior. Round 2 also exposed a test-port contract bug: units(propertyId, {})
included archived units in memory although the PostgreSQL adapter excludes them. A failing
regression and the test-port fix were committed separately; production code did not change.
Core mutation score, round 3, measured 2026-10-29. The full GitHub runs used the same
FC_RUNS=100 and fixed seed. The starting 58.85% is the last full main artifact before PR #72
split reservation modules; the branch artifacts measure successive behavior-test
additions on the split code. Counts are detected mutants (killed or timed out) over scored
mutants, excluding runtime errors.
| Full CI run | Detected / scored | Whole-core score |
|---|---|---|
| Main baseline | 6,245 / 10,611 | 58.85% |
| Reservation and policy tests | 7,018 / 10,598 | 66.22% |
| Guest grants and rate freezes | 7,131 / 10,583 | 67.38% |
| Calendar and policy terms | 7,369 / 10,583 | 69.63% |
| ARI safety and import recovery | 7,558 / 10,583 | 71.42% |
The split-module starting scores below come from the PR #72 targeted mutation review, which
mutated reservations/** and the moved folio policy-postings file. The after scores are from the
round 3 whole-core full run. The overall baseline above and each branch score come from GitHub
full-run artifacts; the review scores supply a useful file-level comparison where the earlier
main artifact had one reservation monolith instead of the split files.
| Module | Before | Round 3 after | NoCoverage after |
|---|---|---|---|
reservations/front-desk.ts |
0.00% | 81.57% | 1 |
reservations/policies.ts |
29.74% | 63.76% | 31 |
folios/policy-postings.ts |
25.56% | 80.00% | 1 |
reservations/room-builder.ts |
27.79% | 59.05% | 31 |
reservations/room-actions.ts |
35.28% | 67.12% | 24 |
reservations/inventory.ts |
39.78% | 65.09% | 66 |
reservations/stays.ts |
36.16% | 66.78% | 43 |
reservations/reservation-save.ts |
47.06% | 65.75% | 6 |
reservations/runner.ts |
— (formerly internal.ts, 48.21%) |
74.51% | 4 |
reservations/guests.ts |
34.06% | 68.84% | 16 |
reservations/rate-management.ts |
61.27% | 65.16% | 47 |
channex/ari.ts |
45.67% | 57.38% | 63 |
channex/import.ts |
45.85% | 50.27% | 244 |
The ARI and import before scores are from the full main artifact; the other named before scores
are from the PR #72 review’s split-module run. runner.ts is a new file split from internal.ts,
so its before value is an ancestor, not a like-for-like baseline. The round 3 full run detected
7,546 killed and 12 timed-out mutants; 2,404 survived and 621 had no core-test coverage. It
excluded 17 runtime errors from the scored denominator. No handwritten source was excluded or
ignored. The nightly thresholds.break is now 68%, three whole points below the measured
71% floor. Tests added no production testability seam and exposed no production bug. The
@guestcraft/testing memory port gained reservation, stay, overdue-arrival and due-deposit
projections, each checked against PostgreSQL. The remaining no-coverage clusters are largest in
booking import (244), inventory (66), ARI (63), rate management (47) and stays (43); surviving
mutants also remain in these modules and in reservation policy and room-building decisions.
4.6 Actions minutes and the self-hosted runner
Section titled “4.6 Actions minutes and the self-hosted runner”The repository was public from 2026-10-28 to 2026-10-29, when standard runners had no minute quota. Before and after that it is private, with a quota of 3,000 minutes a month (GitHub Pro) that the parallel work of 2026-10-25 to 2026-10-28 used up: about 15 minutes for every push to a pull request and again for every merge (GitHub Actions billing). When the quota runs out GitHub stops starting jobs (no charge, because the account has no spending budget).
Since 2026-10-29 every workflow therefore runs on a self-hosted runner selected by the
repository variable CI_RUNNER (its runner label): runs-on: ${{ vars.CI_RUNNER || 'ubuntu-24.04' }}.
The runners are three services on a maintainer’s Ubuntu 24.04 machine, under a dedicated user in
the docker group, so they cost nothing and keep every check (quality, browser, smoke, images,
docs) on every pull request. While CI_RUNNER is set:
- pull requests build the release images for amd64 only, because a private repository has no free arm64 runner; the release workflow keeps the GitHub-hosted arm64 build;
- the browser job runs Chromium in a container of the official Playwright image, pinned by
digest at the version of
@playwright/test, instead of installing it on the runner. Other users of the machine create and remove Docker networks and containers at any moment. Each of them changes the host’s interfaces, and a Chromium on the host then aborts the requests in flight withnet::ERR_NETWORK_CHANGED. In its own network namespace it never sees those changes. The tests connect to it throughGUESTCRAFT_E2E_BROWSER_WS, and it reaches the app on the loopback through them. A retry would not help, because the tests of a file share their database state in order.apps/web/test/e2e/network-churn.tsreproduces the failure (see the web README). The container is alone on a bridge network of its own, so containers on Docker’s default bridge cannot reach it, and it is published on the host’s loopback only. The server has no authentication: every user of the machine who can open a loopback connection can create browsers on it, and, knowing a job’s tunnel port, reach that job’s loopback through it. This is not isolation from local users, who can already reach the runner’s loopback services, such as the job’s PostgreSQL; a secret path would not help, because the server reveals it at/json; - OpenSSF Scorecard and dependency review are skipped, because they need a public repository.
- jobs that start containers or servers name them per run and choose free ports, and the
browser job starts PostgreSQL itself instead of as a job service (the machine hides other
users’ processes from
/proc, which job services need), so the three runners can run jobs of different pull requests at the same time; each runner also installs pnpm and keeps its store in its own directory (~/setup-pnpm-<runner name>); - jobs have longer time limits, for example 60 minutes instead of 25 for
quality. The runners run at a low CPU and I/O priority on a shared machine, and under load the test suite with coverage takes more than 25 minutes. The GitHub-hosted limits still apply whenCI_RUNNERis unset. - tests use a bounded number of workers: Vitest starts one worker per CPU by default, which on the
shared 96-CPU machine would be 95 per job.
qualitycaps them at 24, and the nightly property run at 4 packages of 6 workers, so three jobs at once leave most of the machine to its other users. - the three runners share one Docker daemon and one Docker client configuration.
smoketags its images per run (GUESTCRAFT_LOCAL_TAG) and builds with the daemon’s own builder (BUILDX_BUILDER=default); the image jobs create their Buildx builder without making it the current one (use: false). Otherwise one job can start another job’s images, or lose its build when another job removes the builder it was using.
A self-hosted runner executes the code of every pull request with Docker access to its machine, so write access to this repository is limited to trusted collaborators. Deleting the variable (for example when the repository becomes public again) moves every job back to GitHub’s runners without other changes.
4.7 Badges and first Scorecard result
Section titled “4.7 Badges and first Scorecard result”The badges workflow accepts only successful quality or mutation runs whose source is this
repository’s main branch and whose event was push, schedule or workflow_dispatch. It
downloads that run’s artifact in a read-only job, converts the report to a fixed Shields endpoint
JSON shape, then pushes the JSON in a separate write-enabled job to the orphan badges branch.
Pull requests, including a fork branch named main, cannot publish. The branch holds
coverage.json (the total line coverage from Vitest) and mutation.json (the core score over
killed, timed-out, survived and uncovered mutants; compile/runtime errors and ignored mutants
are excluded). Missing or malformed reports fail before publishing. Both badges use fixed colors:
red below 50, orange 50–59.99, yellow 60–69.99, yellowgreen 70–79.99, green 80–89.99,
brightgreen 90–100. The current quality and mutation workflows already upload the needed JSON
reports; the badges workflow consumes them only from eligible main runs. The endpoint URLs are
served through Shields.io’s public endpoint renderer using GitHub’s raw branch files; there is
no badge account or paid service.
The first local OpenSSF Scorecard CLI scan (v5.6.0, 2026-10-28, main at
5aa3933166ac7a509b8a6ac1f653b916c1c19769) scored 6.6/10. This is a baseline,
not a product-completion measure. Checks below 5 and their actions:
| Check | Score | Action |
|---|---|---|
| Branch-Protection | 4 | Owner: require approvers, Code Owners review, last-push approval and stale-review dismissal on main; keep release branches protected. |
| CII-Best-Practices | 0 | Complete the OpenSSF Best Practices self-assessment and link its badge when awarded. |
| Code-Review | 0 | Require and record independent approvals on future pull requests; the six sampled merges had none. |
| Contributors | 3 | Invite and document independent contributions; recheck after the project has more contributors. |
| Maintained | 0 | Recheck after the repository is 90 days old; the current score reflects its age. |
| SAST | 0 | Verify CodeQL default setup runs on new commits; this scan found 0 of 30 commits with SAST results. |
| Security-Policy | 4 | Investigate Scorecard’s “no linked content found” finding and link the private reporting path explicitly from SECURITY.md. |
Packaging and Signed-Releases are unscored because GuestCraft has no release yet. The Action’s
first published score will be recorded after it runs on main; it may differ from this CLI
baseline. CodeQL default setup is configured and its workflow badge URL returns a passing SVG.
4.8 Web route transfer budgets
Section titled “4.8 Web route transfer budgets”Acceptance: every public or authenticated App Router page in a production build has a recorded
initial JavaScript and CSS size in both gzip and brotli. A new route, missing asset, stale route,
or size beyond any of its four limits fails the browser job after its production build. The
check needs no login, customer data, running server or port.
pnpm --filter @guestcraft/web build && pnpm web:budgets reads Next.js’s per-page client
reference manifest and route build manifest. It includes the shared bootstrap and each unique
route entry asset once, then compresses the built bytes with fixed gzip level 9 and brotli
quality 11. Redirect pages are included; internal error pages, API handlers, images, fonts,
HTML, RSC payloads and later interaction-driven chunks are outside this initial JS/CSS
budget. The numbers approximate a cold transfer with compression; actual HTTP encoding,
browser cache and CDN behavior can differ. They do not measure LCP, INP or execution cost.
The complete 32-route baseline and four byte limits per route are in
web-route-budgets.json. Representative
measured sizes from the 2026-10-28 main build (KiB, rounded) are:
| Route | JS gzip | JS brotli | CSS gzip | CSS brotli |
|---|---|---|---|---|
/sign-in |
200 | 173 | 13 | 11 |
/w/[workspaceId]/p/[propertyId]/today |
472 | 409 | 13 | 11 |
/w/[workspaceId]/p/[propertyId]/planning |
472 | 409 | 13 | 11 |
/w/[workspaceId]/p/[propertyId]/rates |
431 | 373 | 13 | 11 |
/w/[workspaceId]/p/[propertyId]/reports |
517 | 443 | 13 | 11 |
Each limit is the recorded size plus the greater of 2% or 1,024 bytes. To inspect a regression,
run the build and pnpm web:budgets, read the failed route/encoding, and compare its
entryJSFiles, entryCSSFiles and rootMainFiles with the base build. Check shared chunks
first when many routes move together; check the route entry and its imported panels when one
route moves. pnpm web:budgets --update lowers recorded sizes and limits after a shrink. It
never raises an existing value. A new route or justified increase needs a reviewed hand edit
to the JSON file, including the reason in the pull request. --snapshot prints measured
values and suggested limits without changing the file.
The three largest candidate refactors, to measure in separate screen PRs, are:
- Load reservation, folio, booking and other heavy panels only when opened. Today, Planning, Reservations and Rooms share roughly 472 KiB of initial gzip JavaScript.
- Defer the Recharts chart module until the Reports chart is visible; Reports has the largest initial JavaScript transfer at roughly 517 KiB gzip, while preserving its table.
- Split the Planning grid’s rendering and interaction code from the common desk entry, so routes without a grid do not pay for it. Verify keyboard behavior and compare assets before and after; the shared desk chunk can move multiple route totals at once.
A Lighthouse-style lab gate is deferred. These authenticated dynamic pages need a seeded database and session, while rendering and timing vary with CPU contention on shared CI. A cheap, deterministic built-asset gate gives a reliable regression signal without pretending to control network, paint or interaction timing. Real-user Core Web Vitals and a controlled synthetic run remain separate future measurements.
5. Top 10 next actions
Section titled “5. Top 10 next actions”Turn on Dependabot alerts(done on 2026-10-28). (row 6)- Remove the Stryker release-age exception on 2026-10-31. (row 7)
- Add If-Match/ETag to the 8 writes and 3 reads of finding S3: lost updates on room types, units and properties. (row 8)
- Enforce changed-line coverage with diff-cover: 90%, and 100% for money, isolation and sync. (row 9)
- Add oasdiff breaking-change detection against the base commit’s contract. (row 10)
- Add zizmor to
quality; actionlint runs since 2026-10-28. (row 12) - Fix S2 and S4 (membership RPC endpoints, unauthenticated
/roles) and decide on S1 (path naming). (row 16) - Continue the core use-case tests for the remaining ARI, booking-import and reservation-policy gaps; round 3’s measured score and nightly floor are in section 4.5. (rows 29, 11)
- Turn on Biome’s cognitive-complexity limit for new code, with the 159 existing functions listed. (row 13)
- Make
browsera required check and run axe on every end-to-end screen. (rows 14–15)
6. Sources
Section titled “6. Sources”Consulted on 2026-10-28. Versions were checked on the npm registry and on GitHub releases.
- spectral: Spectral CLI 6.17.0, https://github.com/stoplightio/spectral/releases/tag/v6.17.0
- zalando: Zalando RESTful API guidelines (snake_case, problem JSON, ETag/If-Match), https://github.com/zalando/restful-api-guidelines
- gitleaks: gitleaks, README (“feature complete… security patches only”), https://github.com/gitleaks/gitleaks
- osv: osv-scanner configuration and ignore entries, https://google.github.io/osv-scanner/configuration/
- vitest-coverage: Vitest coverage thresholds and
autoUpdate, https://vitest.dev/config/coverage - stryker: StrykerJS Vitest runner, https://stryker-mutator.io/docs/stryker-js/vitest-runner/ and incremental mode, https://stryker-mutator.io/docs/stryker-js/incremental/
- stryker-10-1: StrykerJS 10.1.0 release notes (Vitest 5 support, #6214), https://github.com/stryker-mutator/stryker-js/releases/tag/v10.1.0
- ghas-price: GitHub Secret Protection and Code Security, $19 and $30 per active committer per month, https://github.blog/changelog/2025-03-04-introducing-github-secret-protection-and-github-code-security/
- gh-plans: GitHub plans (Dependabot alerts on all plans), https://docs.github.com/en/get-started/learning-about-github/githubs-plans
- gh-actions-billing: GitHub Actions billing and included minutes, https://docs.github.com/en/billing/concepts/product-billing/github-actions
- diff-cover: diff-cover, https://github.com/Bachmann1234/diff_cover
- oasdiff: oasdiff breaking-change detection, https://github.com/oasdiff/oasdiff
- zizmor: zizmor audits, https://docs.zizmor.sh/audits/
- actionlint: actionlint, https://github.com/rhysd/actionlint
- nx: Nx “s1ngularity” post-mortem (workflow injection to npm token theft, August 2025), https://nx.dev/blog/s1ngularity-postmortem
- npm ecosystem compromise, September 2025 (CISA alert), https://www.cisa.gov/news-events/alerts/2025/09/23/widespread-supply-chain-compromise-impacting-npm-ecosystem
- biome-complexity: Biome
noExcessiveCognitiveComplexity, https://biomejs.dev/linter/rules/no-excessive-cognitive-complexity/ - jscpd: jscpd, https://github.com/kucherenko/jscpd
- size-limit: size-limit, https://github.com/ai/size-limit
- semgrep-license: Semgrep Rules License v1.0, https://semgrep.dev/legal/rules-license
- type-coverage-ts7: type-coverage and TypeScript 7, https://github.com/plantain-00/type-coverage/issues/150
- scorecard: OpenSSF Scorecard checks, https://github.com/ossf/scorecard/blob/main/docs/checks.md and the Action’s private-repository note, https://github.com/ossf/scorecard-action
- slsa: SLSA 1.2, what’s new, https://slsa.dev/spec/v1.2/whats-new
- pnpm-sbom:
pnpm sbom, https://pnpm.io/cli/sbom - attest: actions/attest (free on public repositories; storage records need an organization), https://github.com/actions/attest
- syft: Syft 1.55.0, https://github.com/anchore/syft/releases/tag/v1.55.0
- release-please: release-please-action 5.0.0 (release-please 17.6.0), https://github.com/googleapis/release-please-action
- npm-trusted: npm trusted publishing (no provenance from private repositories), https://docs.npmjs.com/trusted-publishers
- osps: OpenSSF Open Source Project Security Baseline, https://baseline.openssf.org/
- badge: OpenSSF Best Practices Badge criteria, https://www.bestpractices.dev/en/criteria
- diataxis: Diátaxis, https://diataxis.fr/
- google-review: Google engineering practices, what to look for in a review, https://google.github.io/eng-practices/review/reviewer/looking-for.html and review speed, https://google.github.io/eng-practices/review/reviewer/speed.html
- pnpm supply-chain settings, https://pnpm.io/supply-chain-security