-
Notifications
You must be signed in to change notification settings - Fork 0
feat(identity): derive package name/version into project identity (stages 1-2) #117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| 'agent-bundle': patch | ||
| --- | ||
|
|
||
| Derive validated `packageName`/`packageVersion` from the project's `package.json` into the project identity (issue #94 stages 1-2). Both axes now flow through the normalized model metadata, `ProjectContext`, artifact manifests, inspect output, and dev status DTOs; projects without a package version keep a clearly labeled development fallback in displays. New warning diagnostics: AB4008 (`plugin.version` differs from the package version), AB4009 (invalid npm package name), AB4010 (invalid package semver), AB4011 (unparsable package.json). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import { realpathSync } from 'node:fs'; | ||
| import { isAbsolute, relative, resolve } from 'node:path'; | ||
| import { readFileSync, realpathSync } from 'node:fs'; | ||
| import { isAbsolute, join, relative, resolve } from 'node:path'; | ||
|
|
||
| import { digest } from './digest.ts'; | ||
| import { deepFreeze } from './freeze.ts'; | ||
|
|
@@ -25,10 +25,106 @@ export interface ProjectContext { | |
| readonly configDigest: string; | ||
| readonly configPath: string; | ||
| readonly modelDigest: string; | ||
| /** The validated npm package name axis; absent for unpackaged development projects. */ | ||
| readonly packageName?: string; | ||
| /** The validated semantic release-version axis; absent for unpackaged development projects. */ | ||
| readonly packageVersion?: string; | ||
| readonly revision: string; | ||
| readonly sourceInputs: readonly ProjectSourceInput[]; | ||
| } | ||
|
|
||
| export type PackageIdentityIssueKind = 'invalid-name' | 'invalid-version' | 'unparsable'; | ||
|
|
||
| /** One problem found while deriving package identity from `package.json`. */ | ||
| export interface PackageIdentityIssue { | ||
| readonly kind: PackageIdentityIssueKind; | ||
| readonly message: string; | ||
| } | ||
|
|
||
| /** The release-identity axes derived from a project's `package.json`. */ | ||
| export interface PackageIdentitySnapshot { | ||
| readonly issues: readonly PackageIdentityIssue[]; | ||
| readonly packageName?: string; | ||
| readonly packageVersion?: string; | ||
| } | ||
|
|
||
| /** npm's naming rules for new packages: lowercase, URL-safe, optional scope. */ | ||
| const packageNamePattern = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/u; | ||
|
|
||
| /** The strict semver 2.0.0 grammar, without any leading `v`. */ | ||
| const packageVersionPattern = | ||
| /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(?:\+[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*)?$/u; | ||
|
|
||
| /** True for a name npm would accept for a new package. */ | ||
| export const isValidPackageName = (value: string): boolean => | ||
| value.length > 0 && value.length <= 214 && packageNamePattern.test(value); | ||
|
|
||
| /** True for a strict semver 2.0.0 version. */ | ||
| export const isValidPackageVersion = (value: string): boolean => packageVersionPattern.test(value); | ||
|
|
||
| /** | ||
| * Derives the release-identity axes from `<root>/package.json`. A missing | ||
| * package.json (or missing name/version fields) is a normal development | ||
| * state: no identity and no issues. An invalid name or version becomes an | ||
| * issue for the caller to surface as a diagnostic, never a crash, and the | ||
| * invalid value is withheld from the derived identity. | ||
| */ | ||
| export const snapshotPackageIdentity = (root: string): PackageIdentitySnapshot => { | ||
| let bytes: string; | ||
| try { | ||
| bytes = readFileSync(join(resolve(root), 'package.json'), 'utf8'); | ||
|
Comment on lines
+74
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Resolve and contain-check Useful? React with 👍 / 👎. |
||
| } catch { | ||
| return deepFreeze({ issues: [] }); | ||
| } | ||
| let parsed: unknown; | ||
| try { | ||
| parsed = JSON.parse(bytes); | ||
| } catch { | ||
| return deepFreeze({ issues: [{ kind: 'unparsable', message: 'package.json is not valid JSON.' }] }); | ||
| } | ||
| if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { | ||
| return deepFreeze({ issues: [{ kind: 'unparsable', message: 'package.json must contain a JSON object.' }] }); | ||
| } | ||
| const record = parsed as Readonly<Record<string, unknown>>; | ||
| const issues: PackageIdentityIssue[] = []; | ||
| let packageName: string | undefined; | ||
| if (record.name !== undefined) { | ||
| if (typeof record.name === 'string' && isValidPackageName(record.name)) packageName = record.name; | ||
| else { | ||
| issues.push({ | ||
| kind: 'invalid-name', | ||
| message: `package.json name ${JSON.stringify(record.name)} is not a valid npm package name.`, | ||
| }); | ||
| } | ||
| } | ||
| let packageVersion: string | undefined; | ||
| if (record.version !== undefined) { | ||
| if (typeof record.version === 'string' && isValidPackageVersion(record.version)) packageVersion = record.version; | ||
| else { | ||
| issues.push({ | ||
| kind: 'invalid-version', | ||
| message: `package.json version ${JSON.stringify(record.version)} is not a valid semantic version.`, | ||
| }); | ||
| } | ||
| } | ||
| return deepFreeze({ | ||
| issues, | ||
| ...(packageName === undefined ? {} : { packageName }), | ||
| ...(packageVersion === undefined ? {} : { packageVersion }), | ||
| }); | ||
| }; | ||
|
|
||
| /** | ||
| * The human display label for the release-version axis. Without a package | ||
| * version there is no release identity, so the label is a clearly marked | ||
| * development fallback over the source revision — never a semantic version. | ||
| */ | ||
| export const projectVersionLabel = ( | ||
| context: Pick<ProjectContext, 'packageVersion' | 'revision'>, | ||
| ): string => | ||
| context.packageVersion ?? | ||
| `dev.${context.revision.slice(0, 12)} (development fallback — no package.json version)`; | ||
|
|
||
| export interface CreateProjectContextOptions { | ||
| readonly configPath: string; | ||
| readonly model: NormalizedPlugin; | ||
|
|
@@ -293,10 +389,13 @@ export const createProjectContext = (options: CreateProjectContextOptions): Proj | |
| throw new TypeError(`Configuration source ${JSON.stringify(configPath)} must have a SHA-256 digest.`); | ||
| } | ||
| assertModelPathsResolveInsideProject(canonicalRoot, options.model); | ||
| const { packageName, packageVersion } = options.model.metadata; | ||
| return deepFreeze({ | ||
| configDigest: configInput.sha256, | ||
| configPath, | ||
| modelDigest: digest(canonicalizeNormalizedModel(canonicalRoot, options.model)), | ||
| ...(packageName === undefined ? {} : { packageName }), | ||
| ...(packageVersion === undefined ? {} : { packageVersion }), | ||
| revision: digest({ inputs: sourceInputs }), | ||
| sourceInputs, | ||
| }); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reject reserved names such as
node_modulesandfavicon.ico, which this regex currently accepts even though npm's package-name validator marks them invalid. With either value inpackage.json,snapshotPackageIdentityemits it as a validatedpackageName, AB4009 is skipped, and artifact-manifest validation also accepts the invalid release identity.Useful? React with 👍 / 👎.