Skip to content

chore(release): bump VERSION to 0.2.0 and correct the release record - #628

Merged
IanFrelinger merged 4 commits into
masterfrom
chore/release-record-truth
Sep 15, 2026
Merged

IanFrelinger merged 4 commits into
masterfrom
chore/release-record-truth

Conversation

@IanFrelinger

Copy link
Copy Markdown
Owner

VERSION read 0.1.2 while 0.1.2 was already published on nuget.org, so the
version string in this tree no longer identified its content.

THE MINOR MOVED RATHER THAN THE PATCH, because signed bytes changed. #592 gave
every double one canonical decimal form across all three target frameworks: the
netstandard2.0 asset under Mono had been writing different digits from
net8.0/net10.0 for the same value, and the canonical payload is the message
every certification signature is computed over. Both golden corpora changed with
it. #621 compounds it - a host that supplied a brick signer previously got
composition records under the committed dev key and now gets them under its own.
A record minted under 0.1.2 and the same record minted now are not
byte-identical, so 0.1.3 would have told a consumer holding stored signed
records that this was a safe patch.

Corrections landed in the same pass, each verified against the artefact rather
than against a status document:

  • Section 5.4b said master was 104 commits past v0.1.2 with seven touching
    src/Ashlar.Certification.Contracts/. Measured 2026-09-14: 37 and 3. No count
    is recorded now - it changes on every merge, and both previous ones were
    wrong. The section says how to re-derive it instead.

  • Section 5.4b said scripts/release-preflight-local.sh "is the only thing that
    inspects both". It is not: release-staging-on-label.yml resolves the canonical
    version from root VERSION and runs automatically when a PR is labelled, and
    resolve-canonical-package-version.sh, verify-docs-published-version.sh and
    tests/uat/tier9.sh read it too. What none of them do is compare VERSION to the
    latest tag, which is the actual gap the section exists to name.

  • README's self-extend bullet claimed the operator-governed path has "a
    post-apply canary that auto-rolls-back a change that fails verification",
    unconditionally. SelfExtendAdmissionBridge takes IPostApplyVerification as an
    optional parameter defaulting to null, and rolls back only "when verification
    is not null". AddCertificationInfrastructure supplies one, so the supported
    wiring has the canary; a host constructing the bridge by hand and passing
    nothing gets no canary and no rollback, silently. Scoped to what holds.

  • Four go-criteria were met and left unticked: security defaults documented,
    NuGet 0.1.2 published, GHCR images published, GHCR images available. Ticked
    with one line of evidence each. The GHCR line now also records that nexo-api
    is amd64-only by design, which a reader would otherwise infer from the
    multi-arch claim beside it. A no-go item whose own text read "resolved" was
    still an open checkbox; it is closed.

Verified in-container: cert-gate 922/922.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

Verification

check result
cert-gate in-container 979/979
v0.1.2 release assets 0 → 72 (E5), all 22 checksums verified before upload

🤖 Generated with Claude Code

VERSION read 0.1.2 while 0.1.2 was already published on nuget.org, so the
version string in this tree no longer identified its content.

THE MINOR MOVED RATHER THAN THE PATCH, because signed bytes changed. #592 gave
every double one canonical decimal form across all three target frameworks: the
netstandard2.0 asset under Mono had been writing different digits from
net8.0/net10.0 for the same value, and the canonical payload is the message
every certification signature is computed over. Both golden corpora changed with
it. #621 compounds it - a host that supplied a brick signer previously got
composition records under the committed dev key and now gets them under its own.
A record minted under 0.1.2 and the same record minted now are not
byte-identical, so 0.1.3 would have told a consumer holding stored signed
records that this was a safe patch.

Corrections landed in the same pass, each verified against the artefact rather
than against a status document:

- Section 5.4b's counts - 104 commits past v0.1.2, seven touching
  src/Ashlar.Certification.Contracts/ - were ACCURATE when recorded on
  2026-09-13. A draft of this commit "corrected" them to 37 and 3; that was
  wrong, measured in a shallow clone whose master stops at 37 commits. In a full
  clone it is 112 and 7. The body now carries no count, since it changes on
  every merge, and the note says to re-derive it in a full clone - a shallow
  checkout also makes `git describe` find no tag at all, which is how the bad
  measurement looked plausible.

- Section 5.4b said scripts/release-preflight-local.sh "is the only thing that
  inspects both". It is not: release-staging-on-label.yml resolves the canonical
  version from root VERSION and runs automatically when a PR is labelled, and
  resolve-canonical-package-version.sh, verify-docs-published-version.sh and
  tests/uat/tier9.sh read it too. What none of them do is compare VERSION to the
  latest tag, which is the actual gap the section exists to name.

- README's self-extend bullet claimed the operator-governed path has "a
  post-apply canary that auto-rolls-back a change that fails verification",
  unconditionally. SelfExtendAdmissionBridge takes IPostApplyVerification as an
  optional parameter defaulting to null, and rolls back only "when verification
  is not null". AddCertificationInfrastructure supplies one, so the supported
  wiring has the canary; a host constructing the bridge by hand and passing
  nothing gets no canary and no rollback, silently. Scoped to what holds.

- Four go-criteria were met and left unticked: security defaults documented,
  NuGet 0.1.2 published, GHCR images published, GHCR images available. Ticked
  with one line of evidence each. The GHCR line now also records that nexo-api
  is amd64-only by design, which a reader would otherwise infer from the
  multi-arch claim beside it. A no-go item whose own text read "resolved" was
  still an open checkbox; it is closed.

Verified in-container: cert-gate 922/922.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@IanFrelinger
IanFrelinger force-pushed the chore/release-record-truth branch from 66d44b3 to 5ba174f Compare September 14, 2026 23:06
t and others added 3 commits September 14, 2026 20:20
…st the guards

Two gaps docs/release/RELEASE-READINESS.md named and nothing closed.

1. NOTHING WARNED THAT VERSION WAS STALE. release.yml asserts a tag matches root
VERSION, but only at tag time and only when ref_type == 'tag' - by then the tag
exists and the guard's only move is to refuse the release. That is the good
failure, but it fails late, and it is how 0.1.2 came to sit in the tree while
0.1.2 was already published.

scripts/check-version-staleness.sh compares VERSION to the newest v* tag and to
nuget.org. It runs from release-preflight-local.sh - the last human checkpoint
before a tag - and from rc-gate. Advisory by design: it always exits 0. Whether
a tree is "too far" past its tag is a judgement call, and a check that reddens a
lane on a judgement call is how lanes get muted in this repository
(docs/HowGatesGoQuiet.md).

It needs a FULL clone, and says so when it does not have one. A shallow checkout
truncates the history, git describe then finds no tag, and the check silently
measures nothing - which is not hypothetical: a draft of the VERSION bump in
#628 "corrected" this section's commit counts using numbers measured in a
shallow clone, where master stops at 37 commits. The rc-gate checkout sets
fetch-depth: 0 and fetch-tags for exactly this reason.

2. THE RELEASE GUARDS HAD NO TESTS. scripts/lib/release-staging-guards.sh stands
between a mislabelled package and nuget.org, and was only ever exercised as a
side effect of a real release - which has run three times in this repository's
lifetime. A guard exercised only by the event it guards is indistinguishable
from a guard that does nothing.

tests/scripts/release-guards.test.sh covers normalize_semver, assert_valid_semver
and assert_version_matches_canonical: matching tag exits 0, mismatched exits
nonzero AND says "does not match", an ABSENT VERSION file exits 0 with SKIPPED
while an EMPTY one refuses, and a bare version still matches a v-prefixed tag.
It also pins that the three nuget-visibility scripts raise a short poll budget
unless ASHLAR_NUGET_VERIFY_ALLOW_SHORT=1 - nuget.org indexing lags publication,
and a short budget reports "not visible" for a package that published fine.

21 assertions, pure bash, no network or dotnet, so they run in shell-lint on
every pull request rather than in a monthly lane. Mutation-measured: disabling
the mismatch check fails exactly one assertion and leaves 20 passing.

shell-lint's bash -n sweep covered scripts/ only, so tests/ shell was not even
syntax-checked. Widened to both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rkflows by line

Section 5.4 is headed "Repository variables / secrets (release workflow)" and
listed six. The release workflows read FIFTEEN. The nine missing ones are added,
each with what UNSET means, because for most of them unset is the intended state
and the default is the behaviour you get:

- NUGET_POST_PUSH_VERIFY and RELEASE_CROSS_VERIFY both test != 'false', so only
  the literal string false disables them. A reader who set either to 0 or off
  would believe it was disabled and be wrong.
- NUGET_POST_PUSH_ATTEMPTS (40) and NUGET_POST_PUSH_SLEEP_SEC (15) are the
  ~10-minute nuget.org visibility budget. nuget.org indexing lags publication,
  so a shortened budget reports "not visible" for a package that published fine
  - which reads as a failed release.
- NUGET_STAGING_FEED_URL and NUGET_STAGING_API_KEY are the only way to rehearse
  a publish against something that is not nuget.org, and neither was documented.
- RELEASE_NOTIFICATION_WEBHOOK_URL, NUGET_POST_PUSH_VERIFY_PACKAGE_IDS, and
  GITHUB_TOKEN for completeness, the last so its absence from the configurable
  rows is not read as an omission.

Every value re-read from the live API on 2026-09-14: three variables and one
secret exist; the other eleven are genuinely unset.

The section also cited three workflow lines - reusable-release-nuget.yml:226,
:185 and release.yml:198. All three resolved correctly today, and all three will
rot the next time those files are edited, which is the defect this document has
already been corrected for twice. They now name the step or the condition.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…the A4 claim

docs/SELF-EXTEND-AUDIT.md is named by the public-v0.x go criterion "Known
limitations documented: certification-evidence.md + SELF-EXTEND-AUDIT.md
current", so it is read as live rather than as a dated sprint record.

Its control-flow trace carries 28 file.cs:line anchors from the audit date.
Measured 2026-09-14: five now land on a bare brace or a blank line, and landing
on real code is not the same as landing on the code described -
BackgroundAgentRegistry.cs:203 is an ArgumentNullException throw where the trace
describes machine-origin agent validation.

Annotated rather than re-pinned. That is this repository's policy for dated
documents, and re-pinning has already failed twice in this tree: a re-anchoring
in docs/certification-evidence.md went stale within hours of being written, and
a correction to two line ranges in ci/cert-gate-assertions.md was itself wrong.
Member and type names are stable; the note says to search for those.

The note also scopes A4. The audit lists "Post-apply verification with automatic
rollback exists and is tested" among the enforced invariants, which reads as
unconditional. SelfExtendAdmissionBridge takes IPostApplyVerification as an
optional parameter defaulting to null and rolls back only "when verification is
not null". AddCertificationInfrastructure supplies one, so the supported wiring
has the canary; a host constructing the bridge by hand and passing nothing gets
no canary and no rollback, silently. README carried the same overclaim and is
corrected in the same branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@IanFrelinger
IanFrelinger merged commit 736a74b into master Sep 15, 2026
21 checks passed
@IanFrelinger
IanFrelinger deleted the chore/release-record-truth branch September 15, 2026 01:23
IanFrelinger pushed a commit that referenced this pull request Oct 1, 2026
…ing and deprecation suppressions

WHY. Ashlar.Brick.Contracts is a stable-tier package, and the mechanism
docs/SdkCompatibilityPolicy.md names for its promise is the public-API analyzer:
RS0016 fails the build when a public symbol is in neither PublicAPI file. The five
files under src/Ashlar.Brick.Contracts/Authoring/Ports/ opened with a file-wide
`#pragma warning disable RS0016` that was never restored ("Profile ports ship via
Unshipped incrementally; suppress until batched" - the batch never came). None of
their lines was in either PublicAPI file, so 18 public types - AgentProfile,
AgentProfileCapabilities, GenerationTunables, BrickConstraintManifest,
GenerationRequest, GeneratedArtifact, AcceptanceDecision, DeploymentSmokeResult,
AcceptanceContext, AcceptanceResult, IAcceptanceEvaluator,
DefaultAcceptanceEvaluator, IAcceptanceGatedDeploymentTarget, IArtifactDrafter,
IDeterministicDrafter, ISandboxProvider, IDeploymentTarget, DeploymentApplyResult
(namespace Ashlar.Core.Domain.Bricks.Ports) - were promised by nothing, and any
change to them built green. With 0.2.0 about to publish, the promise in the policy
was false for them.

SHIPPED, NOT UNSHIPPED - the evidence.
- `git ls-tree v0.1.0|v0.1.1|v0.1.2 -- src/Ashlar.Brick.Contracts/Authoring/Ports/`
  lists all five files at every tag, and both `git diff v0.1.0 v0.1.2 -- .../Ports/`
  and `git diff v0.1.2 f0fd3fd -- src/Ashlar.Brick.Contracts/` are empty. Every one
  of these symbols was public, unchanged, in three tagged releases the policy
  records as published. That is a promise already made: Shipped.txt.
- Nothing in this surface is new since 0.1.2, so nothing goes in Unshipped.txt, and
  that is also what keeps the 0.2.0 tag commit right. The 0.2.0 cut is already on
  master (#628 set VERSION=0.2.0; #678 "release: cut 0.2.0" set
  ci/published-version=0.2.0 and recorded that PublicAPI promotion was a no-op),
  and RELEASE_RUNBOOK step 7's promotion has already left every Unshipped.txt at
  the bare header - no PublicAPI file has changed since. A line put in
  Unshipped.txt now would reach the 0.2.0 tag unpromoted unless someone re-ran
  step 7, and the tag is what turns a line into a promise; so a symbol genuinely
  new since 0.1.2 would belong in Shipped.txt as well. There is none here.
- The 147 lines are the analyzer's own, not hand-written: read from the RS0016
  diagnostics' APIName property (SARIF via -p:ErrorLog), 144 names identical for
  netstandard2.0, net8.0 and net10.0, plus the three positional-record primary
  constructors the analyzer only reports once their types are declared. `dotnet
  format analyzers --diagnostics RS0016` applied no fix to this multi-targeted
  project (measured; also single-TFM), so it was not used. The lines carry nullable
  annotations (`string!`) as the analyzer emits them and as the Abstractions,
  Client, Sdk and Authoring files already do; the older oblivious Brick.Contracts
  lines are left alone. Shipped.txt: 465 -> 612 lines.
- Not [Experimental]: demoting a shipped stable API is a breaking change under the
  policy, and it would not be true - these are in use as a stable contract.

THE DEAD CS0618 PRAGMA. src/Ashlar.Hosting/Sdk/Builders/HostAshlarSdkBuilder.cs:7
disabled CS0618 because "AshlarSdkBuilder is an obsolete type forwarder in this
file". #223 (2026-07-04) moved the forwarder into AshlarSdkBuilder.cs and the pragma
stayed, suppressing nothing for 88 days while reading as a reason - and silencing
in advance the next obsolete call anyone wrote into the file. Deleted. Measured:
Ashlar.Hosting builds with 0 warnings / 0 errors without it on net8.0 and net10.0.

THE PREVENTION: DiagnosticSuppressionConventionTests (cert-gate namespace, row in
ci/cert-gate-assertions.md).
- Rule 1: nothing switches public-API tracking off. No source anywhere disables
  RS0016/RS0017/RS0026/RS0027 by pragma or [SuppressMessage]/
  [UnconditionalSuppressMessage], and no source has a bare `#pragma warning
  disable`; no MSBuild file lists those ids in NoWarn/WarningsNotAsErrors; no
  .editorconfig/.globalconfig lowers them to none/silent/suggestion or sets
  `dotnet_public_api_analyzer.skip_namespaces`, and none that applies to an
  analyzer project lowers the ApiDesign category or every analyzer; no analyzer
  project or Directory.Build.* above one sets RunAnalyzers(DuringBuild) false or
  removes an API file from AdditionalFiles; and each of the 6 projects referencing
  the analyzer wires both API files as AdditionalFiles. The source scan is
  tree-wide on purpose: a pragma acts on its own file, files are linked into these
  projects from outside them, and an RS0016 pragma anywhere else is dead by
  definition.
- Rule 2: a `#pragma warning disable` of CS0618 or ASHLAREXP001 - in every spelling
  the compiler accepts, which for CS0618 includes the bare number: Roslyn maps a
  numeric pragma id (and a numeric /nowarn id) to "CS" + the number padded to four
  digits, so `618` and `0618` ARE CS0618 - must name, as an exact identifier, an
  in-tree [Obsolete] (CS0618 - not error: true, no DiagnosticId) or
  [Experimental(ASHLAREXP001)] symbol; at least one named symbol must be used in
  the code the pragma governs OUTSIDE a declaration carrying the same attribute
  (the compiler reports neither diagnostic in an obsolete/experimental context);
  and the comment must give a reason of at least three words. Tree-wide, because
  the defect it exists for was in Ashlar.Hosting, which does not use the API
  analyzer.
- Rule 2 also covers the disables that cannot carry such a comment: a
  [SuppressMessage]/[UnconditionalSuppressMessage] naming either id; a
  NoWarn/WarningsNotAsErrors element or attribute listing either id (any
  spelling) in any MSBuild file; and a `dotnet_diagnostic.<id>.severity` of
  none/silent/suggestion in any analyzer config. A project-wide disable names no
  symbol by construction, so every one must be on ReviewedProjectWideDisables,
  which records why. Two exist and both are deliberate, documented opt-ins:
  Directory.Build.targets:32 (test projects exercise the experimental surface;
  the diagnostic exists for external consumers) and the FirstFlight spike's
  csproj:14 (the spike is a consumer, outside Ashlar.sln and every gate).
- Eight existing pragmas fail Rule 2 in files outside this change; they are
  grandfathered by exact text, each with what clears it. Both lists fail in both
  directions (a new violation, or an entry that no longer matches), so each can
  only change by a reviewed diff. Compiler ground truth, measured by deleting each
  pragma and building on f0fd3fd: both Ashlar.Sdk/Legacy CS0618 pragmas are DEAD
  (Ashlar.Sdk builds clean - every use sits inside an [Obsolete] context); the
  test one is live but uncommented; all six ASHLAREXP001 pragmas are live, and
  five of them name no symbol ("touch-set", "lineage"). ObjectiveDocument.cs
  already complies and is asserted to pass.
- Stated floors, not derived ones (2500 C# files, 100 MSBuild files, 6 analyzer
  projects, 40 experimental names, 1 obsolete name, 10 disable pragmas; measured
  3149, 133, 6, 65, 2, 15). The obsolete floor is 1, under the measured 2, so that
  retiring one forwarder - the normal end of a deprecation - is not reported as a
  broken parser; the real-tree anchors (AshlarSdkBuilder, TouchSet, ...) say in
  their failure messages that a removed or graduated symbol means "move the
  anchor", not "the parser is broken". Controls over in-memory text pin every
  banned spelling (including 618/0618 and a numeric restore), immunity to
  comments and literals (including raw and verbatim strings), the obsolete-context
  rule, CS0619/DiagnosticId exclusion, the project-wide and attribute forms and
  what they must not catch (CS8618, 6180, WarningsAsErrors, a commented-out
  NoWarn), and the shipped HostAshlarSdkBuilder.cs verbatim as a dead pragma in
  all its spellings.

VERIFIED in the devtest container on the committed tree:
- Builds, 0 warnings / 0 errors: Brick.Contracts on netstandard2.0, net8.0 and
  net10.0; Hosting on net8.0 and net10.0; Tests.Infrastructure on net8.0 and
  net10.0. Ashlar.LocalDevCore.slnf (what build-core builds) restores and builds,
  0 errors. The class passes 96/96 on net8.0 and on net10.0 (TRX executed=96).
  scripts/run-cert-gate.sh exits 0: total=1410 executed=1410
  passed=1410 failed=0 (the class's 96 among them), zero-test guard 1410 against
  expected>=1405 from --list-tests, skip guard 0 skipped, matching
  scripts/cert-gate-skipped.baseline.
- Mutations, each proven applied by the original string's count moving, each
  restored to an empty `git status --porcelain`, then rerun green:
  * the shipped dead pragma back at HostAshlarSdkBuilder.cs:7 as
    `#pragma warning disable 618 // AshlarSdkBuilder is ...` -> the Rule 2 fact
    fails (95/96): "HostAshlarSdkBuilder.cs:7: '#pragma warning disable 618 ...'
    - CS0618: is dead". Review measured the first round passing 63/63 on it.
  * a new `new AshlarSdkBuilder(null!)` in Ashlar.Hosting under a bare
    `#pragma warning disable 618` -> Hosting builds exit 0 (the compiler IS
    silenced) and the Rule 2 fact fails: "ApivProbe.cs:1: '#pragma warning
    disable 618' - CS0618: carries no comment". Without the pragma, Hosting
    fails with CS0618 on net8.0 and net10.0 (2 errors).
  * `<NoWarn>$(NoWarn);CS0618</NoWarn>` in Ashlar.Hosting.csproj with that use
    present -> Hosting builds exit 0 and the project-wide fact fails naming
    "Ashlar.Hosting.csproj:9"; spelled `618`, it fails the same way.
  * `dotnet_diagnostic.CS0618.severity = none` in the root .editorconfig -> the
    project-wide fact fails naming ".editorconfig:188".
  * FirstFlight's reviewed NoWarn deleted -> the project-wide fact fails: "is on
    the reviewed list but no longer found".
  * an assembly-level [SuppressMessage("Usage", "CS0618:...")] -> the Rule 2 fact
    fails naming it.
  * numeric-id normalisation disabled -> exactly the 9 numeric-spelling controls
    fail (87/96); the project-wide element scan disabled -> the real-tree
    project-wide fact fails with BOTH reviewed entries stale, plus the 5 element
    controls (90/96).
  * from the first round, still standing: RS0016 pragma back on AgentProfile.cs:1
    -> Rule 1 fails naming it; a new public property on GenerationTunables ->
    Brick.Contracts fails RS0016 for Extra.get/Extra.init on all three TFMs, and
    the SAME change with the old pragma restored builds 0/0 - what the
    suppression hid; one Shipped.txt line removed -> RS0016 on all three TFMs;
    RS0016 in the project's NoWarn -> Rule 1 fails; a Ports-scoped
    skip_namespaces -> the property's RS0016 disappears (the option really
    untracks) and Rule 1 fails naming the line; the eight grandfathered pragmas
    rewritten as their notes say -> eight "no longer violates", and with the
    list emptied it passes; declaration parser broken -> the floor fires;
    obsolete-context rule removed -> exactly its control fails; literals not
    blanked -> six facts fail.
- scripts/ci/run-repo-gates.sh: all 21 gates pass. Knowledge graph regenerated
  (declared facts 4399 -> 4414) and verified current.

WHAT THIS DOES NOT FIX.
- The eight grandfathered pragmas (two to delete, six to name their symbols) and
  docs/SdkCompatibilityPolicy.md, which still quotes 465 Shipped lines for
  Brick.Contracts and does not list the Ports types - all outside this change.
- Rule 2 is lexical: it proves a disable names a real symbol that the governed code
  still mentions outside that symbol's own context, not that the compiler would
  report there (a lambda inside an obsolete member, nameof, a same-named symbol
  from another assembly all read as uses). An obsolete API declared outside the
  tree cannot be named and needs a reviewed grandfather entry. CS0612 (an
  [Obsolete] with no message) is a different id and is not policed.
- Neither rule evaluates MSBuild: conditions are ignored (a disable is flagged
  whatever its Condition), and a disable routed through a property of another
  name (`<NoWarn>$(MyList)</NoWarn>`) or passed on a command line (`-p:NoWarn=`,
  `-nowarn:` in a script or workflow) is not seen. TreatWarningsAsErrors,
  WarningLevel and the bulk `dotnet_analyzer_diagnostic.*` settings are not
  checked for Rule 2. Rule 1 follows no imports beyond the Directory.Build.*
  chain and does not treat an <auto-generated/> header as a suppression.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant