feat(release): add lifecycle semver contract - #309
Conversation
| export function evaluatePublicationVersion(version) { | ||
| const parsed = parseSemverV2(version); | ||
| const lifecycleClass = classifyLifecycleVersion(version); | ||
|
|
||
| if (hasBuildMetadata(parsed)) { | ||
| return { | ||
| version, | ||
| lifecycleClass, | ||
| publicPublicationEligible: false, | ||
| reasonCode: "build_metadata_disallowed", | ||
| }; | ||
| } |
There was a problem hiding this comment.
Publication eligibility field misnamed
evaluatePublicationVersion() returns publicPublicationEligible and reasonCode instead of the fixture's eligible/reason_code wire keys, so callers that use the raw helper output see a missing eligible field and fail eligibility evaluation. Should we rename the returned fields to match the wire schema and have the test assert deep equality instead of remapping?
Want Baz to fix this for you? Activate Fixer
Other fix methods
Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
scripts/ci/lifecycle_semver.mjs around lines 198-235, the evaluatePublicationVersion()
function returns eligibility under the camelCase key `publicPublicationEligible`, but
the fixture/wire contract uses `eligible` with snake_case fields (`reason_code`,
`lifecycle_class`). Refactor evaluatePublicationVersion() to return an object whose keys
exactly match the fixture schema (rename `publicPublicationEligible` -> `eligible`, and
ensure `lifecycleClass` -> `lifecycle_class`, `reasonCode` -> `reason_code`). Then
update the relevant test to assert deep equality against the fixture objects directly,
removing any remapping like `publicPublicationEligible: fixture.eligible` so the helper
output satisfies the contract shape.
| const left = parseSemver(leftVersion); | ||
| const right = parseSemver(rightVersion); | ||
|
|
||
| for (let index = 0; index < left.core.length; index += 1) { | ||
| if (left.core[index] !== right.core[index]) { | ||
| return left.core[index] < right.core[index] ? -1 : 1; | ||
| } | ||
| } | ||
|
|
||
| if (left.prerelease.length === 0 && right.prerelease.length === 0) return 0; | ||
| if (left.prerelease.length === 0) return 1; | ||
| if (right.prerelease.length === 0) return -1; | ||
|
|
||
| const length = Math.max(left.prerelease.length, right.prerelease.length); | ||
| for (let index = 0; index < length; index += 1) { | ||
| const leftIdentifier = left.prerelease[index]; | ||
| const rightIdentifier = right.prerelease[index]; | ||
| if (leftIdentifier === undefined) return -1; | ||
| if (rightIdentifier === undefined) return 1; | ||
| if (leftIdentifier === rightIdentifier) continue; | ||
| if (typeof leftIdentifier === "number" && typeof rightIdentifier === "number") { | ||
| return leftIdentifier < rightIdentifier ? -1 : 1; | ||
| } | ||
| if (typeof leftIdentifier === "number") return -1; | ||
| if (typeof rightIdentifier === "number") return 1; | ||
| return leftIdentifier < rightIdentifier ? -1 : 1; | ||
| } | ||
|
|
||
| return 0; | ||
| return compareSemverV2(left.raw, right.raw); | ||
| } |
There was a problem hiding this comment.
compareSemver() parses both operands with parseSemver() then compareSemverV2 reparses left.raw/right.raw, so every comparison does two full SemVer parses per operand — should we compare the already-parsed values directly (or add a parsed-version comparator), and apply the same cleanup to the parseSemverV2(base.raw)/parseSemverV2(next.raw) roundtrips in assertSemverIncrement() and nextSimulatedReleaseVersion()?
Want Baz to fix this for you? Activate Fixer
Other fix methods
Prompt for AI Agents
In scripts/ci/semver_increment.mjs around lines 63-66 inside `compareSemver()`, stop
doing `parseSemver(leftVersion/rightVersion)` followed by `compareSemverV2(left.raw,
right.raw)`, because each operand is validated/parsed twice. Refactor so
`compareSemver()` parses each input exactly once with `parseSemverV2`, enforces the same
"no build identifiers" rule currently enforced by `parseSemver()`, and then performs the
comparison using those already-parsed results (if `compareSemverV2` only accepts
strings, add a small parsed-compare helper or overload it in
scripts/ci/lifecycle_semver.mjs and call that here). Then in
scripts/ci/semver_increment.mjs around lines 80-83 inside `assertSemverIncrement()`, and
in `nextSimulatedReleaseVersion()`, remove the redundant `parseSemverV2(base.raw)` /
`parseSemverV2(next.raw)` roundtrips and compute the core comparison directly from the
already-available parsed `base.core` / `next.core` (or from the cached V2 parse results
from the refactor).
User description
Summary
clawsec.lifecycle-semver/v1contract and fixture setbeta.N,rc.N, final, and legacy-prerelease classes<package>-v<semver>tags, including package names containing-vWhy
ClawSec currently validates versions differently in different release paths. The multi-harness design cannot safely introduce beta, RC, private stable-intent, and stable promotion until those paths share one exact SemVer contract and adversarial fixture set.
This is the independent
lifecycle-semver-v1Wave 1 unit from draft architecture PR #306. It defines syntax and lifecycle binding only; later PRs will wire stable-tag policy, Pages selection, and signed lifecycle cutover enforcement.Security and compatibility properties
parseSemver,compareSemver,assertSemverIncrement, andnextSimulatedReleaseVersioninterfaces remain availableScope
Exactly five files:
contracts/lifecycle-semver-v1.jsoncontracts/fixtures/lifecycle-semver-v1.jsonscripts/ci/lifecycle_semver.mjsscripts/ci/semver_increment.mjsscripts/test-skill-lifecycle-semver.mjsNo workflow, release script, skill, package dependency, tag, catalog, or publication behavior changes in this PR.
Remote validation
The exact bundle was copied with
scpto a fresh quarantine ondavida@20.14.133.241before this branch was pushed./tmp/clawsec-lifecycle-semver.P1icUg0c615331333f10c77ea76ca1d335c822b517bd849ee263b0d6bc8c4881548b94v22.23.1node scripts/test-skill-lifecycle-semver.mjsnode scripts/test-skill-release-semver.mjs0.1.0-beta.1 -> 0.1.0-rc.1Independent adversarial review also completed 274,514 generated syntax cases and 100,000 randomized precedence comparisons with zero mismatches.
The broader unchanged tag-release simulation was attempted only as supplemental regression evidence. It reached archive creation but the remote lab image has no
zipbinary; the local machine instead lacks ED25519 support in its system OpenSSL. The contract test and existing SemVer regression are the authoritative gates for this bounded PR, while normal GitHub CI retains the repository's complete test environment.Release boundary
This PR is draft-only. Do not merge, tag, release, publish, or activate lifecycle policy as part of this PR.
Generated description
Below is a concise technical summary of the changes proposed in this PR:
Define a strict
clawsec.lifecycle-semver/v1contract and fixture set that standardize SemVer parsing, package-tag syntax, lifecycle classes, and publication eligibility. Route the release SemVer helpers through the shared parser and comparator inlifecycle_semver.mjswhile preserving legacy exports, incrementing, and simulation behavior.Modified files (3)
Latest Contributors(2)
clawsec.lifecycle-semver/v1.Modified files (4)
Latest Contributors(1)