Skip to content

feat(conformance): add the executable four-path producer contract - #9

Merged
qmarcelle merged 2 commits into
mainfrom
feat/four-path-producer-conformance
Jul 26, 2026
Merged

feat(conformance): add the executable four-path producer contract#9
qmarcelle merged 2 commits into
mainfrom
feat/four-path-producer-conformance

Conversation

@qmarcelle

Copy link
Copy Markdown
Contributor

The four stable read paths had a prose contract in docs/conformance.md and no executable one. This adds the standard-owned suite that measures a producer candidate against it, plus red tests proving each protected behavior is actually protected.

check:conformance        a candidate satisfies the contract
check:conformance:test   breaking each protected behavior makes that fail

Follows the existing check:architecture / check:architecture:test idiom.

It runs against a candidate, and does not skip

The producer lives in another repository, so the suite takes WORKSPACEJSON_CLI_CANDIDATE — a built package directory.

It does not skip when the candidate is absent; it exits non-zero with instructions. A conformance gate that goes green because it could not find the implementation reports conformance it never measured — the same principle as check:examples failing on an empty examples directory.

What is asserted

Path Asserted
generated.fileIndex Non-empty from repository evidence; keys repository-root-relative POSIX; every key names a file that exists; real files represented; deterministically ordered
generated.frameworkManifest Dependency-corroborated frameworks published at the documented >= 0.7 floor; uncorroborated AGENTS.md tokens not published; deterministically ordered
manual.fragileFiles Preserved verbatim; absent left absent, never fabricated
manual.coChangePatterns Preserved verbatim; absent left absent, never fabricated

Beyond the paths: invalid artifacts refused rather than overwritten, --force moving them aside recoverably, generated.by.name not attributable to an invoker, output validating against the package-owned schema, a second run on an unchanged repository byte-identical, and mediated invocation producing the same artifact as direct after removing only generated.generatedAt.

What is deliberately NOT asserted

Per-file values inside fileIndex. Every value field is optional in the published type, so {} is a conformant entry. Those values are behavioral, git-derived, and whether that source may enter the stable contract is an open determination made outside this repository. Requiring them would fail a producer that is behaving correctly.

The scope boundary is stated in the suite header so a later reader does not mistake it for an oversight. Also unchanged: no requirement on non-empty human-owned fields, and no git-derived co-change on the acceptance surface.

The red tests carry two honesty guards

  1. Each mutation asserts it changed bytes. A stale anchor would otherwise leave the candidate pristine, the suite green, and the case reporting "the guard works" having tested nothing.
  2. Each case names the check it expects to break. Reddening the suite for an unrelated reason is not evidence about the targeted behavior.

Plus a baseline asserting a clean candidate is accepted, so the suite cannot pass by rejecting everything.

baseline: an unmutated candidate is accepted
rejected — fileIndex emptied
rejected — fileIndex key ordering removed
rejected — producer-output exclusion removed
rejected — framework confidence below the documented floor
rejected — manual evidence discarded instead of preserved
rejected — producer identity misattributed to an invoker
rejected — refusal on an invalid artifact removed
8 passed, 0 failed

It found a real defect immediately

First run against the current producer: 24/28, three failures. The producer indexed its own artifact, so run 1 and run 2 disagreed and generate --check would fail on a repository's first CI run after adoption. Fixed in workspacejson/cli#12; the case that catches it is retained here. That is the watched-red → watched-green receipt.

⚠️ CI needs a decision from you

workspacejson/cli is private, so the new job needs WORKSPACEJSON_CLI_READ_TOKEN — a fine-grained PAT scoped to contents: read on that repository and nothing else.

Until that secret exists this job will fail, and that is deliberate — the alternative is a gate that skips and reports nothing. If you would rather it not block main before the token is configured, say so and I will gate the job on the secret's presence with a loud warning instead. I did not make that call unilaterally because it trades enforcement for greenness.

Local run against a built candidate is green either way.

Verification

check:architecture       OK — 140 tracked files, 0 violations
check:architecture:test  25 passed
check:docs               OK — 37 markdown files, 119 links, 36 commands verified
check:schema             OK
check:examples           OK — 4/4
check:conformance        28 passed, 0 failed
check:conformance:test   8 passed, 0 failed

check:docs flagged internal tracker identifiers in my first draft of the workflow comment and script headers; all ten are removed and the work is described instead, per this repository's public-prose rule.

The four stable read paths had a prose contract in docs/conformance.md and no
executable one. This adds the standard-owned suite that measures a producer
candidate against it, plus red tests proving each protected behavior is
actually protected.

  check:conformance        a candidate satisfies the contract
  check:conformance:test   breaking each protected behavior makes that fail

The suite runs against a candidate supplied by WORKSPACEJSON_CLI_CANDIDATE,
because the producer lives in another repository. It does NOT skip when the
candidate is absent — it exits non-zero with instructions. A conformance gate
that goes green because it could not find the implementation reports
conformance it never measured.

Asserted per path: fileIndex populated from repository evidence with
repository-root-relative POSIX keys that name real files, deterministically
ordered; frameworkManifest publishing only dependency-corroborated frameworks
at the documented confidence floor, and omitting uncorroborated tokens;
manual.fragileFiles and manual.coChangePatterns preserved verbatim, and left
absent rather than fabricated when a maintainer declared nothing.

Beyond the paths: invalid artifacts refused rather than overwritten, --force
moving them aside recoverably, producer identity not attributable to an
invoker, output validating against the package-owned schema, a second run on an
unchanged repository byte-identical, and mediated invocation producing the same
artifact as direct after removing only generated.generatedAt.

Deliberately NOT asserted: per-file values inside fileIndex. Every value field
is optional in the published type, so an empty entry is conformant. Those
values are behavioral and git-derived, and whether that source may enter the
stable contract is an open determination made outside this repository.
Requiring them would fail a producer that is behaving correctly. The scope
boundary is stated in the suite so a later reader does not mistake it for an
oversight.

The red tests carry two honesty guards: each mutation asserts it changed bytes,
so a stale anchor cannot leave a case silently testing nothing; and each names
the check it expects to break, so reddening the suite for an unrelated reason
does not count. A baseline case asserts a clean candidate is accepted, so the
suite cannot pass by rejecting everything.

Running this against the current producer found a real defect on the first
attempt: the producer indexed its own artifact, so run 1 and run 2 disagreed
and generate --check would fail on a repository's first CI run after adoption.
Fixed in the producer repository; the case that catches it is retained here.
Copilot AI review requested due to automatic review settings July 26, 2026 21:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…blic

The conformance job checked out the producer with a fine-grained PAT because
that repository was private. It is public, so the checkout needs no token and
no secret.

This is strictly better than the token arrangement it replaces:

- the gate runs on forks and on first-time contributors' pull requests, which
  a secret-dependent job cannot;
- there is no credential to provision, rotate or leak;
- nothing has to be configured before the gate can enforce anything, so the
  job does not sit red waiting on an out-of-band step.

Verified with an unauthenticated clone against a disabled global git config, so
the result does not depend on a credential helper that happens to be present
locally.

Also ignores .candidate/, the checkout path. A conformance run leaves no
untracked noise, and a vendored copy of the producer cannot be committed by
accident — `packages/cli` under a tracked path would trip the
repository-boundary guard.

The job continues to track the producer's default branch rather than a pinned
SHA. Noticing when the implementation drifts from the contract is the point; a
pin would only re-prove a known-good pairing.
@qmarcelle

Copy link
Copy Markdown
Contributor Author

The CI decision dissolved — workspacejson/cli is public

Pushed dd9dcf2. The job no longer takes a token, and there is nothing for you to configure.

       - name: Check out the producer under test
         uses: actions/checkout@v7
         with:
           repository: workspacejson/cli
           ref: main
           path: .candidate
-          token: ${{ secrets.WORKSPACEJSON_CLI_READ_TOKEN }}

Strictly better than the arrangement it replaces:

  • the gate runs on forks and first-time contributors' pull requests, which a secret-dependent job cannot;
  • no credential to provision, rotate or leak;
  • nothing has to be configured before the gate can enforce anything — so it does not sit red waiting on an out-of-band step.

Verified with an unauthenticated clone against a disabled global git config, so the result does not depend on a credential helper that happens to be present locally:

$ GIT_CONFIG_GLOBAL=/dev/null git clone --depth 1 https://github.com/workspacejson/cli.git candidate
  exit=0 ; 16 entries

Also in this commit

.candidate/ is now ignored. Two reasons, the second being the one that matters: a conformance run leaves no untracked noise, and a vendored copy of the producer cannot be committed by accidentpackages/cli under a tracked path would trip the repository-boundary guard.

Worth noting the guard itself is unaffected either way: check-architecture reads git ls-files, so an untracked checkout is invisible to it. I checked rather than assumed, because the producer repository deliberately caches its own parity source outside its working tree for exactly this hazard.

Ref pinning — deliberate

The job still tracks the producer's default branch rather than a pinned SHA. Noticing when the implementation drifts from the contract is the entire point of the gate; a pin would only ever re-prove a known-good pairing.

Gates, re-run after the change

check:architecture       PASS
check:architecture:test  PASS
check:docs               PASS
check:schema             PASS
check:examples           PASS
check:conformance        PASS  (28 passed)
check:conformance:test   PASS  (8 passed)

No open decisions on this PR now.

@qmarcelle

Copy link
Copy Markdown
Contributor Author

The red CI is the gate working — merge order matters

Four-path producer conformance fails with 24/28, and it should. The job checks out the producer at main, and main still has the defect this suite found:

$ gh api ".../evidence.ts?ref=main"                                  | grep -c producerOutputs
0        # defect present
$ gh api ".../evidence.ts?ref=fix/meta-198-fileindex-self-inclusion" | grep -c producerOutputs
2        # fixed

The four failures are all one cause — the producer indexing its own artifact, so run 1 and run 2 disagree:

FAIL  a second run against an unchanged repository is byte-identical
FAIL  a second run reports no material drift (usable as a CI gate)
FAIL  direct and mediated output are identical after removing only generated.generatedAt
FAIL  mediated invocation over a current artifact writes nothing

The last two are downstream of the first: an unconverged artifact means the mediated run rewrites where it should skip.

Merge order

1. workspacejson/cli#12   <- the producer fix (all 3 checks green)
2. this PR                <- goes green automatically, no rebase needed

Nothing to change here. The job tracks the producer's default branch deliberately, so this red is a true statement about cli@main right now. Better that you see the gate bite on a real defect than merge it green against a pinned known-good SHA.

test (20) and test (22) both pass.


On Dependabot #8 (@types/node 22.19.17 -> 22.20.1) — safe, with two notes

Tested locally rather than guessed:

@types/node 22.20.1 installed
packages/spec    36 tests passed
packages/rules  173 tests passed
typecheck        Done (both packages)
check:architecture / schema / examples / conformance   all PASS

Pins restored in this branch — the bump is not mixed into this PR.

Note 1 — its green predates this job. #8 shows "2 successful checks"; this PR adds a third. After #9 merges, #8 needs a rebase and will run the conformance gate. Its current green does not cover it.

Note 2 — it creates a pin divergence. standard would move to 22.20.1 while workspacejson/cli stays pinned at 22.19.17, and the conformance job now builds both in one CI run. Harmless — dev-only, and the suite exercises built output — but worth knowing, because the CLI's @types/node pin is exactly what surfaced the Dirent defect fixed in cli#12. Bumping the CLI's pin should be a separate, deliberate step, not a side effect.

Unrelated but easy: Dependabot reports The following labels could not be found: dependencies. .github/dependabot.yml requests it at lines 14 and 73, and the repository has only the nine GitHub defaults. Either create the label or drop those two labels: blocks — Dependabot will keep warning on every PR until one of those happens.

One good finding while checking

standard's types/ambient.d.ts shadows only four third-party modules — simple-git, remark, ajv, ajv/dist/2020.js — and no node: builtins. So the ambient-shadowing exposure that hid a real compile error in the producer repository does not exist here. Worth having that scoped: this repository is clean on it.

@qmarcelle
qmarcelle merged commit 7fc03c2 into main Jul 26, 2026
5 of 6 checks passed
@qmarcelle
qmarcelle deleted the feat/four-path-producer-conformance branch July 26, 2026 22:18
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.

2 participants