Repository navigation
refactor: one place that knows a project's layout - #372
sohail2721 wants to merge 4 commits into
Conversation
…tself
`src` was spelled four ways in ten places, and the directories `sdk generate`,
`portal generate` and `plugin generate` write to were named as string literals
in six commands and again, independently, in the `.gitignore` entries. Nothing
made either set agree.
ProjectContext now answers for both, and GENERATED is derived from the same
names the accessors use, so a rename cannot reach one and miss the other.
The four `src` spellings resolved identically -- `new DirectoryPath(input, 'src')`
and `createInput(input).join('src')` both resolve the same path -- so this
changes no behaviour.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eight prompt sites re-derived `spec/`, `content/` and `static/` from string literals purely to print them, duplicating the private getters inside PortalSourceContext. A prompt spelling one differently would send the reader somewhere that does not exist. The names are exported and both sides use them. The getters stay private: the prompts need the name, not the path the context derives from it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The context skill forbids public path getters so a caller cannot reassemble a context's internals. ProjectContext's whole domain is the layout, so the derivation is the thing being asked for, and withholding it sends the caller back to the literal the context exists to replace. The exception is written down, narrowly: a derived directory, never the constructor parameter. The plan records that step 3 was withdrawn. BuildContext is constructed over versioned subdirectories discovered inside a build, which no project layout can derive, so the accessor it wanted could not have replaced those callers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
e99330b to
715f07b
Compare
mrafnadeem-apimatic
left a comment
There was a problem hiding this comment.
The PR doesn't solve the original problem I mentioned: Project and BuildContext need to be deleted and we need to use only ProjectContext .
| ### Encapsulation — DON'T | ||
|
|
||
| - **DON'T** expose internal paths as public getters — no `public get outputDirectory()` or `public get filePath()`. If a caller needs a path, it should come as a return value from an operation. | ||
| - **Exception, and only this one:** a context whose domain *is* the layout may answer with a directory it derives, as `ProjectContext.sourceDirectory()` does. The rule exists so a caller cannot reassemble a context's internals; here the derivation is the thing being asked for, and withholding it only sends the caller back to the string literal the context exists to replace. It still must be a derivation, never the constructor parameter itself — `ProjectContext` answers `sourceDirectory()` and never `projectDirectory()`. |
There was a problem hiding this comment.
this would not be needed if we just used a single context
There was a problem hiding this comment.
Narrowed, but not removed. BuildContext is gone and actions take a ProjectContext, so nothing derives a path from sourceDirectory() any more — its eleven remaining callers all hand it to a prompt that names the directory to the reader, or to PortalArtifactsService, which uploads it. Removing it entirely means prompts take contexts instead of paths, which puts file system objects in the layer whose only job is text. Tell me if you want that and I will do it as its own change.
…caller builds
Review asked for one context, and it was right that the last round did not
deliver it. What made the accessors look impossible was that every action took
a source directory; that was the thing to change, not a constraint to plan
around. Actions take a ProjectContext now, and no caller joins 'src' onto
anything.
BuildContext looked unmergeable because sdk generate builds one over a
*versioned* subdirectory, which no project layout derives. A project narrows to
a version instead: the same project reading that directory and writing where it
already writes. One private constructor covers both, and the versioned branch
of sdk generate reads better for it -- versionToBuild answers the project to
build from rather than a { version, buildContext } pair.
The portal fixtures become real projects, with their source under src/, rather
than a test-only way to name a source directory outright.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
You were right, and the last round did not deliver it. Done in
What I got wrong. I argued the accessors could not have a caller because every action is handed a source directory. That was true, and it was the thing to change rather than a constraint to plan around; I treated the signatures as fixed when they were the work. It is about 20 files and no behaviour change. The versioned build, which is what I claimed made it impossible: private constructor(projectDirectory, source, version?)
private reading(v) { return new ProjectContext(this.projectDirectory, v, v.leafName()); }
On your comment about the skill exception — it is narrower now, and I have said so in the file, but it is not gone. What I did not delete, and why. One thing worth a look: the portal fixtures are now real projects, with their contents under
|
|
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>
…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>
|
handled in #374 |



Follows
.ai/plans/project-context.md, which is in this PR and records both rounds of it — including the step I withdrew and then had to do.One context
ProjectContextwas named for a project and knew one thing about it: the.gitignore.BuildContextknew the source directory as a build input. Everything else about the layout was re-derived by whoever needed it.BuildContextis deleted. Commands and actions now take aProjectContextand nothing else — no caller joinssrconto anything, and no action constructs a context of its own.The versioned build, which is what made this look impossible
sdk generatebuilds over a versioned subdirectory —src/versioned_docs/v2— which no project layout derives. That is a real constraint and I drew the wrong conclusion from it in the first round.A project narrows to a version instead: the same project reading that directory and writing where it already writes.
ProjectContext.at()never produces the versioned case; only narrowing does, so nothing outside can construct the odd case by accident. The versioned branch ofsdk generatereads better for it:versionToBuildanswers the project to build from, instead of the old{ version, buildContext }pair threaded through a closure that could also return anActionResult.What was duplicated, and is not now
srcwas spelled four ways in ten places, acrosscommands/andactions/. One factory now.GENERATED(the.gitignoreentries) and the same three literals in five commands.GENERATEDis derived from the same names the accessors use, so a rename cannot reach one and miss the other.spec/,content/andstatic/to print them. The names are exported and both sides use them; the getters stay private.No behaviour changes. The four
srcspellings all resolved identically —joinisnew DirectoryPath(path.join(resolved, sub))over a constructor that already callspath.resolve— so the ternary insdk generate/sdk publishwas dead weight, not a difference.What is still exposed, and why
sourceDirectory()has eleven callers left, all inside actions. Every one hands the path to a prompt that names the directory to the reader, or toPortalArtifactsService, which uploads it. Nothing derives anything from it. Removing it entirely means prompts take contexts instead of paths, which puts file system objects in the layer whose only job is text — happy to do that, as its own change..ai/skills/context.mdcarries the narrowed exception: a context whose domain is the layout may answer with a directory it derives, and only for a caller handing it to a prompt or a service — never with its own constructor parameter.ProjectContextanswerssourceDirectory()and neverprojectDirectory().What is not folded in
PortalSourceContextis 630 lines of portal resolution — specs, content tree, navigation, static files — againstProjectContext's 200. Inlining it makes one class that both knows a layout and validates a docs site, and the skill's own rule about every method touching domain state stops holding. It is handed back byportalSource()instead, which is the composite pattern the skill blesses. Same forPluginConfigContextandApimaticConfigContext, views of different blocks of one file. Say so in review if you want those folded in too.Known, not fixed:
plugin generate --destination ./outwrites where.gitignoredoes not name, soplugin publish'sgit initlands in a tracked directory.ProjectContextowning both halves is what made it visible.Reviewing
The portal fixtures are now real projects, with their contents under
src/, rather than a test-only way to name a source directory outright — that is whytest/resources/portal-inputs/*moved.tscclean onsrcandtest, eslint clean, 1225 passing / 0 failing, all 10 checks green.Exercised against dev after pushing:
sdk generate --language typescript --input <dir><dir>/sdk/typescriptsdk generate --language pythonfrom inside the project./sdk/pythonsdk generate --language csharp --destination ./elsewhere./elsewhere/csharp--api-version v2of twosrc/versioned_docs/v2→./sdk/v2/typescript./sdk/v9/typescript--api-versionon an unversioned build./sdk/typescript🤖 Generated with Claude Code