Skip to content

DO NOT MERGE — retrospective Copilot review of merged PR #33 (e4ae226, vendor graph) - #38

Closed
mdheller wants to merge 1 commit into
review-base/pr33from
review/pr33-retrospective
Closed

DO NOT MERGE — retrospective Copilot review of merged PR #33 (e4ae226, vendor graph)#38
mdheller wants to merge 1 commit into
review-base/pr33from
review/pr33-retrospective

Conversation

@mdheller

Copy link
Copy Markdown
Member

DO NOT MERGE — retrospective review only

This PR exists only to obtain a Copilot review of content that is already on main.

PR #33 ("engine 0.4.46: vendor graph — dependency staleness as a derived query") merged as
e4ae22682c6e5b231900c25d80d5b6cf9b03549f on 2026-07-30 without a Copilot review. This repo does
not auto-request Copilot, and Copilot cannot review a closed PR.

The diff below is byte-for-byte PR #33: base is dbe854f (main immediately before the merge),
head is e4ae226 (the merge commit itself). Nothing here is new work.

  • Base branch: review-base/pr33dbe854f
  • Head branch: review/pr33-retrospectivee4ae226
  • Both branches are throwaway. Neither targets main. Delete both once the review is captured.

What already happened on #33

  • Human adversarial review
  • CodeQL pass, zero open alerts on the merge ref
  • Full local test run (21 vendor-graph tests, 410 suite-wide)

This is a missing second opinion, not a known defect.

Where a reviewer's attention is most useful

  1. ts/src/vendor-graph.ts (1042 lines) — the whole surface, unreviewed by Copilot.
  2. The sha-assertion on the vendored EffectRequest schema (loadEffectRequestContract) — what it
    does and does not actually guard.
  3. analyzeVendorFreshnessproposeRevendor: RevendorProposal.contractViolations is computed
    and sealed but never branched on, while its own doc comment says "Non-empty means the proposal is
    NOT emitted."
  4. The three graph query functions (stalenessOf, blastRadiusOf, contractCrossingRiskOf) and the
    new export * from './vendor-graph' in ts/src/index.ts.

…#33)

* engine 0.4.46: vendor graph — dependency staleness as a derived query

prophet-platform ran engine 0.4.40 while main was 0.4.45. Five releases
invisible in production, one of them a silent-wrong Cypher defect. A second,
byte-identical copy in apps/lifecycle-warden went unnoticed the whole time,
because nothing enumerated the copies. Merging an upstream PR does not ship
it; only a re-vendor does.

The fix is not a better spreadsheet. This makes freshness a DERIVED query:
state the pins as typed nodes and edges, and recompute the verdict from the
graph every time it is asked.

ts/src/vendor-graph.ts implements the vfp: vocabulary from sociosphere PR #492
under its declared names rather than re-inventing them — vfp:Repository,
vfp:Artifact, vfp:ConsumerApp, vfp:VendorPin, vfp:Release, vfp:Contract, and
the edges vendors / producedBy / supersededBy / pinnedAt / pinFor / hostedIn /
releasedAs / changesContract / guardedBy. Every class is KKO-typed in the
AtomSpace TypeLattice: an Artifact is a kko:Products, and a VendorPin is a
kko:RelationTypes because a pin is a REIFIED relation carrying its own policy,
owner, disposition and dates — a governed object, not an edge label.

Three derived questions, all pure (store.version() is identical before and
after, asserted directly):

  1. stalenessOf       — the supersededBy path from the pin to the newest
                         artifact: release distance AND the intervening
                         artifacts, not a boolean.
  2. blastRadiusOf     — what breaks if I cut 0.4.46, answered BEFORE cutting
                         it and without creating a node for it. Counted over
                         ConsumerApp, never Repository: repo granularity is
                         exactly what hid the lifecycle-warden copy.
  3. contractCrossingRiskOf — whether the gap spans a release that moved a
                         load-bearing contract, read from DECLARED
                         changesContract edges. Never guessed from version
                         numbers: 0.4.43 and 0.4.45 are both patch bumps and
                         both moved contracts.

The governed trigger emits an EffectRequest-shaped PROPOSAL against the
sourceos-spec contract now vendored at ontology/effect-request/, sha-asserted
at import exactly as SemanticAction is, with scripts/gen-effect-request.mjs
mirroring gen-semantic-action.mjs. It is a proposal and nothing else — the
engine never re-vendors, never opens a PR, never writes. Same rule the NLQ
restricted search obeys. requiresHumanApproval is forced true whenever the gap
crosses a contract; the membrane decides, outside this engine. The contract
this plane speaks is itself a vendored copy subject to this plane.

analyzeVendorFreshness seals like enrich/explore/nlq: sha256 over the derived
output plus the {seq,nodes,edges} snapshot, so a staleness verdict is provable
and replayable. requestedAt is taken from the caller rather than the clock —
a wall-clock read would make the seal differ every run — and omitting it emits
no proposals rather than inventing one.

nlq.ts exports validateAgainst / assertSupportedKeywords / SchemaObj so both
vendored contracts clear ONE validation subset guarded by ONE keyword bar,
never two that can drift apart. format: date-time falls outside that subset,
so vendor-graph checks requestedAt explicitly rather than leaving it
unenforced.

Measured against a fixture of the real situation: release distance 5, blast
radius 2, crossesContract true over [receipt-shape, schema] at 0.4.43 and
0.4.45. Suite 389 -> 408, none broken.

* review: close the CodeQL ReDoS in slug(), and make the contract check provable

Two findings from review of #33.

1. CodeQL js/polynomial-redos (alert 35, the PR's one failing check) is a TRUE
   positive, not noise. `slug()` trimmed separators with `/^-+|-+$/`; on
   `x` + n dashes + `y` the `-+$` alternative is retried from every dash and
   each attempt scans to the end. Measured on the pre-fix code: 48 ms at
   n=10k, 2.8 s at n=80k, 11.4 s at n=160k, 146 s at n=400k — clean quadratic.
   `artifact_id` from a caller-supplied register becomes `pinKey`, which
   `slug()` puts straight into the EffectRequest `id`, and this is a published
   library, so the input is not ours to trust. Replaced with a linear index
   walk; same output.

2. `assert.deepEqual(p.contractViolations, [])` appeared in two tests, but an
   empty array is also what a validator that does nothing returns. The test
   named "validated against the sha-asserted vendored contract" only checked
   the pinned sha/specVersion/title — it proved the contract is PINNED, never
   that validation FIRES. Added a test that breaks a proposal against the same
   schema object across nine keywords (required, const, enum, type, pattern,
   minLength, uniqueItems, additionalProperties, nested properties) and
   requires a violation for each, so the green assertions above mean something.

Both new tests were verified to fail against the defect they guard: the ReDoS
test fails at 146 s with the old regex restored, and the teeth test fails on
"required" with `validateAgainst` stubbed to a no-op.

ts/src/nlq.ts is deliberately untouched — engine internals are out of lane.
@mdheller
mdheller requested a review from Copilot July 30, 2026 02:06

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.

Adds a vendor-freshness “vendor graph” plane to model vendored artifacts as a typed graph and answer derived staleness/risk questions, including generating governed re-vendor EffectRequest proposals validated against a sha-pinned vendored schema.

Changes:

  • Introduces vendor-graph.ts with ingest + three pure graph queries (stalenessOf, blastRadiusOf, contractCrossingRiskOf) and sealed analysis (analyzeVendorFreshness).
  • Vendors and sha-pins the EffectRequest JSON Schema, adds generator script, and validates proposals against the shared JSON-Schema subset validator.
  • Exports the new surfaces via ts/src/index.ts, adds comprehensive tests, and bumps package version.

Reviewed changes

Copilot reviewed 9 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
ts/src/vendor-graph.ts Implements vendor graph ingest, derived queries, proposal emission, and sealing
ts/src/vendor-graph.test.ts Adds tests covering ingest, queries, purity, sealing, and schema validation “teeth”
ts/src/nlq.ts Exports schema utilities so other vendored contracts can use the same validator + keyword bar
ts/src/index.ts Re-exports vendor-graph and the embedded effect-request data for library consumers
ts/src/effect-request-data.ts Generated embedded vendored schema bytes + pinned sha + specVersion
scripts/gen-effect-request.mjs Generator to embed schema bytes and pinned sha in TS output
package.json Bumps version to 0.4.46
ontology/effect-request/PROVENANCE.md Documents provenance and validation caveats of the vendored contract
ontology/effect-request/EffectRequest.json Adds the vendored contract JSON Schema source file

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

Comment thread ts/src/vendor-graph.ts
Comment on lines +868 to +875
export interface RevendorProposal {
pinId: string
/** Always `proposed`. An EffectDecision must precede any world change. */
status: 'proposed'
effectRequest: EffectRequest
/** Violations of the vendored contract. Non-empty means the proposal is NOT emitted. */
contractViolations: string[]
}
Comment thread ts/src/vendor-graph.ts
Comment on lines +1029 to +1039
const proposals = opts.requestedAt === undefined ? [] : pins
.filter((p) => p.freshnessState === 'stale')
.map((p) => {
const repo = store.out(store.out(p.pinId, VFP_EDGE.pinnedAt)[0]?.id ?? '', VFP_EDGE.producedBy)[0]
const repoName = str(repo?.properties['repoName'])
const br = blastRadius.find((b) => b.repository === repoName)
return proposeRevendor(store, p.pinId, {
requestedAt: opts.requestedAt!,
...(br ? { blastRadius: br.count } : {}),
})
})
Comment thread ts/src/vendor-graph.ts
Comment on lines +1054 to +1061
function newestReleasedVersion(store: HellGraphStore, repository: string): string {
const repoNodeId = vfpId.repository(repository)
const produced = store.in(repoNodeId, VFP_EDGE.producedBy)
// The newest artifact is the one nothing supersedes.
const heads = produced.filter((a) => store.out(a.id, VFP_EDGE.supersededBy).length === 0)
const versions = heads.map((a) => str(a.properties['version']) ?? '').filter(Boolean).sort()
return versions[versions.length - 1] ?? 'unknown'
}
Comment thread ts/src/vendor-graph.ts
Comment on lines +516 to +527
/** Walk `vfp:supersededBy` forward from `artifactNodeId`, longest path, cycle-safe. */
function supersessionChain(store: HellGraphStore, artifactNodeId: string): string[] {
const chain: string[] = []
const seen = new Set<string>([artifactNodeId])
let cur = artifactNodeId
for (;;) {
// Deterministic when a release history forks: lowest node id wins, so the receipt is replayable.
const next = store.out(cur, VFP_EDGE.supersededBy)
.map((n) => n.id)
.filter((id) => !seen.has(id))
.sort()[0]
if (next === undefined) return chain
Comment thread ts/src/vendor-graph.ts
Comment on lines +413 to +418
const releaseId = vfpId.release(sourceId, version)
const rProps: Record<string, PropertyValue> = { version, sourceId }
put(rProps, 'observedAt', field<string>(r, 'observedAt'))
store.addNode(releaseId, [VFP.Release], rProps)
store.addEdge(VFP_EDGE.releasedAs, artifactId, releaseId)
stats.releases++
Comment on lines +376 to +378
test('the analysis seals like enrich/explore, and is deterministic', () => {
const a = analyzeVendorFreshness(fixture(), { requestedAt: '2026-07-29T00:00:00Z' })
const b = analyzeVendorFreshness(fixture(), { requestedAt: '2026-07-29T00:00:00Z' })
@mdheller

Copy link
Copy Markdown
Member Author

Human adversarial pass — findings Copilot's review did not surface

Copilot's review above independently confirmed two things found in a parallel manual pass
(contractViolations never acted on; lexicographic versions.sort()), and added two that pass
missed (supersessionChain "longest path" doc mismatch; unguarded stats.releases++). Both of
Copilot's new ones were verified against the merged code and are real.

Below are the findings the manual pass produced that Copilot did not report. All were verified
by running the merged code, not by reading it.

1. The sha-assertion does not guard the file its own provenance record names

ontology/effect-request/PROVENANCE.md says the digest is "asserted at import … a drifted or
hand-edited copy fails LOUDLY at load." That is true of ts/src/effect-request-data.ts, and false
of ontology/effect-request/EffectRequest.json — the file the record is about. Nothing at
runtime reads the .json; it is only an input to scripts/gen-effect-request.mjs.

Proven by flipping additionalProperties: false → true and widening the id pattern to ^.*$ in
the on-disk .json:

gate result with a semantically tampered EffectRequest.json
npm test (410 tests) 410 pass
npm run typecheck pass
npm run check:kko pass
npm run check:dist pass

Mutating a byte in the embedded EFFECT_REQUEST_SCHEMA_TEXT does throw at import, as designed —
so the guard on the shipped bytes is real. But EFFECT_REQUEST_SCHEMA_TEXT and
EFFECT_REQUEST_SCHEMA_SHA256 are adjacent in one generated file, and the generator recomputes the
digest from whatever bytes are on disk while re-emitting the hardcoded upstream commit line. So a
tampered .json + a regeneration produces a self-consistent pin still claiming provenance from
487e4b61.

The only independent anchor is the literal in vendor-graph.test.ts:361 — and no workflow runs
the TS suite
(ci.yml is Rust-only; ts-ci.yml runs typecheck/build/dist-drift/check:kko).
PR #36 closes that.

PR #35 built exactly this gate for KKO four minutes later. There is no check:effect-request
(nor check:semantic-action). A ~40-line mirror of check-kko-provenance.mjs closes it.

2. policyLabels round-trips through a comma join and corrupts labels

Ingest stores policyLabels.join(','); proposeRevendor emits declaredLabels.split(','). A label
containing a comma is silently split in two, and it is schema-legal so nothing complains:

declared policy_labels : ["tier-1, prod-critical"]   (1 label)
emitted  policyLabels  : ["tier-1"," prod-critical"] (2 labels)
contractViolations     : []

These are trust-zone/policy-posture labels carried onto a governed proposal.

3. Three declared governance signals are ingested and never read

Written into the graph, read by nothing (observationMaxAgeDays is typed on the manifest and never
referenced at all):

  • observedAt + policy.observationMaxAgeDays — the register declares a 90-day observation
    budget; staleness never consults it. observed is derived purely from "is there a release history
    in the graph", so a pin whose upstream was last looked at a year ago reports current.
  • workspaceBound — computed with care (unbound ≠ absent), then never surfaced. A repo the
    workspace manifest does not know exists is the lifecycle-warden failure mode this plane was
    written to catch.
  • dueAt — remediation due date is stored; "overdue" is underivable.

dispositionViolations is the only aggregated violation list a receipt consumer would read, and
none of these reach it.

4. requestedAt is validated structurally only

ISO_DATE_TIME accepts 2026-99-99T99:99:99Z, 2026-02-30T00:00:00Z and 0000-00-00T00:00:00Z.
Open PR #37 relocates this regex into nlq.ts verbatim, so the gap survives that fix. (Copilot
raised the same point on #37.)

5. Severity note on the versions.sort() bug Copilot found

Not hypothetical: hellgraph published v0.4.4v0.4.9, so a register naming any of them alongside
a two-digit patch mis-sorts today, and any 0.10.x makes it unconditional. It only stays latent
because the current register happens to start at 0.4.40. The wrong proposedVersion is sealed
inside the receipt hash.

Reachability

Estate-wide scan (Node byte-scan over ~190k files, tarballs gunzipped, plus git grep across all
local and remote refs in prophet-platform / Noetica / sherlock-search): zero callers of any
of the eight new exports. The API is correctly bundled into ts/dist and does ship, but nothing
consumes it, no vendor-freshness manifest ships with the engine, and
sociosphere-ruleset-v2/tools/validate_vendor_freshness.py is an incumbent working implementation
with its own CI workflow.

Also worth noting: prophet-platform/apps/hellgraph-service/src/contract.ts:34 already pins
EffectRequest.json at the identical digest 99829aa5…. All three copies (sourceos-spec,
prophet-platform, hellgraph) are byte-identical — verified — but this vendoring created a second
independently-pinned copy of the same contract, which is the multi-copy condition the plane exists
to detect.

Meta

Copilot's reviews in this repo are running degraded — every review carries "Copilot couldn't run
its full agentic review … make sure your repository has a runner available". Coverage was 9/13 files
here, 11/15 on #34, 5/9 on #37. There is no .github/copilot-code-review.yml and the repo/org have
zero self-hosted runners. Worth fixing before relying on Copilot as a gate.


Nothing here is a security incident. #33 merged with a human adversarial review, a clean CodeQL
pass and a full local test run. Finding 1 is the one worth a follow-up PR.

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

Copilot reviewed 9 out of 13 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (5)

ts/src/vendor-graph.ts:1039

  • analyzeVendorFreshness already computes pins (staleness) and risks (contract crossings), but proposeRevendor() recomputes stalenessOf() + contractCrossingRiskOf() for every stale pin. On larger graphs this becomes redundant repeated traversals. Consider refactoring so proposals are built from the already-computed StalenessVerdict + ContractCrossingRisk (or expose an internal helper that accepts those precomputed values), keeping proposeRevendor() as a convenience wrapper.
  const proposals = opts.requestedAt === undefined ? [] : pins
    .filter((p) => p.freshnessState === 'stale')
    .map((p) => {
      const repo = store.out(store.out(p.pinId, VFP_EDGE.pinnedAt)[0]?.id ?? '', VFP_EDGE.producedBy)[0]
      const repoName = str(repo?.properties['repoName'])
      const br = blastRadius.find((b) => b.repository === repoName)
      return proposeRevendor(store, p.pinId, {
        requestedAt: opts.requestedAt!,
        ...(br ? { blastRadius: br.count } : {}),
      })
    })

ts/src/vendor-graph.ts:477

  • This can create a producedBy edge to vfpId.repository('') (or to a repository node that was never created) when the matching source entry lacks a valid repo (sources with missing repo are skipped in the earlier source ingest loop). Instead, read repo and only add the edge when it is non-empty, ideally reusing ensureRepo(repo, ...) so the target repository node is guaranteed to exist.
      const src = sources.find((s) => field<string>(s, 'sourceId') === sourceId)
      if (src) store.addEdge(VFP_EDGE.producedBy, pinnedArtifactId, vfpId.repository(field<string>(src, 'repo') ?? ''))

ts/src/vendor-graph.ts:165

  • The exported policy name 'track-minor' is implemented using only the major component (majorOf) and the docstring describes major-based behavior. This mismatch will confuse API/manifest authors and makes the policy semantics ambiguous. Either rename the policy to reflect major-based tracking (e.g., 'track-major') or update the implementation to actually track minor (major.minor) as the name implies (and keep docs consistent).
export type FreshnessPolicy = 'pin-exact' | 'track-minor' | 'track-latest'

ts/src/vendor-graph.ts:591

  • The exported policy name 'track-minor' is implemented using only the major component (majorOf) and the docstring describes major-based behavior. This mismatch will confuse API/manifest authors and makes the policy semantics ambiguous. Either rename the policy to reflect major-based tracking (e.g., 'track-major') or update the implementation to actually track minor (major.minor) as the name implies (and keep docs consistent).
const majorOf = (v: string): string => v.split('.')[0] ?? v

ts/src/vendor-graph.ts:644

  • The exported policy name 'track-minor' is implemented using only the major component (majorOf) and the docstring describes major-based behavior. This mismatch will confuse API/manifest authors and makes the policy semantics ambiguous. Either rename the policy to reflect major-based tracking (e.g., 'track-major') or update the implementation to actually track minor (major.minor) as the name implies (and keep docs consistent).
    const sameMajorNewer = intervening.some((a) => majorOf(a.version) === majorOf(pinnedVersion))
    const majorMoved = releaseDistance > 0 && majorOf(latestVersion) !== majorOf(pinnedVersion)
    freshnessState = sameMajorNewer || majorMoved ? 'stale' : 'current'

Comment thread ts/src/vendor-graph.ts
Comment on lines +868 to +875
export interface RevendorProposal {
pinId: string
/** Always `proposed`. An EffectDecision must precede any world change. */
status: 'proposed'
effectRequest: EffectRequest
/** Violations of the vendored contract. Non-empty means the proposal is NOT emitted. */
contractViolations: string[]
}
Comment thread ts/src/vendor-graph.ts
Comment on lines +904 to +914
export function proposeRevendor(store: HellGraphStore, pinId: string, opts: ProposeOptions): RevendorProposal {
if (!ISO_DATE_TIME.test(opts.requestedAt)) {
// The shared subset validator implements `format: "uri"` only, so `requestedAt`'s
// `format: "date-time"` would otherwise go unchecked. Checked here rather than left unenforced.
throw new Error(
`vendor-graph: requestedAt '${opts.requestedAt}' is not an ISO-8601 date-time (the contract's ` +
'format: date-time is outside the shared validator subset and is enforced here instead)')
}
const verdict = stalenessOf(store, pinId)
const risk = contractCrossingRiskOf(store, pinId)
const pin = store.getNode(pinId)!
Comment thread ts/src/vendor-graph.ts
Comment on lines +924 to +955
const effectRequest: EffectRequest = {
id: `urn:srcos:effect:vendor-revendor.${slug(verdict.pinKey)}.${slug(verdict.pinnedVersion)}-${slug(toVersion)}`,
type: 'EffectRequest',
specVersion: EFFECT_REQUEST_SPEC_VERSION,
requestedByEventRef: opts.requestedByEventRef ?? `urn:srcos:vendor-freshness:${slug(verdict.pinKey)}`,
effectKind: 'update',
capability: 'vendor.revendor',
target: {
kind: 'vendor-pin',
identifier: verdict.pinKey,
...(location !== undefined ? { location } : {}),
},
parameters: {
fromVersion: verdict.pinnedVersion,
toVersion,
gapSize: verdict.releaseDistance,
blastRadius: opts.blastRadius ?? 0,
crossesContract: risk.crossesContract,
contractKinds: risk.contractKinds,
},
// A re-emitted finding must not open a second PR.
idempotencyKey: `${verdict.pinKey}@${verdict.pinnedVersion}->${toVersion}`,
requiresHumanApproval: risk.crossesContract,
policyLabels: declaredLabels ? declaredLabels.split(',') : [],
riskLabels,
requestedAt: opts.requestedAt,
}

const contractViolations: string[] = []
validateAgainst(EFFECT_REQUEST_SCHEMA as SchemaObj, effectRequest, 'EffectRequest', contractViolations)
return { pinId, status: 'proposed', effectRequest, contractViolations }
}
Comment thread ts/src/vendor-graph.ts
Comment on lines +1054 to +1061
function newestReleasedVersion(store: HellGraphStore, repository: string): string {
const repoNodeId = vfpId.repository(repository)
const produced = store.in(repoNodeId, VFP_EDGE.producedBy)
// The newest artifact is the one nothing supersedes.
const heads = produced.filter((a) => store.out(a.id, VFP_EDGE.supersededBy).length === 0)
const versions = heads.map((a) => str(a.properties['version']) ?? '').filter(Boolean).sort()
return versions[versions.length - 1] ?? 'unknown'
}
@mdheller

Copy link
Copy Markdown
Member Author

Evaluation — and this PR should be CLOSED, not merged

Retrospective review container for already-merged #33 (e4ae226, vendor graph). Its job was to get Copilot onto the vendor-graph reasoner after the fact; done, twice. It must not be merged — its base is review-base/pr33 and merging would replay merged history. Recommend closing once the findings below are carried into a fix PR against main.

Copilot coverage: 9 of 13 files, with the degraded-mode banner — so roughly a third of the diff was not looked at, and the findings below should not be read as exhaustive. Two rounds: 10 visible comments and 5 suppressed.

Real — already owned by another lane

contractViolations is sealed without ever being read. Raised five times across both rounds (:875, :914, :955, :1039). RevendorProposal documents "non-empty means the proposal is NOT emitted", and analyzeVendorFreshness returns every proposal unconditionally, so the emitted array contradicts its own contract.

Confirmed alongside it: analyzeVendorFreshness has no caller anywhere outside its own test file. Checked across main — the only hits are its definition in vendor-graph.ts, vendor-graph.test.ts, and the ts/dist build output. A reasoner that nothing invokes cannot have been wrong in production yet, which is the only reason this has been survivable.

Another lane is fixing this in vendor-graph.ts; not duplicated here deliberately. Flagging only that the fix should settle the contract in one direction rather than both — either filter violating proposals out or make the interface say callers must check — because the current docstring and behaviour disagree and either resolution is defensible.

Real — not yet owned, and biting now

:1059newestReleasedVersion sorts versions lexicographically. Raised in both rounds.

const versions = heads.map((a) => str(a.properties['version']) ?? '').filter(Boolean).sort()
return versions[versions.length - 1] ?? 'unknown'

This is not a latent edge case — the engine is at 0.4.46/0.4.47 today:

lexicographic .sort(): ["0.4.10","0.4.46","0.4.5","0.4.9"] -> picks 0.4.9
semver-correct       : ["0.4.5","0.4.9","0.4.10","0.4.46"] -> should be 0.4.46

It feeds blastRadiusOf(store, { repository, proposedVersion: newestReleasedVersion(...) }), so a blast-radius report names the wrong proposed version for any repository past a two-digit patch. Needs a numeric-component comparator.

Real, minor

  • :477producedBy edge to an empty repository id (suppressed). vfpId.repository(field<string>(src, 'repo') ?? '') builds an edge to vfp:repo: when the matched source has no repo. Guard on non-empty and route through ensureRepo so the target node is guaranteed to exist.
  • :517 — docstring says "longest path", implementation takes the lowest next node id on a fork. The inline comment two lines down already says "lowest node id wins", so the function contradicts itself in adjacent lines. Doc fix — the deterministic behaviour is the one you want for a replayable receipt; the word "longest" is what is wrong.
  • :418stats.releases++ is unguarded while artifacts and contracts are gated by seen* sets. Inconsistent stats semantics if a manifest repeats a release.
  • Missing tests for semver ordering and forked supersededBy histories. Follows directly from the two findings above and would have caught the sort.

Rejected

  • 'track-minor' is implemented with majorOf (suppressed, raised three times). False positive. track-minor means "follow releases within the same major", and partitioning by major is how you implement that: sameMajorNewer marks a pin stale when a newer release shares its major, and majorMoved handles the major changing. Using majorOf is the mechanism, not evidence that the policy tracks majors. The name and the behaviour agree.
  • :1039proposeRevendor recomputes stalenessOf / contractCrossingRiskOf (suppressed). Correct that it recomputes, but this is a purity/clarity tradeoff the module makes deliberately — proposeRevendor is documented as pure with respect to the graph and usable standalone. Worth revisiting only if a profile shows it mattering; the graphs here are small and no caller exists yet.

Disposition

Nothing fixed here. The semver sort is the item worth raising as its own PR against main — it is wrong today, not eventually.

@mdheller

Copy link
Copy Markdown
Member Author

Review complete — closing, not merging

This PR did its job. It was never merge-eligible: it is byte-for-byte the already-merged content of #33 (e4ae226), staged against a throwaway base (review-base/pr33) purely to obtain the Copilot second opinion that #33 merged without. Merging it would re-apply main onto a review branch. Closing is the correct terminal state.

Recording the disposition of every finding here, because two of the three channels are easy to lose: Copilot left 10 inline comments and 5 more inside the <details>Comments suppressed due to low confidence</details> block on the review body, and that suppressed block is returned by neither issues/38/comments nor pulls/38/comments. It is only in the review body.

Findings the review produced, and where they went

# Finding Disposition
1 newestReleasedVersion uses lexicographic .sort()0.4.9 beats 0.4.46 Fixed — #42. Was live on main; the engine is past a two-digit patch, so every blast-radius report named a wrong proposedVersion
2 RevendorProposal.contractViolations computed and sealed but never branched on, contradicting its own doc comment Fixed — #40
3 supersessionChain docstring claims "longest path", implementation takes lowest next node id Fixed — #42 (docstring corrected; the lowest-id behaviour is deliberate and replayable, so the code was right and the comment was wrong)
4 producedBy edge built to vfp:repo: when a source has no repo — dangling edge to a node the ingest loop never creates Fixed — #42
5 stats.releases++ counts manifest lines while every sibling counter counts nodes created Fixed — #42
6 No test coverage for semver ordering or forked supersededBy histories Fixed — #42 (both added, red-before-green: 4 new tests fail against unmodified source)
7 proposeRevendor() recomputes stalenessOf() + contractCrossingRiskOf() per stale pin instead of reusing analyzeVendorFreshness's already-computed pins/risks NOT COVERED — performance only, no correctness impact. No PR, no task.
8 The exported policy 'track-minor' is implemented using only majorOf, and its docstring describes major-based behaviour NOT COVERED — no PR, no task. The sharpest uncovered item from this review: a policy whose name states one contract and whose implementation honours another. Raised three times in the suppressed block (vendor-graph.ts:165, :591, :644) and never surfaced in the inline channel at all.

Items 7 and 8 are carried forward to the estate backlog by this comment. Item 8 should get a PR — either rename the policy to 'track-major' or implement minor-tracking; today a manifest author selecting track-minor silently gets major-tracking.

Branches

review/pr33-retrospective and review-base/pr33 are throwaway and can be deleted once #39 is also closed (#39's base is this PR's head branch). Not deleting them here.

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