Skip to content

fix: widen the ts-ci PR filter to match its own push filter; consume contractViolations - #40

Open
mdheller wants to merge 2 commits into
mainfrom
fix/ts-ci-filter-and-contract-violations
Open

fix: widen the ts-ci PR filter to match its own push filter; consume contractViolations#40
mdheller wants to merge 2 commits into
mainfrom
fix/ts-ci-filter-and-contract-violations

Conversation

@mdheller

Copy link
Copy Markdown
Member

Two defects from the estate sweep. Separate commits.


1. ts-ci.yml — the required check's PR filter was narrower than its own push filter

on.push.paths declares ontology/** and scripts/** relevant. The PR change-detector matched only ^(package(-lock)?\.json|ts/|\.github/workflows/ts-ci\.yml). The derivation those push paths exist for is real:

ontology/kko/kko-2.10.n3        -> scripts/gen-kko.mjs             -> ts/src/kko-data.ts
ontology/effect-request/*.json  -> scripts/gen-effect-request.mjs  -> ts/src/*
ontology/semantic-action/*.json -> scripts/gen-semantic-action.mjs -> ts/src/semantic-action-data.ts
                                                                   -> ts/dist

So a re-vendor touching only ontology/ took the "required check trivially satisfied" branch and build-and-verify-dist went green having built nothing. It is a required check — branches/main/protection returns ["build-and-verify-dist","CodeQL"].

One correction to the finding, stated because it narrows it: the kko-provenance job does cover the first of those three chains — it runs on every PR with no path filter and check-kko-provenance.mjs verifies that ts/src/kko-data.ts reproduces from the vendored .n3. But it is not a required check, and nothing covers effect-request or semantic-action. PR #37 touches ontology/effect-request/PROVENANCE.md, so that is live traffic on the uncovered path.

The regex moves into a TS_PATHS env var, widened with ontology/ and scripts/. A new step then derives on.push.paths from the file itself and asserts the detector matches every declared family, so widening one filter and forgetting the other fails loudly rather than silently skipping a build.

Red-then-green

Same probe, both trees, reading the regex straight out of whichever ts-ci.yml is on disk:

PR touches only… origin/main this branch
ts/src/vendor-graph.ts BUILDS BUILDS
package-lock.json BUILDS BUILDS
ontology/kko/kko-2.10.n3 skipped (green, built nothing) BUILDS
ontology/effect-request/EffectRequest.json skipped BUILDS
ontology/semantic-action/SemanticAction.json skipped BUILDS
scripts/gen-kko.mjs skipped BUILDS
README.md skipped skipped
rust/src/lib.rs skipped skipped

Agreement assertion:

origin/main:  ASSERTION FAILS: push declares ['ontology/**', 'scripts/**'] relevant,
                               detector does not match them          exit=1
this branch:  ASSERTION PASSES: detector covers all 5 declared push paths   exit=0

2. vendor-graph.ts — a verdict computed, sealed, and never read

RevendorProposal.contractViolations is populated at proposeRevendor and sealed into the analysis, and its own doc comment has always said "Non-empty means the proposal is NOT emitted." Nothing implemented that sentence.

17 occurrences across the repo: 8 in the four compiled ts/dist copies, 4 in vendor-graph.ts (interface field, definition, populate, return), 5 in tests. No production reader — no branch, no .length, no throw, no exit code. analyzeVendorFreshness sealed every candidate regardless. The sibling dispositionViolations is a named top-level field, and the validator provably works (the TEETH test is a genuine counter-test).

What was done, and why

The declared rule becomes the actual rule. A candidate with a non-empty contractViolations is withheld from proposals, and its violations are named in a new top-level sealed field contractViolations, formatted ${pinKey}: ${violation} exactly as dispositionViolations is.

Both halves, not one. Withholding alone is a silent drop. Naming alone leaves a consumer free to act on a proposal the contract says was never emitted.

Reported, not thrown, for three reasons: analyzeVendorFreshness is documented PURE and deterministic; one malformed pin must not destroy the verdicts for every other pin in the run, which is exactly why dispositionViolations is a list and not an exception; and the module's doctrine is that the engine proposes while a membrane gate outside it decides — throwing would be the engine taking an action.

proposeRevendor's signature and behaviour are unchanged, so direct callers see no difference. For a conforming graph the only observable change is the new contractViolations: [], which does alter the seal hash. No test pins a literal analysis hash.

The new test drives the violation from the graph rather than mutating a finished request: the pin declares policy_labels: ['ops','ops'], proposeRevendor joins and re-splits it, and the vendored contract's uniqueItems rejects it — the shape a bad register entry would really take.

Red-then-green

ts/src/vendor-graph.test.ts:

state result first failure
this branch 23 pass / 0 fail
withholding reverted 22 / 1 only the conforming proposal is emitted
reporting reverted (field dropped from seal) 21 / 2 the withheld proposal is reported, not dropped
both reverted (origin/main behaviour) 21 / 2 only the conforming proposal is emitted

Full suite 412 pass / 0 fail, typecheck clean, check:kko clean, ts/dist rebuilt and committed.


Overlap to be aware of before merging

PR #37 also edits ts/src/vendor-graph.ts and ts/src/vendor-graph.test.ts, and its ontology/effect-request/PROVENANCE.md prose states the current behaviour explicitly:

"It still fails loudly there rather than recording a contractViolations entry, because analyzeVendorFreshness seals whatever proposals it is handed without reading that array: a bad instant must stop the run, not ride inside the seal."

That is the same defect, independently observed and worked around from the other side. Once this lands, that paragraph's premise no longer holds and the sentence should be revisited — I have deliberately not touched that file, since it belongs to #37.

Both #34 and #37 also modify ts/dist/*, so whichever merges second will need a npm run build and a re-commit of ts/dist. Unavoidable for a committed build artifact.

Rebased on origin/main @ 97c14e0 immediately before pushing.

mdheller added 2 commits July 29, 2026 23:43
…h filter

ts-ci.yml declared ontology/** and scripts/** relevant in on.push.paths, but the
PR change-detector matched only ^(package(-lock)?\.json|ts/|\.github/workflows/ts-ci\.yml).
The derivation those push paths exist for is real:

  ontology/kko/kko-2.10.n3        -> scripts/gen-kko.mjs             -> ts/src/kko-data.ts
  ontology/effect-request/*.json  -> scripts/gen-effect-request.mjs  -> ts/src/*
  ontology/semantic-action/*.json -> scripts/gen-semantic-action.mjs -> ts/src/semantic-action-data.ts
                                                                     -> ts/dist

so a re-vendor touching only ontology/ took the "required check trivially
satisfied" branch and build-and-verify-dist -- a REQUIRED check on main, per
branches/main/protection: ["build-and-verify-dist","CodeQL"] -- went green
having built nothing.

Worth stating precisely: the kko-provenance job does cover the FIRST of those
three chains, on every PR and with no path filter. But it is not a required
check, and nothing covers effect-request or semantic-action at all.

The regex moves to a TS_PATHS env var used by the grep, widened with ontology/
and scripts/. A new step then derives on.push.paths from this file and asserts
the detector matches every declared family, so widening one filter and
forgetting the other fails loudly instead of silently skipping a build.

Red-then-green, same probe against both trees:

  origin/main   ontology/kko/kko-2.10.n3                    -> skipped (green, built nothing)
                ontology/effect-request/EffectRequest.json  -> skipped
                scripts/gen-kko.mjs                         -> skipped
                assertion: FAILS -- push declares ['ontology/**','scripts/**'], detector does not match

  fixed         all of the above                            -> BUILDS
                README.md, rust/src/lib.rs                  -> still skipped
                assertion: PASSES -- detector covers all 5 declared push paths
…nread

`RevendorProposal.contractViolations` is populated by proposeRevendor and sealed
into the analysis, and its own doc comment has always said "Non-empty means the
proposal is NOT emitted". Nothing implemented that sentence. Across the repo the
field occurs 17 times: 8 in the four compiled ts/dist copies, 4 in vendor-graph.ts
(the interface field, the definition, the populate, the return), and 5 in tests.
No production reader -- no branch, no `.length`, no throw, no exit code.
analyzeVendorFreshness sealed every candidate regardless. Meanwhile the sibling
dispositionViolations IS a named top-level field of the sealed analysis, and the
validator provably works (vendor-graph.test.ts's TEETH test is a real counter-test).

The declared rule is now the actual rule. A candidate whose contractViolations is
non-empty is WITHHELD from `proposals`, and its violations are named in a new
top-level sealed field `contractViolations`, formatted `${pinKey}: ${violation}`
exactly as dispositionViolations is.

Both halves are needed. Withholding alone is a silent drop. Naming alone leaves a
consumer free to act on a proposal the contract says was never emitted.

Reported, not thrown, for three reasons: analyzeVendorFreshness is documented PURE
and deterministic; one malformed pin must not destroy the verdicts for every other
pin in the run, which is why dispositionViolations is a list and not an exception;
and the module's whole doctrine is that the engine proposes while a membrane gate
outside it decides -- throwing would be the engine taking an action.

proposeRevendor's own signature and behaviour are unchanged, so direct callers see
no difference. For a conforming graph the observable output is also unchanged; only
the new empty `contractViolations: []` is added, which does change the seal hash.
No test pins a literal analysis hash.

Red-then-green, ts/src/vendor-graph.test.ts:
  restored                                   23 pass / 0 fail
  withholding reverted                       22 / 1  "only the conforming proposal is emitted"
  reporting reverted (field dropped)         21 / 2  "the withheld proposal is reported, not dropped"
  both reverted (origin/main behaviour)      21 / 2
Full suite 412 pass / 0 fail; typecheck clean; ts/dist rebuilt.
Copilot AI review requested due to automatic review settings July 30, 2026 03:48

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.

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Fixes two defects: (1) ensures the TS required CI check runs for PRs that affect ts/dist indirectly via vendored ontology/scripts changes, and (2) enforces the documented rule that re-vendor proposals with contract violations are not emitted, while still reporting those violations in the sealed analysis.

Changes:

  • Widen PR change detection in ts-ci.yml to include ontology/** and scripts/**, and add a guard asserting PR detection covers on.push.paths.
  • Update analyzeVendorFreshness to withhold contract-violating proposals and surface them as a new top-level contractViolations field in the sealed record.
  • Add tests verifying withholding + reporting behavior and that contractViolations participates in the seal hash.

Reviewed changes

Copilot reviewed 3 out of 7 changed files in this pull request and generated 2 comments.

File Description
ts/src/vendor-graph.ts Adds top-level contractViolations to the sealed analysis and filters out invalid proposals.
ts/src/vendor-graph.test.ts Adds regression tests ensuring violating proposals are withheld and still reported/sealed.
.github/workflows/ts-ci.yml Aligns PR path detection with push paths and adds a self-guard to prevent filter drift.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ts/src/vendor-graph.ts
Comment on lines +973 to +983
/**
* Proposals WITHHELD because they did not validate against the vendored contract, as
* `${pinKey}: ${violation}`. `RevendorProposal.contractViolations` says a violating proposal is
* not emitted; this is where the ones that were not emitted are named, so the withholding is
* visible in the seal instead of being a silent omission.
*/
contractViolations: string[]
/**
* Re-vendor proposals. Emitted, never executed; empty unless `requestedAt` is supplied.
* Contains only proposals that validated — see `contractViolations` for the rest.
*/
Comment on lines +61 to +76
python3 - <<'PY'
import os, re, sys, pathlib
wf = pathlib.Path('.github/workflows/ts-ci.yml').read_text().split('\n')
start = next(i for i, l in enumerate(wf) if l.startswith(' push:'))
end = next(i for i, l in enumerate(wf[start + 1:], start + 1) if re.match(r'^ \S', l))
declared = re.findall(r'^\s*-\s*"([^"]+)"', '\n'.join(wf[start:end]), re.M)
if not declared:
sys.exit('::error::could not read on.push.paths from ts-ci.yml — this guard has gone blind')
rx = re.compile(os.environ['TS_PATHS'])
# One representative path per declared family; a glob stands for a file beneath it.
bad = [p for p in declared if not rx.match(p.replace('**', 'x').replace('*', 'x'))]
if bad:
sys.exit(f'::error::on.push.paths declares {bad} relevant, but the PR change-detector '
f'regex does not match them. A PR touching only those paths would report '
f'build-and-verify-dist green without building anything.')
print(f'detector covers all {len(declared)} declared push paths: {declared}')
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