Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/contributing/packages-and-manifests.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Use `package.json` as the canonical source of truth for saved package metadata.
- `kody.id` — user-scoped Kody package id
- `kody.description` — package description for search/detail
- `kody.tags` — search tags
- `kody.dependencies` — direct static saved package dependencies imported via
`kody:@...`
- `kody.app` — optional hosted package app config
- `kody.jobs` — optional package-owned schedules
- `kody.retrievers` — optional package-owned search/context retrievers
Expand Down Expand Up @@ -68,6 +70,15 @@ The top-level saved identity is the package.

- Cross-package imports use the full package name, for example
`kody:@scope/my-package/export-name`.
- Static `kody:@...` imports are bundled snapshots. During checks and
publish-time artifact rebuilds, Kody records the imported saved package's
published commit in bundle dependency metadata. A later publish of that
imported package does not rewrite already-published dependent bundles.
- Direct static `kody:@...` imports are a breaking manifest contract: they must
be listed in `package.json#kody.dependencies` by package name, for example
`"dependencies": ["@scope/my-package"]` inside the `kody` object. Repo checks
fail when a static import is missing from the list or when the list contains a
package that is not statically imported. Type-only imports do not count.
- Callable exports are resolved from package exports, not from a second Kody
registry.
- Packages may also export non-callable helper modules and values for reuse.
Expand Down Expand Up @@ -218,6 +229,19 @@ checks and do not mutate D1, KV snapshots, published bundle artifacts, package
projections, or vectors. Non-fast-forward external heads are refused unless the
caller passes `allow_force: true`.

When publish succeeds, `package_publish_external_push` decorates the response
with `static_dependents`, a bounded summary of direct saved packages whose
published bundle artifact dependency metadata references the package that was
just published. `already_published` responses include the same summary when the
published commit is available. The stale count compares each dependent
artifact's captured dependency commit to the current published commit.

This summary is visibility only. Do not add automatic fanout republishing to the
publish path. Agents should inspect and republish dependent packages only when
the static snapshot semantics matter for the change. Dynamic runtime invocation
through package execution, where available, resolves the current published
target at invocation time and should not force a dependent package republish.

The scheduled reconcile job in
`packages/worker/src/jobs/reconcile-artifacts-pushes.ts` is a safety net for
pushed-but-unpublished commits. Every five minutes it scans a small batch of
Expand Down
24 changes: 24 additions & 0 deletions docs/use/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Important fields:
- `kody.id` — user-scoped Kody package id
- `kody.description` — package description for search/detail
- `kody.tags` — package tags
- `kody.dependencies` — direct saved package names imported through static
`kody:@...` imports
- `kody.app` — optional hosted package app config
- `kody.services` — optional package-owned service runtimes
- `kody.jobs` — optional package-owned schedules
Expand Down Expand Up @@ -72,6 +74,14 @@ exhaustive.

- Cross-package imports use the full package name such as
`kody:@scope/my-package/export-name`.
- Static `kody:@...` imports in saved package code are bundled into published
runtime artifacts as snapshots of the imported package's then-published
bundle. If the imported package is published again later, already-published
dependents keep using the older bundled snapshot until they are republished.
- Every direct static `kody:@...` import must be declared in
`package.json#kody.dependencies` using the imported package name, for example
`"dependencies": ["@scope/my-package"]` inside the `kody` object. Package
checks fail when static imports and declarations differ.
- Exports are normal modules. They may expose a default export, named exports,
or both.
- Direct package invocation calls the resolved module's default export when that
Expand Down Expand Up @@ -221,6 +231,20 @@ edit it with a normal git client without round-tripping each file change through
projections. If the pushed HEAD is already current, the tool returns
`already_published`. If checks fail, it returns `checks_failed` with the
failed check entries and leaves the underlying storage state unchanged.
Successful `published` responses, and `already_published` responses when the
metadata is available, include a bounded `static_dependents` summary of
direct saved packages whose published bundle artifacts statically reference
this package. Stale entries mean the dependent bundle captured an older
dependency commit. Kody does not automatically republish those dependents;
inspect and republish only the ones whose static snapshot should pick up the
new publish.

Dynamic package invocation is different from static bundled imports. When a
runtime feature invokes another package dynamically through the package
execution path, it resolves the current published package at invocation time
instead of embedding a source snapshot in the dependent bundle. Dynamic
invocation should not require republishing a dependent package just because the
called package was republished.

Choose the narrowest token scope that fits the task. Use `read` for inspection
or local diffing, and `write` only when the git client needs to push. Keep TTLs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const mockModule = vi.hoisted(() => ({
getEntitySourceById: vi.fn(),
resolveArtifactSourceHead: vi.fn(),
publishFromExternalRef: vi.fn(),
getStaticPackageDependentsSummary: vi.fn(),
}))

vi.mock('@sentry/cloudflare', () => ({
Expand Down Expand Up @@ -38,6 +39,11 @@ vi.mock('#worker/repo/repo-session-do.ts', () => ({
}),
}))

vi.mock('#worker/package-runtime/static-package-dependents.ts', () => ({
getStaticPackageDependentsSummary: (...args: Array<unknown>) =>
mockModule.getStaticPackageDependentsSummary(...args),
}))

const { publishExternalPushCapability } =
await import('./publish-external-push.ts')

Expand Down Expand Up @@ -66,6 +72,14 @@ beforeEach(() => {
created_at: '2026-05-04T00:00:00.000Z',
updated_at: '2026-05-04T00:00:00.000Z',
})
mockModule.getStaticPackageDependentsSummary.mockResolvedValue({
total: 0,
stale: 0,
truncated: false,
items: [],
recommended_next_action:
'No published bundle artifacts currently declare a static dependency on this package.',
})
})

function createContext() {
Expand Down Expand Up @@ -104,6 +118,14 @@ test('publishes a new external Artifacts HEAD', async () => {
)

expect(result.status).toBe('published')
expect(result).toEqual(
expect.objectContaining({
static_dependents: expect.objectContaining({
total: 0,
items: [],
}),
}),
)
expect(mockModule.publishFromExternalRef).toHaveBeenCalledWith(
expect.objectContaining({
sourceId: 'source-1',
Expand All @@ -129,10 +151,91 @@ test('returns already_published when Artifacts HEAD matches D1', async () => {
expect(result).toEqual({
status: 'already_published',
published_commit: 'commit-old',
static_dependents: {
total: 0,
stale: 0,
truncated: false,
items: [],
recommended_next_action:
'No published bundle artifacts currently declare a static dependency on this package.',
},
})
expect(mockModule.publishFromExternalRef).not.toHaveBeenCalled()
})

test('published output lists stale static dependents for the new dependency commit', async () => {
mockModule.resolveArtifactSourceHead.mockResolvedValue({
branch: 'main',
commit: 'commit-new',
})
mockModule.publishFromExternalRef.mockResolvedValue({
status: 'published',
previous_commit: 'commit-old',
published_commit: 'commit-new',
manifest: {},
checks: [{ kind: 'manifest', ok: true, message: 'ok' }],
})
mockModule.getStaticPackageDependentsSummary.mockResolvedValue({
total: 1,
stale: 1,
truncated: false,
items: [
{
package_id: 'package-b',
kody_id: 'package-b',
name: '@kentcdodds/package-b',
source_id: 'source-b',
published_commit: 'commit-b',
stale: true,
artifact_count: 1,
entrypoints: ['src/index.ts'],
entrypoints_truncated: false,
bundled_dependency_commit: 'commit-a-old',
current_dependency_commit: 'commit-new',
recommended_action:
'Inspect this dependent package and republish it if its bundled static kody:@ snapshot should include the newly published dependency commit.',
},
],
recommended_next_action:
'Inspect stale static dependents and republish only the packages whose bundled snapshot should include this package publish. Kody does not republish dependents automatically.',
})

const result = await publishExternalPushCapability.handler(
{ package_id: 'package-1' },
createContext(),
)

expect(result).toEqual(
expect.objectContaining({
status: 'published',
static_dependents: expect.objectContaining({
total: 1,
stale: 1,
items: [
expect.objectContaining({
package_id: 'package-b',
stale: true,
bundled_dependency_commit: 'commit-a-old',
current_dependency_commit: 'commit-new',
}),
],
}),
}),
)
expect(mockModule.getStaticPackageDependentsSummary).toHaveBeenCalledWith({
db: {},
userId: 'user-1',
sourceId: 'source-1',
currentDependencyCommit: 'commit-new',
})
expect(publishExternalPushCapability.outputTypeDefinition).toContain(
'static_dependents',
)
expect(publishExternalPushCapability.outputTypeDefinition).toContain(
'current_dependency_commit',
)
})

test('surfaces not-fast-forward refusal without force', async () => {
mockModule.resolveArtifactSourceHead.mockResolvedValue({
branch: 'main',
Expand Down Expand Up @@ -289,6 +392,14 @@ test('returns already_published when a retry observes that the reset attempt com
expect(result).toEqual({
status: 'already_published',
published_commit: 'commit-new',
static_dependents: {
total: 0,
stale: 0,
truncated: false,
items: [],
recommended_next_action:
'No published bundle artifacts currently declare a static dependency on this package.',
},
})
expect(mockModule.publishFromExternalRef).toHaveBeenCalledTimes(1)
} finally {
Expand Down
Loading