Skip to content

feat(release): add lifecycle semver contract - #309

Draft
davida-ps wants to merge 1 commit into
mainfrom
davida-ps/lifecycle-semver-v1
Draft

feat(release): add lifecycle semver contract#309
davida-ps wants to merge 1 commit into
mainfrom
davida-ps/lifecycle-semver-v1

Conversation

@davida-ps

@davida-ps davida-ps commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

User description

Summary

  • add the harness-agnostic clawsec.lifecycle-semver/v1 contract and fixture set
  • implement strict SemVer 2.0 parsing and precedence without numeric precision loss
  • define canonical beta.N, rc.N, final, and legacy-prerelease classes
  • separate SemVer validity, public-publication eligibility, stable-intent, and active-catalog stable authorization
  • parse and format package-qualified <package>-v<semver> tags, including package names containing -v
  • route the existing release comparator through the shared parser while preserving its exports, CLI, trimming, build rejection, and legacy simulation outputs

Why

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-v1 Wave 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

  • numeric identifiers remain unbounded decimal strings for parsing and comparison
  • build metadata is valid SemVer syntax but is rejected as a ClawSec publication identity
  • beta and RC versions are lab-only and never publicly eligible
  • a final version may be publication-eligible but is not stable without active signed-catalog authorization
  • stable-intent embeds the exact final intended version while remaining outside the active public catalog
  • long malformed versions, package tags, and prerelease simulations use linear parsing rather than backtracking regexes
  • the legacy parseSemver, compareSemver, assertSemverIncrement, and nextSimulatedReleaseVersion interfaces remain available

Scope

Exactly five files:

  • contracts/lifecycle-semver-v1.json
  • contracts/fixtures/lifecycle-semver-v1.json
  • scripts/ci/lifecycle_semver.mjs
  • scripts/ci/semver_increment.mjs
  • scripts/test-skill-lifecycle-semver.mjs

No workflow, release script, skill, package dependency, tag, catalog, or publication behavior changes in this PR.

Remote validation

The exact bundle was copied with scp to a fresh quarantine on davida@20.14.133.241 before this branch was pushed.

  • quarantine: /tmp/clawsec-lifecycle-semver.P1icUg
  • archive SHA-256 matched locally and remotely: 0c615331333f10c77ea76ca1d335c822b517bd849ee263b0d6bc8c4881548b94
  • all five changed-file SHA-256 digests matched after extraction
  • Node.js v22.23.1
  • node scripts/test-skill-lifecycle-semver.mjs
  • node scripts/test-skill-release-semver.mjs
  • positive CLI transition: 0.1.0-beta.1 -> 0.1.0-rc.1
  • negative CLI case rejected build metadata as required
  • ESLint passed on all changed JavaScript files with zero warnings

Independent 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 zip binary; 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/v1 contract 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 in lifecycle_semver.mjs while preserving legacy exports, incrementing, and simulation behavior.

TopicDetails
Release helpers Reuse the shared SemVer parser in the release helpers so legacy comparison, increment, and simulation keep working with exact precedence.
Modified files (3)
  • scripts/ci/lifecycle_semver.mjs
  • scripts/ci/semver_increment.mjs
  • scripts/test-skill-lifecycle-semver.mjs
Latest Contributors(2)
UserCommitDate
David.a@prompt.securityfeat(release): add lif...July 22, 2026
david.a@prompt.securityfix(clawsec-suite): pu...July 14, 2026
SemVer contract Define strict SemVer syntax, package-qualified tags, lifecycle classes, and publication eligibility for clawsec.lifecycle-semver/v1.
Modified files (4)
  • contracts/fixtures/lifecycle-semver-v1.json
  • contracts/lifecycle-semver-v1.json
  • scripts/ci/lifecycle_semver.mjs
  • scripts/test-skill-lifecycle-semver.mjs
Latest Contributors(1)
UserCommitDate
David.a@prompt.securityfeat(release): add lif...July 22, 2026
Review this PR on Baz | Customize your next review

Comment on lines +198 to +209
export function evaluatePublicationVersion(version) {
const parsed = parseSemverV2(version);
const lifecycleClass = classifyLifecycleVersion(version);

if (hasBuildMetadata(parsed)) {
return {
version,
lifecycleClass,
publicPublicationEligible: false,
reasonCode: "build_metadata_disallowed",
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Severity cross_repo

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

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.

Comment on lines 63 to 66
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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant