Skip to content

refactor: the 2.0 pre-release fixes — one home per concept, one portal gate, one project layout - #374

Merged
saeedjamshaid merged 6 commits into
devfrom
saeedjamshaid/one-home-per-concept
Sep 25, 2026
Merged

saeedjamshaid merged 6 commits into
devfrom
saeedjamshaid/one-home-per-concept

Conversation

@saeedjamshaid

@saeedjamshaid saeedjamshaid commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

These are the pre-release fixes from the 2.0 release review, planned in .ai/plans/release-2.0-fixes.md (added here). The review found no must-fix defects, but it did find concepts living in more than one place, a duplicated gate, an unguarded contract, and one recovery path with no tests. There are six commits: one per batch, one for a decision made in review, and two that answer a review of this PR. Each builds and passes the suite on its own. The plan's §3.8, §4.6 and §5.6 record where each commit departed from the plan.

1. One home per concept (fec7109d)

  • The languages block of apimatic.json now has one module, types/apimatic-config/languages-block.ts. It holds the types, the entry builder, the merge, isPublished, and the portal's lenient reader. That reader names every key against its type, so a rename fails tsc instead of making the portal read nothing without any error. language-entry.ts is gone, and record-published-sdk.ts is back to just the action class.
  • TIMING_DEFAULTS / GenerationTimings are defined once, in the generation poller, instead of once per service.
  • FileService.readFile is gone. It duplicated getContents, and its only caller, hasContent, was itself unused.
  • Portal output file names: _shell.html, 404.html and portal.zip are each named once, in portal-context.ts.
  • languageLabel: code-samples.ts uses the shared function.
  • Registry package-name fields are typed against the package configurations.
  • Prompts name apimatic.json and nav.json by their constants. DirectoryPath.fromUserInput() replaces three hand-rolled trim-and-unquote copies.

2. Portal flow polish (c1f95a9d)

  • One gate for generate and serve: the runtime and authorization checks move into PreparePortalProjectAction, which both commands run, and the copies go. Serve now probes its port once the project is prepared, so no fallback-port message appears before an authorization refusal.
  • portal.config.json is typed on both sides: PortalBuildPaths in the CLI, and BuildPaths in the template, which renames its PortalConfig. The same Equal<> test that guards the identity file now holds the two to one shape.
  • validation-service: validateViaFile closes its stream in a finally.
  • Dev server output is cleaned of colour codes with node:util's stripVTControlCharacters.
  • PortalContext.save gets its first tests (10 cases). A staging failure keeps the previous portal; a failed swap keeps the staged copy and says where it is.

3. One place that knows a project's layout (c5c55c36)

This redoes the intent of #372 on current dev.

  • ProjectContext absorbs BuildContext, which is deleted. ProjectContext.at(input) and ProjectContext.in(directory) are the only ways to get one. Commands and actions take the project, and no caller joins src, sdk, portal or plugin onto a path anymore.
  • A versioned build narrows the project to one version. versionToBuild() returns either the project to build from or a problem for the prompts to name. It replaces the closure that returned { version, buildContext } or an ActionResult, and the three getters that shared one preamble are now a single walk.
  • The layout's names live once, in types/project-layout.ts. GENERATED, the list of .gitignore entries, is derived from the output names.
  • The portal test fixtures are real projects, with their contents under src/.
  • Tests: ProjectContext has the unit tests BuildContext never had, covering the layout, the versioned walk and the destination overrides.

No behavior changes.

4. Drop the spec pruning nothing calls (7b7b7d82)

On main, quickstart used ValidationService.stripUnallowedFeatures to strip spec features the subscription doesn't allow. #363 removed that call, and review decided the pruning does not come back. This commit deletes the method, along with parseErrorResponse and FeaturesToRemove, which existed only for it. It also removes what only fed the pruning:

  • validateViaFile no longer decodes the x-unallowed-features header;
  • UnallowedFeaturesResponse, RemovableFeature and the ValidateApiResponse wrapper go;
  • ValidateAction returns a plain ActionResult, since nothing read its value.

5. Only the comments the code cannot say (ba90b550)

The batches added multi-line doc blocks that restated the code or narrated its design, against the one-line-of-why rule. They are cut to one line or removed. What stays is a constraint the code cannot show, such as the server's 25-minute limit behind the 30-minute timeout, or why /plugin/ must stay untracked.

6. Delete quickstart's Project, and keep the source checks in ProjectContext (f22c2fc3)

  • Quickstart's Project record is gone. This finishes refactor: one place that knows a project's layout #372's "Project and BuildContext need to be deleted". The wizard runs as its two flows, adopting a downloaded project or starting a new one, which finish the same way. Each flow makes its ProjectContext once.
  • Checks against the source directory live in ProjectContext. isSourceDirectory() and isSourceWithin() replace six hand-written comparisons. sourceDirectory() now serves only prompts and services, and context.md's exception says so.
  • SonarCloud's three findings: the version problems reach one prompt keyed by problem, which drops the non-Error throw. The two smaller findings are fixed too.
  • validateViaFile opens its stream inside the try again. A path Node refuses outright, such as one containing a NUL byte, is reported instead of thrown.

No other behavior changes. Prompt text and order are the same, and a new test pins both version messages.

Decided in review

  • The gitignore/--destination gap is deferred to its own ticket (plan §5.2, step 9). plugin generate --destination can write to a directory .gitignore doesn't list, and the git init that plugin publish then prints would nest one repository inside another. It isn't a regression, since main has no .gitignore handling at all, and the fix changes behavior, so it needs its own design.
  • The spec pruning is dropped (commit 4).
  • Left as is, pre-existing on main: SdkPublishAction zips outputDirectory/<language>, re-deriving SdkContext's layout, while a versioned project writes its SDK one level deeper.

Test plan

  • pnpm build
  • pnpm lint
  • pnpm test: 1389 passing, 0 failing (38 pending, unchanged)
  • APIMATIC_E2E=1 portal build: 37 passing, two real Vite builds on the moved fixtures
  • Smoke: plugin publish --input <fixture> and sdk generate --input … --destination <src> resolve the project as before

🤖 Generated with Claude Code

The first of the three pre-release fix batches in .ai/plans/release-2.0-fixes.md.
Behavior-preserving consolidations:

- The apimatic.json `languages` block gets one module,
  types/apimatic-config/languages-block.ts: its types, the entry builder, the
  merge, `isPublished`, and a lenient reader for the portal whose keys are
  checked against the typed shape, so a rename fails tsc instead of reading nothing.
- TIMING_DEFAULTS and GenerationTimings live once, in the generation poller.
- FileService.readFile and its only caller, the unused hasContent, are gone.
- `_shell.html`, `404.html` and `portal.zip` are each named once, in portal-context.
- code-samples uses the shared languageLabel.
- The registry package-name fields are typed against the package configurations.
- Prompts name apimatic.json and nav.json by their constants, the artifacts
  reader uses FileName, and DirectoryPath.fromUserInput replaces three copies
  of trim-and-unquote.
- Riders: prompt.md says log.warn, and PortalSourceContext's doc comment uses
  the glossary's "source directory".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
saeedjamshaid and others added 2 commits September 26, 2026 00:52
…onfig.json, and a tested save

The second pre-release fix batch in .ai/plans/release-2.0-fixes.md:

- The runtime and authorization checks move into PreparePortalProjectAction, which both
  portal generate and portal serve run; the copies and their prompt wrappers go. Serve
  probes its port once the project is prepared, beside the server that uses it, so no
  fallback-port message precedes an authorization refusal.
- portal.config.json is typed on both sides (PortalBuildPaths in the CLI, BuildPaths in
  the template, which renames its PortalConfig) and held to one shape by the same Equal<>
  test that guards the identity file. portal.server.ts casts once.
- validation-service: the strip failure is a real ServiceError, so its message prints
  instead of `undefined`; validateViaFile closes its read stream in a finally.
- The dev server strips colour codes with node:util's stripVTControlCharacters.
- PortalContext.save, the staged replace of the portal directory, gets its first tests:
  staging failure keeps the previous portal, a failed swap keeps the staged copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The third pre-release fix batch in .ai/plans/release-2.0-fixes.md, and the redo of PR #372's
intent on current dev:

- ProjectContext absorbs BuildContext, which is deleted. ProjectContext.at(input) and
  ProjectContext.in(directory) are the only ways in; commands and actions take the project,
  and no caller joins `src`, `sdk`, `portal` or `plugin` onto anything.
- A versioned build narrows the project to one version instead of threading a
  { version, buildContext } pair, or an ActionResult, out of a closure: versionToBuild()
  answers the project to build from or a problem the prompts name, and the three getters
  that shared one preamble are one walk.
- The layout's names live once, in types/project-layout.ts. GENERATED, the .gitignore
  entries, derives from the output names; PortalSourceContext and the prompts read the
  source directory's children from there.
- The portal fixtures are real projects, their contents under src/.
- ProjectContext gets the unit tests BuildContext never had: the layout, the versioned walk,
  the destination overrides.
- context.md records the narrowed exception for a layout context; command.md and action.md
  scaffold with ProjectContext.

No behavior changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@saeedjamshaid saeedjamshaid changed the title refactor: give each concept one home before the 2.0 release refactor: the 2.0 pre-release fixes — one home per concept, one portal gate, one project layout Sep 25, 2026
saeedjamshaid and others added 3 commits September 26, 2026 01:18
…art stopped

Quickstart on main stripped a spec's unallowed features through
ValidationService.stripUnallowedFeatures; #363 removed that call, and decided in review, the
pruning does not come back. So the method goes, with parseErrorResponse and FeaturesToRemove,
and so does what only fed it: validateViaFile no longer decodes the x-unallowed-features
header, UnallowedFeaturesResponse, RemovableFeature and the ValidateApiResponse wrapper go, and
ValidateAction returns a plain ActionResult, whose value nothing read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The batches added multi-line doc blocks that restated the code or narrated its design, against
the one-line-of-why rule. Those go; what stays is a constraint the code cannot show, such as the
server's 25-minute limit behind the 30-minute timeout, or why /plugin/ must stay untracked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rojectContext

PR #372's review asked for Project and BuildContext to be deleted in favour of ProjectContext
alone. Quickstart's Project record still carried a raw project directory beside a
ProjectContext, so the wizard now runs as its two flows, adopting a downloaded project or
starting one, which finish the same way. The checks that a destination is not the source move
into ProjectContext as isSourceDirectory() and isSourceWithin(), so sourceDirectory() serves
only prompts and services, and context.md's exception says that.

Also from the review: the version problems reach one prompt keyed by problem, which drops the
non-Error throw SonarCloud flagged, along with its two smaller findings; and validateViaFile
opens its stream inside the try again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@saeedjamshaid
saeedjamshaid merged commit dcc486a into dev Sep 25, 2026
10 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 2.0.0-beta.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant