Skip to content

fix: get develop green — proposal drift, tsdoc, and the per-service base URL bug - #10

Merged
CartagoGit merged 11 commits into
developfrom
fix/green-develop-lint-and-postman
Sep 10, 2026
Merged

fix: get develop green — proposal drift, tsdoc, and the per-service base URL bug#10
CartagoGit merged 11 commits into
developfrom
fix/green-develop-lint-and-postman

Conversation

@CartagoGit

Copy link
Copy Markdown
Owner

Three gates were red on develop. All three are fixed here, none by relaxing a check.

validate-package — a real product bug

tanit generate aborted on its own parity check:

✘ 9 in the routes but NOT in the collection:
    GET    //api/users
    ...
→ Generation aborted.

pathToSegments strips the base URL before comparing routes, and it matched exactly one literal string — {{baseUrl}}. r00019 moved base URLs to per-operation refs, so a merged collection emits {{baseUrl_<serviceId>}}, which never matched and survived as a path segment. Confirmed directly before changing anything:

uriFromRaw("{{baseUrl}}/api/users")       → "api/users"
uriFromRaw("{{baseUrl_svc_a}}/api/users") → "{{baseUrl_svc_a}}/api/users"

The packaged binary therefore produced nothing. validate:package now passes end to end: 9 requests found, binary installs and generates, exit 0. Four regression tests added.

This also explains why the failure looked like a path bug: service ids derive from the framework search root, so in CI the id carried the temp directory and the error printed ..._tmp_postman-package-iyBnwp_consumer_mi-api}}/api/auth/refresh. That leak is a separate concern and is not addressed here.

lint — proposal drift

lint:proposals reported 19 violations. Sixteen were done/ proposals whose directory did not match their own kind frontmatter. Three were the same proposal existing in several lifecycle states at once (f00016 in three places, f00017 in two) — debris from a transition that COPIED instead of moving, swept in by an auto-commit.

Diffing decided which copy survives; the bodies are identical apart from frontmatter. f00016 keeps done/feats/, the only copy carrying the evidence: block with five real commit SHAs. f00017 keeps review/, a strict superset of the other.

Also documents the three public exports lint:tsdoc was blocking on, and regenerates docs/API.md.

Verified locally

  • bun run lint — green end to end
  • bun run validate:package — green
  • bun run lint:integration-verifier --audit — 5/5

Opened as a pull request rather than pushed directly: develop now requires ci-summary, which is exactly what these commits repair.

🤖 Generated with Claude Code

CartagoGit and others added 11 commits September 10, 2026 16:59
`lint:proposals` reported 19 violations, in two groups.

Sixteen were `done/` proposals whose directory did not match their own
`kind` frontmatter — fifteen `x000xx` files sitting in `done/fixes/`
while declaring themselves chores, feats, refactors or tests. Moved to
the directory each one names.

Three were the same proposal existing in several lifecycle states at
once: `f00016` in `done/feats/`, `in-progress/` AND `review/`, and
`f00017` in both `in-progress/` and `review/`.

That is debris from a lifecycle transition that COPIED instead of
moving, swept into the repository by an auto-commit (51b9571,
"chore: update …", 390/241/399 insertions — all new files, nothing
removed). It is the same accident class this migration exists to make
impossible.

Diffing the bodies decided which copy survives; they are identical apart
from frontmatter:

  - `f00016` keeps `done/feats/`. It is the only copy carrying the
    `evidence:` block with five real commit SHAs and
    `last-transition-from: review` — the complete lifecycle record. The
    other two are a re-run from `in-progress` that lost the evidence.
  - `f00017` keeps `review/`, a strict superset of the `in-progress/`
    copy, which was missing `last-transition-from` and its closing
    frontmatter delimiter.

Nothing is lost that git cannot restore.

Also documents the three undocumented public exports `lint:tsdoc` was
blocking on (`perOperationResolver`, `resolvePerOperationContext`,
`combineServices`) and regenerates `docs/API.md`. The docblocks say why
each one throws rather than falling back — for the resolver, inventing a
server for an operation whose service is missing is how a request ends
up authenticated against the wrong API.

`lint:proposals`: 166 proposals, no drift. `lint:tsdoc`: 135 files, all
public exports explained.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`validate-package` has been red on a real product bug, not a flaky gate:

    ✘ 9 in the routes but NOT in the collection:
        GET    //api/users
        ...
    → Generation aborted.

Every generated request failed the generator's own parity check against
the routes it had just discovered, so `tanit generate` aborted and the
packaged binary produced nothing.

`pathToSegments` strips the base URL before comparing, and it matched
one literal string:

    .replace(/\{\{baseUrl\}\}/, "")

r00019 moved base URLs to per-operation refs, so a merged collection
emits `{{baseUrl_<serviceId>}}` instead. That variant never matched, and
the whole variable survived as a path SEGMENT — `{{baseUrl_x}}/api/users`
normalised to `:p/api/users` while the discovered route normalised to
`api/users`, so nothing could ever line up.

Confirmed directly before changing anything:

    uriFromRaw("{{baseUrl}}/api/users")       → "api/users"
    uriFromRaw("{{baseUrl_svc_a}}/api/users") → "{{baseUrl_svc_a}}/api/users"

The pattern now accepts the optional `_<serviceId>` suffix. Only the
base URL is removed: `{{id}}` and every other variable stay, because
they are part of the route and the comparison normaliser handles them.

This also explains why the failure looked like a path bug. Service ids
are derived from the framework search root, so in CI the id carried the
temp directory the fixture was generated in, and the error printed
`l_express_tmp_postman-package-iyBnwp_consumer_mi-api}}/api/auth/refresh`.
That string was a symptom; the leak is a separate concern and is NOT
addressed here.

`validate:package` now passes end to end: 9 requests found, binary
installs and generates, exit 0. Four regression tests cover the bare
name, a separator-laden service id, other variables surviving, and an
absolute origin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`lint:fixtures` failed in CI and passed locally, which is the signature
of a file that exists on disk and not in git.

Four smoke fixtures reached `develop` carrying a manifest and no code:

    bun-modern-mini       4 files on disk, 3 tracked
    express-const-method  3 on disk, 2 tracked
    sse-mini              2 on disk, 1 tracked
    websocket-mini        2 on disk, 1 tracked

In each case the missing file is `server.js` — the source the scanner
reads. `.gitignore` has a broad `tests/**/*.js` to keep TypeScript's
emitted output out of the repository, and the comment directly above it
already states the intended exception:

    Sólo los fuentes (.ts) y los assets de tests
    (server.js en fixtures y examples) están versionados.

That policy was written down and never implemented — there was no
negation rule, so the broad pattern swallowed the fixtures too. The gate
was right: as its own message puts it, a `package.json` alone on
`develop` lowers framework coverage without warning.

Adds the two negations the comment promised and commits the four
sources. `lint:fixtures`: all fixtures have a manifest AND a source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixing `pathToSegments` in the previous commit uncovered the same defect
in a second place, with a different symptom.

The collection builder groups requests into folders from the first path
segment, and stripped the prefix with the same literal pattern:

    const uriForGroup = firstUrl.replace(/^\{\{baseUrl\}\}/, "");

With a per-service `{{baseUrl_<serviceId>}}` the prefix survived, so
`topGroupFor` returned the VARIABLE as the folder key and all nine
requests of the express example collapsed into one folder named
`{{baseUrl Express Tmp Tmp G84VVxQTKF Mi Api}}` — the temp directory,
title-cased, as a folder name.

    before: requests=9 folders=1
    after:  requests=9 folders=3  (Auth 2, Users 5, Orders 2)

Three copies of "what does a base URL look like" existed, and they
failed differently: one made every route miss the generator's parity
check, one wrecked the folder tree, and a third in `auth-flow` happened
to use a broader pattern and escaped. So the answer is now stated ONCE,
as `BASE_URL_VARIABLE` / `stripBaseUrlVariable` in `uri.helper`, and
both former copies call it.

Two more regression tests pin the edges: a variable in the MIDDLE of a
path is a path parameter and must survive, and `{{baseUrlOther}}` must
not be eaten by a prefix match.

`generate-json-report`, `postman.helper` and `collection-builder`
specs: 37 passing. `bun run lint` green end to end.

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

Adding `BASE_URL_VARIABLE` above `normalizeForComparison` separated that
function from the file-level block comment that had been serving as its
docblock, so `lint:tsdoc` correctly reported it as undocumented. It now
has its own, saying what the normalisation is FOR: a route discovered in
source and the request generated from it must compare equal no matter
which framework's parameter syntax either one used.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s it

`lint:contracts` was right to object: a constant declared next to the
function that first needed it forces every other consumer to import that
function to reach it. This one has three consumers already, which is the
whole reason it exists.

It joins `DEFAULT_BASE_URL` and `BASE_PATH_ENV_VAR` in
`contracts/constants/core/base-url.constant.ts` — the module that already
answers "what is a base URL here" — and `uri.helper` re-exports it so no
import site had to change.

lint:contracts: 467 types and constants, all in contracts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`tests/fixtures/multi-service/users-api/package.json` declared
`@nestjs/common@^11.0.0` and `@nestjs/core@^11.0.0`, both below the
`11.1.18` floor the repository records for packages with open alerts.

The gate's own docstring says why it exists: copying an old example to
make a new one reintroduces the alerts, and nobody notices until GitHub
rebuilds the dependency graph days later on another branch. That is
exactly what this fixture was — a copy carrying stale versions.

These are fake manifests that are never installed, so the bump is a
declaration change only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three e2e assertions were failing, and they looked like a genuine
isolation defect:

    apps_orders should have exactly one GET /health, got []

They are the same base-URL bug, in a fourth place — this time in the
tests' own helper:

    const path = rawUrl.replace(/^\{\{baseUrl\}\}/, "");

With a per-service `{{baseUrl_apps_orders}}` the prefix survived, so the
map was keyed on `{{baseUrl_apps_orders}}/health` and every
`methods.get("/health")` came back empty. Nothing was crossing between
services; the lookup key was wrong. Both files now call
`stripBaseUrlVariable`, which is why that helper was made a single
exported definition in the first place.

Two more fixture sources were also missing from git:
`tests/fixtures/multi-service-isolation/apps/{users,orders}/src/server.js`
— 5 files on disk, 3 tracked. Same `tests/**/*.js` rule as the smoke
fixtures, so the negation now covers `tests/fixtures/**/server.js` too.
Every `.js` under `tests/` is now either tracked or genuinely emitted
output; I checked the whole tree rather than the two I already knew of.

That closes the last of the four literal-`{{baseUrl}}` copies. Counted
in one place, they cost: an aborted generation, a collapsed folder tree,
three phantom isolation failures, and a fixture directory that only
worked on machines where it had once been generated.

`multi-service` + `multi-service-isolation`: 7 passing. `bun run lint`
green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The collections this tool generates referenced a variable that was never
declared. Measured on the express example:

    declared: baseUrl, token, id, authUsername, authPassword
    used:     baseUrl_express_tmp_tmp_FIqkC9ZZW4_mi-api, id

Every one of the nine requests pointed at an undefined variable, so a
user importing the collection into Postman got nine broken URLs — with a
temp directory in the variable name.

`baseUrlVariableFor` chose the per-service `{{baseUrl_<serviceId>}}`
whenever `serviceId` happened to be set. But that form only EXISTS in a
combined collection, where `combineServices` declares one variable per
service; a single-service collection declares plain `baseUrl` and
nothing else. The choice is now derived from what the collection
actually declares, which makes "every variable used is declared" true by
construction instead of by convention.

    before: {{baseUrl_express_tmp_tmp_FIqkC9ZZW4_mi-api}}/api/auth/login
    after:  {{baseUrl}}/api/auth/login          undeclared used: []

This also closes the last two literal `{{baseUrl}}` copies — the shared
test contract and the Laravel catalog enricher — bringing the count to
six places that each answered the question their own way.

One of those edits broke generation outright while I was making it: the
import landed inside a `import type { … }` block and every run died at
`catalog-enricher.service.ts:22`. Caught by generating a collection
rather than by trusting the diff, which is the only reason it is not in
this commit as a regression.

tests/e2e + tests/frameworks: 86 files, 1678 tests, 0 failures — from 41
failures before. `validate:package` green end to end. `typecheck` green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`develop` and `main` both require the status check `ci-summary`. Two
different files published a job by that name: the aggregate inside
`validate.yml`, and a standalone `ci-summary.yml` mirroring the
workflow's conclusion afterwards.

Checked against the live repository rather than reasoned about: the
commit at `develop`'s head carries exactly ONE `ci-summary` check run,
and it comes from `validate.yml`. The standalone file has never run at
all — `gh workflow list` does not even register it. The reason is that a
`workflow_run` trigger only fires from the DEFAULT branch, `main`, and
`main` carries only release-binaries, release-desktop and validate. The
file exists on `develop` alone.

So the required context is unambiguous TODAY, by accident. Had that file
ever reached `main`, two workflows would have published the same context
and the required check would have become ambiguous — a branch protected
by a check nobody can point at, which is the same class of defect as a
required check no workflow produces.

Its job is renamed to `validate-conclusion`, and the header now states
that it is inert and why. The author's intent is preserved; the
collision is not. `validate.yml` is untouched, so the required check
keeps resolving to exactly what it resolves to now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ci-summary` is the required status check on BOTH `develop` and `main`,
and it could not succeed. Every run ended:

    bun: command not found
    Process completed with exit code 127

The job runs `bun run scripts/gates/ci-summary.script.ts` on a runner
where nothing checked out the repository or installed bun. x00071 split
`validate` into eight parallel jobs and gave this one the verdict, but
not the tools to compute it.

It failed even when it should have passed. On the run that prompted this
commit all eight dependencies reported `success` — typecheck, lint,
test-coverage, validate-examples, bench-check, security-audit,
validate-package, integration-verifier — and the aggregate still exited
127.

A required check that cannot succeed does not protect a branch, it
closes it. This is the same shape as the `ci-complete` context no
workflow produced, which kept DelendAI's `main` unmergeable for months.

Adds checkout + setup-bun. Verified the verdict logic itself is sound by
running the script directly with the eight results in the environment:
exit 0 when all are `success`, exit 1 when one is `failure`. It was only
ever missing its interpreter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@CartagoGit
CartagoGit merged commit 311da1f into develop Sep 10, 2026
11 checks passed
@CartagoGit
CartagoGit deleted the fix/green-develop-lint-and-postman branch September 10, 2026 16:40
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