@michaelthielemann/kestrel 4.1.2 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -201
- package/dist/boot.d.ts +45 -0
- package/dist/boot.js +274 -0
- package/dist/cast.d.ts +3 -0
- package/dist/cast.js +6 -0
- package/dist/catalogue.d.ts +11 -0
- package/dist/catalogue.js +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +55 -0
- package/dist/context.d.ts +62 -0
- package/dist/context.js +61 -0
- package/dist/dataflow.d.ts +3 -0
- package/dist/dataflow.js +65 -0
- package/dist/defineConfig.d.ts +157 -0
- package/dist/defineConfig.js +41 -0
- package/dist/defineContract.d.ts +12 -0
- package/dist/defineContract.js +16 -0
- package/dist/defineModule.d.ts +75 -0
- package/dist/defineModule.js +9 -0
- package/dist/definePipeline.d.ts +7 -0
- package/dist/definePipeline.js +11 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +48 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +14 -0
- package/dist/load.d.ts +7 -0
- package/dist/load.js +79 -0
- package/dist/logger.d.ts +18 -0
- package/dist/logger.js +36 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +87 -0
- package/dist/result.d.ts +19 -0
- package/dist/result.js +19 -0
- package/dist/runner.d.ts +29 -0
- package/dist/runner.js +133 -0
- package/dist/schema.d.ts +7 -0
- package/dist/schema.js +142 -0
- package/dist/sort.d.ts +2 -0
- package/dist/sort.js +50 -0
- package/dist/testing/runPipeline.d.ts +9 -0
- package/dist/testing/runPipeline.js +22 -0
- package/dist/triggers/cron.d.ts +10 -0
- package/dist/triggers/cron.js +44 -0
- package/dist/triggers/cronMatch.d.ts +10 -0
- package/dist/triggers/cronMatch.js +68 -0
- package/dist/triggers/http.d.ts +58 -0
- package/dist/triggers/http.js +292 -0
- package/package.json +25 -151
- package/NOTICE +0 -12
- package/kestrel.config.ts +0 -28
- package/layers/access/nuxt.config.ts +0 -5
- package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
- package/layers/access/server/middleware/access-guard.ts +0 -32
- package/layers/access/server/types.d.ts +0 -7
- package/layers/admin/app/components/AdminAccount.vue +0 -212
- package/layers/admin/app/components/AdminNav.vue +0 -44
- package/layers/admin/app/components/BlockFields.vue +0 -67
- package/layers/admin/app/components/BlockPreview.vue +0 -413
- package/layers/admin/app/components/BlockTree.vue +0 -433
- package/layers/admin/app/components/BlocksBody.vue +0 -163
- package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
- package/layers/admin/app/components/CollectionEditor.vue +0 -324
- package/layers/admin/app/components/CollectionList.vue +0 -429
- package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
- package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
- package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
- package/layers/admin/app/components/CollectionListPager.vue +0 -70
- package/layers/admin/app/components/CollectionListTable.vue +0 -347
- package/layers/admin/app/components/EditorStatus.vue +0 -211
- package/layers/admin/app/components/EditorUnsupported.vue +0 -28
- package/layers/admin/app/components/FieldsBody.vue +0 -23
- package/layers/admin/app/components/LocaleBar.vue +0 -107
- package/layers/admin/app/components/PageFields.vue +0 -129
- package/layers/admin/app/components/PageFieldsPane.vue +0 -39
- package/layers/admin/app/components/SeoFields.vue +0 -210
- package/layers/admin/app/components/SingletonEditor.vue +0 -93
- package/layers/admin/app/composables/useAuth.ts +0 -68
- package/layers/admin/app/composables/useBlockTree.ts +0 -95
- package/layers/admin/app/composables/useBlocks.ts +0 -7
- package/layers/admin/app/composables/useCachedResource.ts +0 -35
- package/layers/admin/app/composables/useCollectionOps.ts +0 -102
- package/layers/admin/app/composables/useCollections.ts +0 -6
- package/layers/admin/app/composables/useContentLocales.ts +0 -23
- package/layers/admin/app/composables/useEditForm.ts +0 -402
- package/layers/admin/app/composables/useLinkResolver.ts +0 -40
- package/layers/admin/app/composables/useListBatchActions.ts +0 -78
- package/layers/admin/app/composables/useListColumns.ts +0 -53
- package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
- package/layers/admin/app/composables/useListRows.ts +0 -75
- package/layers/admin/app/composables/useListSelection.ts +0 -28
- package/layers/admin/app/composables/useListUrlState.ts +0 -53
- package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
- package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
- package/layers/admin/app/composables/usePublishStatus.ts +0 -107
- package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
- package/layers/admin/app/composables/useTheme.ts +0 -36
- package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
- package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
- package/layers/admin/app/layouts/admin.vue +0 -266
- package/layers/admin/app/middleware/admin-auth.ts +0 -4
- package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
- package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
- package/layers/admin/app/pages/admin/index.vue +0 -118
- package/layers/admin/app/pages/admin/login.vue +0 -68
- package/layers/admin/app/pages/admin/media.vue +0 -28
- package/layers/admin/app/pages/admin/references.vue +0 -146
- package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
- package/layers/admin/app/plugins/reauth.client.ts +0 -20
- package/layers/admin/app/utils/admin-style-guard.ts +0 -20
- package/layers/admin/app/utils/block-tree.ts +0 -214
- package/layers/admin/app/utils/collection-ops.ts +0 -69
- package/layers/admin/app/utils/dead-refs.ts +0 -30
- package/layers/admin/app/utils/edit-form.ts +0 -267
- package/layers/admin/app/utils/editor-expose.ts +0 -36
- package/layers/admin/app/utils/editor-form-context.ts +0 -82
- package/layers/admin/app/utils/editor-registry.ts +0 -20
- package/layers/admin/app/utils/field-keys.ts +0 -11
- package/layers/admin/app/utils/list-cell.ts +0 -46
- package/layers/admin/app/utils/list-columns.ts +0 -142
- package/layers/admin/app/utils/list-query.ts +0 -96
- package/layers/admin/app/utils/nav-active.ts +0 -9
- package/layers/admin/app/utils/populate-blocks.ts +0 -288
- package/layers/admin/app/utils/preview-channel.ts +0 -62
- package/layers/admin/app/utils/preview-viewport.ts +0 -80
- package/layers/admin/app/utils/reauth.ts +0 -67
- package/layers/admin/app/utils/record-title.ts +0 -15
- package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
- package/layers/admin/app/utils/safe-redirect.ts +0 -12
- package/layers/admin/nuxt.config.ts +0 -20
- package/layers/auth/nuxt.config.ts +0 -1
- package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
- package/layers/collections/nuxt.config.ts +0 -1
- package/layers/collections/server/plugins/01.register.ts +0 -29
- package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
- package/layers/core/app/composables/layouts.ts +0 -5
- package/layers/core/app/utils/field-factories.ts +0 -129
- package/layers/core/app/utils/layouts.ts +0 -38
- package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
- package/layers/core/modules/auto-discovery/index.ts +0 -112
- package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
- package/layers/core/modules/auto-discovery/scan.ts +0 -170
- package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
- package/layers/core/modules/component-namespace/index.ts +0 -44
- package/layers/core/modules/component-namespace/shared.ts +0 -24
- package/layers/core/modules/kestrel/app-shell.ts +0 -55
- package/layers/core/modules/kestrel/auto-imports.ts +0 -44
- package/layers/core/modules/kestrel/index.ts +0 -144
- package/layers/core/modules/plugin-order/index.ts +0 -69
- package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
- package/layers/core/nuxt.config.ts +0 -10
- package/layers/core/server/api/[...path].ts +0 -98
- package/layers/core/server/plugins/00.config.ts +0 -12
- package/layers/core/server/plugins/00.migrate.ts +0 -12
- package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
- package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
- package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
- package/layers/core/server/plugins/02.schema-sync.ts +0 -58
- package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
- package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
- package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
- package/layers/core/server/tasks/db/migrate-module.ts +0 -83
- package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
- package/layers/core/server/tasks/db/migrate.ts +0 -50
- package/layers/core/server/utils/server-config.ts +0 -81
- package/layers/fields/nuxt.config.ts +0 -1
- package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
- package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
- package/layers/media/app/components/KestrelImg.vue +0 -94
- package/layers/media/app/components/MediaConflictDialog.vue +0 -35
- package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
- package/layers/media/app/components/MediaGrid.vue +0 -113
- package/layers/media/app/components/MediaImage.vue +0 -70
- package/layers/media/app/components/MediaLibrary.vue +0 -456
- package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
- package/layers/media/app/components/MediaPathBar.vue +0 -163
- package/layers/media/app/components/MediaPicker.vue +0 -19
- package/layers/media/app/components/MediaRenameDialog.vue +0 -32
- package/layers/media/app/components/MediaTable.vue +0 -75
- package/layers/media/app/components/MediaThumb.vue +0 -25
- package/layers/media/app/components/MediaToolbar.vue +0 -90
- package/layers/media/app/components/MediaUploadDialog.vue +0 -87
- package/layers/media/app/components/MediaViewer.vue +0 -137
- package/layers/media/app/components/field/Media.vue +0 -263
- package/layers/media/app/composables/useBlurUp.ts +0 -55
- package/layers/media/app/composables/useMediaClipboard.ts +0 -14
- package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
- package/layers/media/app/composables/useMediaDnd.ts +0 -56
- package/layers/media/app/composables/useMediaLibrary.ts +0 -152
- package/layers/media/app/composables/useMediaOps.ts +0 -50
- package/layers/media/app/composables/useMediaRelocate.ts +0 -39
- package/layers/media/app/composables/useMediaResolver.ts +0 -50
- package/layers/media/app/composables/useMediaUpload.ts +0 -93
- package/layers/media/app/composables/useMediaVariant.ts +0 -34
- package/layers/media/app/plugins/field-media.client.ts +0 -6
- package/layers/media/app/utils/ai-disclosure.ts +0 -19
- package/layers/media/app/utils/dnd.ts +0 -69
- package/layers/media/app/utils/library.ts +0 -81
- package/layers/media/app/utils/ops.ts +0 -60
- package/layers/media/app/utils/picture-model.ts +0 -125
- package/layers/media/app/utils/thumbhash.ts +0 -19
- package/layers/media/app/utils/variant-name.ts +0 -20
- package/layers/media/nuxt.config.ts +0 -7
- package/layers/media/server/middleware/ondemand-variants.ts +0 -38
- package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
- package/layers/media/server/plugins/02.register-media.ts +0 -22
- package/layers/media/server/plugins/04.variant-capture.ts +0 -27
- package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
- package/layers/media/server/tasks/media/backfill.ts +0 -17
- package/layers/public/app/app.vue +0 -5
- package/layers/public/app/components/BlockRenderer.vue +0 -56
- package/layers/public/app/components/KestrelLink.vue +0 -26
- package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
- package/layers/public/app/composables/public-page.ts +0 -24
- package/layers/public/app/error.vue +0 -12
- package/layers/public/app/layouts/default.vue +0 -5
- package/layers/public/app/pages/[...slug].vue +0 -239
- package/layers/public/app/pages/__kestrel/preview.vue +0 -46
- package/layers/public/app/utils/block-edit-context.ts +0 -16
- package/layers/public/app/utils/json-ld.ts +0 -139
- package/layers/public/app/utils/link-href.ts +0 -46
- package/layers/public/app/utils/page-head.ts +0 -84
- package/layers/public/app/utils/page-layout.ts +0 -18
- package/layers/public/app/utils/preview-protocol.ts +0 -117
- package/layers/public/app/utils/public-route.ts +0 -46
- package/layers/public/app/utils/site-head.ts +0 -43
- package/layers/public/modules/deploy-output/index.ts +0 -80
- package/layers/public/modules/prerender-routes/discover.ts +0 -37
- package/layers/public/modules/prerender-routes/index.ts +0 -88
- package/layers/public/modules/prerender-routes/routes.ts +0 -16
- package/layers/public/modules/prune-media/index.ts +0 -48
- package/layers/public/modules/prune-media/prune-media.ts +0 -81
- package/layers/public/nuxt.config.ts +0 -14
- package/layers/public/server/delivery-live/serve.ts +0 -111
- package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
- package/layers/public/server/pipelines/index.ts +0 -23
- package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
- package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
- package/layers/public/server/plugins/02.register-links.ts +0 -16
- package/layers/public/server/plugins/03.redirects.ts +0 -55
- package/layers/public/server/plugins/05.plan-publish.ts +0 -5
- package/layers/public/server/plugins/zz.publish.ts +0 -127
- package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
- package/layers/public/server/routes/llms.txt.get.ts +0 -66
- package/layers/public/server/routes/redirects.json.get.ts +0 -58
- package/layers/public/server/routes/robots.txt.get.ts +0 -11
- package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
- package/layers/public/server/tasks/publish/run.ts +0 -32
- package/layers/public/server/utils/publish/render-live.ts +0 -26
- package/layers/ui/app/assets/scss/_base.scss +0 -16
- package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
- package/layers/ui/app/assets/scss/_mixins.scss +0 -50
- package/layers/ui/app/assets/scss/_reset.scss +0 -65
- package/layers/ui/app/assets/scss/_tokens.scss +0 -183
- package/layers/ui/app/assets/scss/main.scss +0 -4
- package/layers/ui/app/components/field/Boolean.vue +0 -30
- package/layers/ui/app/components/field/Choice.vue +0 -73
- package/layers/ui/app/components/field/Datetime.vue +0 -98
- package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
- package/layers/ui/app/components/field/Json.vue +0 -62
- package/layers/ui/app/components/field/Layout.vue +0 -138
- package/layers/ui/app/components/field/Link.vue +0 -112
- package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
- package/layers/ui/app/components/field/Number.vue +0 -30
- package/layers/ui/app/components/field/Relation.vue +0 -49
- package/layers/ui/app/components/field/Renderer.vue +0 -30
- package/layers/ui/app/components/field/Repeater.vue +0 -460
- package/layers/ui/app/components/field/Richtext.vue +0 -26
- package/layers/ui/app/components/field/Slug.vue +0 -65
- package/layers/ui/app/components/field/Text.vue +0 -35
- package/layers/ui/app/components/field/Unsupported.vue +0 -34
- package/layers/ui/app/components/ui/Alert.vue +0 -50
- package/layers/ui/app/components/ui/Brand.vue +0 -22
- package/layers/ui/app/components/ui/Button.vue +0 -116
- package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
- package/layers/ui/app/components/ui/Checkbox.vue +0 -53
- package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
- package/layers/ui/app/components/ui/Combobox.vue +0 -345
- package/layers/ui/app/components/ui/DatePicker.vue +0 -92
- package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
- package/layers/ui/app/components/ui/Dialog.vue +0 -107
- package/layers/ui/app/components/ui/EmptyState.vue +0 -46
- package/layers/ui/app/components/ui/Field.vue +0 -58
- package/layers/ui/app/components/ui/Fieldset.vue +0 -65
- package/layers/ui/app/components/ui/Icon.vue +0 -44
- package/layers/ui/app/components/ui/Menu.vue +0 -54
- package/layers/ui/app/components/ui/NumberInput.vue +0 -175
- package/layers/ui/app/components/ui/Richtext.vue +0 -141
- package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
- package/layers/ui/app/components/ui/Select.vue +0 -52
- package/layers/ui/app/components/ui/TextInput.vue +0 -111
- package/layers/ui/app/components/ui/Textarea.vue +0 -39
- package/layers/ui/app/components/ui/TimeInput.vue +0 -18
- package/layers/ui/app/components/ui/Toasts.vue +0 -103
- package/layers/ui/app/components/ui/Tooltip.vue +0 -45
- package/layers/ui/app/composables/useAdminLang.ts +0 -8
- package/layers/ui/app/composables/useDragReorder.ts +0 -41
- package/layers/ui/app/composables/useEchoGuard.ts +0 -28
- package/layers/ui/app/composables/useLinkField.ts +0 -70
- package/layers/ui/app/composables/useRecordOptions.ts +0 -84
- package/layers/ui/app/composables/useRepeater.ts +0 -99
- package/layers/ui/app/composables/useT.ts +0 -32
- package/layers/ui/app/composables/useToast.ts +0 -46
- package/layers/ui/app/i18n/de.ts +0 -432
- package/layers/ui/app/i18n/en.ts +0 -434
- package/layers/ui/app/utils/boolean-field.ts +0 -10
- package/layers/ui/app/utils/field-component.ts +0 -22
- package/layers/ui/app/utils/field-empty.ts +0 -21
- package/layers/ui/app/utils/field-registry.ts +0 -36
- package/layers/ui/app/utils/humanize.ts +0 -14
- package/layers/ui/app/utils/icons.ts +0 -143
- package/layers/ui/app/utils/iso-date.ts +0 -18
- package/layers/ui/app/utils/localized.ts +0 -13
- package/layers/ui/app/utils/record-context.ts +0 -15
- package/layers/ui/app/utils/reorder.ts +0 -8
- package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
- package/layers/ui/app/utils/useFieldA11y.ts +0 -40
- package/layers/ui/nuxt.config.ts +0 -9
- package/nuxt.config.ts +0 -108
- package/scripts/consumer-template-ci.mjs +0 -160
- package/scripts/copy-create-payload.mjs +0 -49
- package/scripts/coverage-all.mjs +0 -163
- package/scripts/dashboard.mjs +0 -89
- package/scripts/hash-password.mjs +0 -29
- package/scripts/kestrel.mjs +0 -240
- package/scripts/lib/cli.mjs +0 -129
- package/scripts/lib/coverage-model.mjs +0 -51
- package/scripts/lib/password.mjs +0 -19
- package/scripts/lib/scaffold.mjs +0 -205
- package/scripts/port-graph.mjs +0 -350
- package/scripts/typecheck.mjs +0 -62
- package/templates/starter/README.md +0 -65
- package/templates/starter/_env.example +0 -17
- package/templates/starter/_gitignore +0 -26
- package/templates/starter/_package.json +0 -22
- package/templates/starter/app/app.vue +0 -7
- package/templates/starter/app/blocks/Prose.vue +0 -13
- package/templates/starter/app/layouts/default.vue +0 -6
- package/templates/starter/nuxt.config.ts +0 -23
- package/templates/starter/pnpm-workspace.yaml +0 -8
- package/templates/starter/tsconfig.json +0 -3
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// Indexed view over an Istanbul-shaped coverage report, for the audit detectors.
|
|
2
|
-
//
|
|
3
|
-
// Two questions only: which branch arms were never taken, and which functions never ran. Both are
|
|
4
|
-
// keyed by line so they can be joined to the graph, whose nodes carry a start line and no span.
|
|
5
|
-
import { readFileSync, statSync } from 'node:fs'
|
|
6
|
-
import { join, relative, isAbsolute } from 'node:path'
|
|
7
|
-
|
|
8
|
-
export function loadCoverage(root, file = 'reports/coverage/coverage-final.json') {
|
|
9
|
-
const path = isAbsolute(file) ? file : join(root, file)
|
|
10
|
-
let raw
|
|
11
|
-
let generatedAt
|
|
12
|
-
try {
|
|
13
|
-
raw = JSON.parse(readFileSync(path, 'utf8'))
|
|
14
|
-
generatedAt = statSync(path).mtimeMs
|
|
15
|
-
} catch {
|
|
16
|
-
return null
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const byFile = new Map()
|
|
20
|
-
for (const entry of Object.values(raw)) {
|
|
21
|
-
const rel = relative(root, entry.path)
|
|
22
|
-
const missedBranchLines = new Set()
|
|
23
|
-
let branchMissCount = 0
|
|
24
|
-
for (const [key, arm] of Object.entries(entry.branchMap ?? {})) {
|
|
25
|
-
const counts = entry.b?.[key] ?? []
|
|
26
|
-
arm.locations?.forEach((location, index) => {
|
|
27
|
-
if (counts[index] !== 0) return
|
|
28
|
-
branchMissCount += 1
|
|
29
|
-
const line = location.start?.line ?? arm.line
|
|
30
|
-
if (line) missedBranchLines.add(line)
|
|
31
|
-
})
|
|
32
|
-
}
|
|
33
|
-
const ranFunctionLines = new Set()
|
|
34
|
-
for (const [key, fn] of Object.entries(entry.fnMap ?? {})) {
|
|
35
|
-
if (!entry.f?.[key]) continue
|
|
36
|
-
const line = fn.decl?.start?.line ?? fn.loc?.start?.line
|
|
37
|
-
if (line) ranFunctionLines.add(line)
|
|
38
|
-
}
|
|
39
|
-
byFile.set(rel, { missedBranchLines, branchMissCount, ranFunctionLines })
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const empty = { missedBranchLines: new Set(), branchMissCount: 0, ranFunctionLines: new Set() }
|
|
43
|
-
return {
|
|
44
|
-
generatedAt,
|
|
45
|
-
files: [...byFile.keys()],
|
|
46
|
-
has: (file) => byFile.has(file),
|
|
47
|
-
missedBranchLines: (file) => (byFile.get(file) ?? empty).missedBranchLines,
|
|
48
|
-
branchMissCount: (file) => (byFile.get(file) ?? empty).branchMissCount,
|
|
49
|
-
ranFunctionLines: (file) => (byFile.get(file) ?? empty).ranFunctionLines,
|
|
50
|
-
}
|
|
51
|
-
}
|
package/scripts/lib/password.mjs
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { scryptSync, randomBytes } from 'node:crypto'
|
|
2
|
-
|
|
3
|
-
// Must match what layers/auth parses back out. ADR-0001 covers the parameter choice.
|
|
4
|
-
const N = 2 ** 17
|
|
5
|
-
const r = 8
|
|
6
|
-
const p = 1
|
|
7
|
-
const KEYLEN = 64
|
|
8
|
-
const MAXMEM = 256 * 1024 * 1024
|
|
9
|
-
|
|
10
|
-
/** `scrypt$N$r$p$salt$hash`, the `KESTREL_ADMIN_PASSWORD_HASH` format. */
|
|
11
|
-
export function hashPassword(password) {
|
|
12
|
-
const salt = randomBytes(16)
|
|
13
|
-
const hash = scryptSync(password, salt, KEYLEN, { N, r, p, maxmem: MAXMEM })
|
|
14
|
-
return `scrypt$${N}$${r}$${p}$${salt.toString('base64url')}$${hash.toString('base64url')}`
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function sessionSecret() {
|
|
18
|
-
return randomBytes(32).toString('base64url')
|
|
19
|
-
}
|
package/scripts/lib/scaffold.mjs
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
export const PACKAGE_NAME = '@michaelthielemann/kestrel'
|
|
2
|
-
|
|
3
|
-
// Template dotfiles are `_`-prefixed because npm strips a literal `.gitignore` from a tarball. ADR-0005.
|
|
4
|
-
const RENAMES = {
|
|
5
|
-
_gitignore: '.gitignore',
|
|
6
|
-
'_env.example': '.env.example',
|
|
7
|
-
'_package.json': 'package.json',
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function targetName(rel) {
|
|
11
|
-
const parts = rel.split('/')
|
|
12
|
-
const base = parts[parts.length - 1]
|
|
13
|
-
parts[parts.length - 1] = RENAMES[base] ?? base
|
|
14
|
-
return parts.join('/')
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** An unknown placeholder is left as-is so a typo is visible rather than silently blank. */
|
|
18
|
-
export function renderTemplate(src, vars) {
|
|
19
|
-
return src.replace(/\{\{(\w+)\}\}/g, (whole, key) => (key in vars ? String(vars[key]) : whole))
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function toPackageName(dirName) {
|
|
23
|
-
const slug = dirName
|
|
24
|
-
.toLowerCase()
|
|
25
|
-
.replace(/[^a-z0-9._-]+/g, '-')
|
|
26
|
-
.replace(/^[-._]+|[-._]+$/g, '')
|
|
27
|
-
return slug || 'kestrel-site'
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const KEY_RE = /^\s*([A-Z][A-Z0-9_]*)\s*=/
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Fills only keys that are absent or empty, so re-running `init` never rotates a live session secret.
|
|
34
|
-
* A key in `force` replaces a value that is already there — for a secret the caller supplied by hand,
|
|
35
|
-
* where being handed one at all is the instruction to change it.
|
|
36
|
-
* Returns the new text plus the keys that changed.
|
|
37
|
-
*/
|
|
38
|
-
export function mergeEnv(existing, entries, force = []) {
|
|
39
|
-
const forced = new Set(force)
|
|
40
|
-
const pending = new Map(Object.entries(entries))
|
|
41
|
-
const lines = existing === '' ? [] : existing.split('\n')
|
|
42
|
-
const written = []
|
|
43
|
-
|
|
44
|
-
// A dotenv loader takes the LAST assignment of a key, so filling the first would leave the file
|
|
45
|
-
// reporting a value the app never sees.
|
|
46
|
-
const lastIndex = new Map()
|
|
47
|
-
lines.forEach((line, i) => {
|
|
48
|
-
const key = KEY_RE.exec(line)?.[1]
|
|
49
|
-
if (key) lastIndex.set(key, i)
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
const out = lines.map((line, i) => {
|
|
53
|
-
const key = KEY_RE.exec(line)?.[1]
|
|
54
|
-
if (!key || !pending.has(key) || lastIndex.get(key) !== i) return line
|
|
55
|
-
const value = pending.get(key)
|
|
56
|
-
pending.delete(key)
|
|
57
|
-
if (line.slice(line.indexOf('=') + 1).trim() !== '' && !forced.has(key)) return line
|
|
58
|
-
written.push(key)
|
|
59
|
-
return `${key}=${value}`
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
if (pending.size) {
|
|
63
|
-
if (out.length && out[out.length - 1].trim() !== '') out.push('')
|
|
64
|
-
for (const [key, value] of pending) {
|
|
65
|
-
out.push(`${key}=${value}`)
|
|
66
|
-
written.push(key)
|
|
67
|
-
}
|
|
68
|
-
out.push('')
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return { text: out.join('\n'), written }
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** `typeof null === 'object'`, and arrays are objects too — both need explicit exclusion for a plain object check. */
|
|
75
|
-
export const isPlainObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
76
|
-
|
|
77
|
-
const NESTED = ['scripts', 'dependencies', 'devDependencies']
|
|
78
|
-
// `type` decides whether every .js in the project is ESM or CJS, so injecting it silently could break a
|
|
79
|
-
// CommonJS project outright. Only these top-level keys may be introduced, and each is reported.
|
|
80
|
-
const TOP_LEVEL = ['name', 'private', 'type']
|
|
81
|
-
|
|
82
|
-
/** Additive: the project's own values always win. Returns the manifest plus every key it introduced. */
|
|
83
|
-
export function mergePackageJson(existing, template) {
|
|
84
|
-
const merged = { ...existing }
|
|
85
|
-
const added = []
|
|
86
|
-
|
|
87
|
-
for (const key of TOP_LEVEL) {
|
|
88
|
-
if (!(key in template) || key in existing) continue
|
|
89
|
-
merged[key] = template[key]
|
|
90
|
-
added.push(key)
|
|
91
|
-
}
|
|
92
|
-
for (const key of NESTED) {
|
|
93
|
-
if (!template[key]) continue
|
|
94
|
-
merged[key] = isPlainObject(existing[key]) ? { ...template[key], ...existing[key] } : template[key]
|
|
95
|
-
for (const name of Object.keys(template[key])) {
|
|
96
|
-
if (!isPlainObject(existing[key]) || !(name in existing[key])) added.push(`${key}.${name}`)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return { merged, added }
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* The value a dotenv key carries, or `undefined`/`''` when it carries none.
|
|
104
|
-
* Horizontal whitespace only: `\s*` would let an empty assignment match the NEXT line's value.
|
|
105
|
-
*/
|
|
106
|
-
export const envValue = (env, key) => new RegExp(`^[^\\S\\n]*${key}[^\\S\\n]*=[^\\S\\n]*(.+)$`, 'm').exec(env)?.[1].trim()
|
|
107
|
-
|
|
108
|
-
const withoutComments = (src) => src.replace(/<!--[\s\S]*?-->/g, '')
|
|
109
|
-
// `<nuxt-page />` is as valid as `<NuxtPage />`; matching only the Pascal spelling would flag a working app.
|
|
110
|
-
const kebab = (tag) => tag.replace(/(?!^)([A-Z])/g, '-$1').toLowerCase()
|
|
111
|
-
export const usesComponent = (src, tag) => new RegExp(`<\\s*(${tag}|${kebab(tag)})[\\s/>]`, 'i').test(src)
|
|
112
|
-
|
|
113
|
-
/** Whether `output.publishOnSave` resolves to `true` — config or its env override, same precedence as
|
|
114
|
-
* `resolveKestrel` (env wins when it names a valid boolean; otherwise the config file's own value). */
|
|
115
|
-
function publishOnSaveEnabled(kestrelConfig, env) {
|
|
116
|
-
const fromEnv = env !== null ? envValue(env, 'KESTREL_OUTPUT_PUBLISH_ON_SAVE')?.toLowerCase() : undefined
|
|
117
|
-
if (fromEnv !== undefined) {
|
|
118
|
-
if (['true', '1', 'yes', 'on'].includes(fromEnv)) return true
|
|
119
|
-
if (['false', '0', 'no', 'off'].includes(fromEnv)) return false
|
|
120
|
-
}
|
|
121
|
-
return typeof kestrelConfig === 'string' && /\bpublishOnSave\s*:\s*true\b/.test(kestrelConfig)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* `kestrel doctor`, as a pure function of what was read; `null` means the file is absent.
|
|
126
|
-
* The `app.vue` rules mirror `layers/core/modules/kestrel/app-shell.ts`; a test pins the two together.
|
|
127
|
-
*/
|
|
128
|
-
export function diagnoseProject({ packageJson, nuxtConfig, appVue, env, kestrelConfig }) {
|
|
129
|
-
const found = []
|
|
130
|
-
const add = (level, message) => found.push({ level, message })
|
|
131
|
-
|
|
132
|
-
let manifest
|
|
133
|
-
if (packageJson !== null) {
|
|
134
|
-
try {
|
|
135
|
-
manifest = JSON.parse(packageJson)
|
|
136
|
-
} catch {
|
|
137
|
-
add('error', 'package.json is not valid JSON — nothing can read it, including pnpm.')
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (packageJson === null) {
|
|
142
|
-
add('error', 'no package.json — run this inside a project directory, or `kestrel init <dir>` to make one.')
|
|
143
|
-
} else if (manifest !== undefined) {
|
|
144
|
-
const deps = { ...manifest.dependencies, ...manifest.devDependencies }
|
|
145
|
-
if (!(PACKAGE_NAME in deps)) add('error', `${PACKAGE_NAME} is not a dependency — run \`pnpm add ${PACKAGE_NAME}\`.`)
|
|
146
|
-
if (!('nuxt' in deps)) {
|
|
147
|
-
add(
|
|
148
|
-
'error',
|
|
149
|
-
'nuxt is not a direct dependency. Kestrel depends on it, but a strict node_modules layout (pnpm) does not link a transitive package\'s `nuxt` binary, so `nuxt dev` will not resolve — run `pnpm add -D nuxt`.',
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
if (!manifest.scripts?.dev) add('warn', 'no `dev` script — add `"dev": "nuxt dev"` so `pnpm dev` works.')
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (nuxtConfig === null) {
|
|
156
|
-
add(
|
|
157
|
-
'error',
|
|
158
|
-
'no nuxt.config.ts. Installing the package does nothing on its own: Nuxt only loads Kestrel when the config extends it. Without this file every route, /admin included, serves the default Nuxt welcome page.',
|
|
159
|
-
)
|
|
160
|
-
} else if (!nuxtConfig.includes(PACKAGE_NAME)) {
|
|
161
|
-
add('error', `nuxt.config.ts does not extend ${PACKAGE_NAME} — add \`extends: ['${PACKAGE_NAME}']\`.`)
|
|
162
|
-
} else if (/extends\s*:\s*\[[^\]]*['"]\.\.[/\\]\.\./.test(nuxtConfig)) {
|
|
163
|
-
add(
|
|
164
|
-
'warn',
|
|
165
|
-
"a relative `extends` path of '../..' or deeper silently drops every Kestrel sub-layer (c12 treats it as a file, not a directory, because pathe reports its extension as '.'). Use the package name.",
|
|
166
|
-
)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (appVue !== null) {
|
|
170
|
-
const src = withoutComments(appVue)
|
|
171
|
-
if (!usesComponent(src, 'NuxtPage')) {
|
|
172
|
-
add(
|
|
173
|
-
'error',
|
|
174
|
-
'app/app.vue renders no <NuxtPage />, so no route renders — including /admin. It shadows the one Kestrel ships. Delete it, or wrap <NuxtPage /> in <NuxtLayout>.',
|
|
175
|
-
)
|
|
176
|
-
} else if (!usesComponent(src, 'NuxtLayout')) {
|
|
177
|
-
add('warn', 'app/app.vue renders no <NuxtLayout />, so the admin loses its navigation shell.')
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (env === null) {
|
|
182
|
-
add('error', 'no .env — sign-in at /admin answers 503 until KESTREL_ADMIN_PASSWORD_HASH is set. Run `kestrel init`.')
|
|
183
|
-
} else {
|
|
184
|
-
const value = (key) => envValue(env, key)
|
|
185
|
-
if (!value('KESTREL_ADMIN_PASSWORD_HASH')) {
|
|
186
|
-
add('error', 'KESTREL_ADMIN_PASSWORD_HASH is unset — /admin renders but sign-in answers 503. Run `kestrel hash-password`.')
|
|
187
|
-
}
|
|
188
|
-
if (!value('KESTREL_SESSION_SECRET')) {
|
|
189
|
-
add('warn', 'KESTREL_SESSION_SECRET is unset — dev falls back to a random per-process secret (sessions drop on restart) and production refuses to boot.')
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Informational, not a misconfiguration: `publishOnSave: false` is the correct 2.0 default. Named
|
|
194
|
-
// because it is a silent behaviour change from 1.x — a headless consumer that only PATCHes content and
|
|
195
|
-
// never opens /admin gets no other signal that saves stopped reaching the static output.
|
|
196
|
-
if (!publishOnSaveEnabled(kestrelConfig, env)) {
|
|
197
|
-
add(
|
|
198
|
-
'info',
|
|
199
|
-
'saving a record no longer publishes it by default — only an explicit Publish action does (ADR-0008). ' +
|
|
200
|
-
'Set `output.publishOnSave: true` in kestrel.config.ts, or KESTREL_OUTPUT_PUBLISH_ON_SAVE=1, to restore the pre-2.0 behaviour.',
|
|
201
|
-
)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return found
|
|
205
|
-
}
|
package/scripts/port-graph.mjs
DELETED
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Port-graph extraction (experiment): for every exported function in packages/kestrel-core/src whose
|
|
3
|
-
// return type resolves to `Effect.Effect<A, E, R>`, the R (requirements) channel becomes that function's
|
|
4
|
-
// port list. Every `Layer.succeed`/`Layer.effect`/`Layer.scoped` call becomes an adapter binding from the
|
|
5
|
-
// layer's containing function to the port it provides. Nodes reuse graphify-out/graph.json's id scheme so
|
|
6
|
-
// the two graphs join; a node not already present there is synthesized with the same scheme and flagged.
|
|
7
|
-
//
|
|
8
|
-
// The R channel alone cannot answer "does persist always follow validate" — this codebase passes the
|
|
9
|
-
// database as a plain function parameter, not a Context.Tag requirement, so R is `never` on every step.
|
|
10
|
-
// To make that invariant askable as a graph query at all, this script also extracts each `definePipeline`
|
|
11
|
-
// call's literal `steps: [...]` array (resolving each step-factory call to the runtime step name registered
|
|
12
|
-
// via `syncStep(name, ...)`) into an ordered step chain, and exposes `stepOrderRespectsInvariant` for
|
|
13
|
-
// querying it.
|
|
14
|
-
import ts from 'typescript'
|
|
15
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
16
|
-
import { dirname, join, relative } from 'node:path'
|
|
17
|
-
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
18
|
-
|
|
19
|
-
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
20
|
-
const corePkg = join(root, 'packages/kestrel-core')
|
|
21
|
-
const graphOutDir = join(root, 'graphify-out')
|
|
22
|
-
|
|
23
|
-
// --- graph id scheme (mirrors graphify's own slugging, see graphify-out/graph.json) ---
|
|
24
|
-
|
|
25
|
-
function slugFile(relFile) {
|
|
26
|
-
return relFile.replace(/\.[^/.]+$/, '').replace(/[^A-Za-z0-9]+/g, '_').toLowerCase()
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function slugSymbol(name) {
|
|
30
|
-
return name.replace(/\(\)$/, '').toLowerCase().replace(/[^a-z0-9]/g, '')
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function loadExistingGraph() {
|
|
34
|
-
const graphPath = join(graphOutDir, 'graph.json')
|
|
35
|
-
if (!existsSync(graphPath)) return { nodes: [], byFileAndLabel: new Map() }
|
|
36
|
-
const graph = JSON.parse(readFileSync(graphPath, 'utf8'))
|
|
37
|
-
const byFileAndLabel = new Map()
|
|
38
|
-
for (const n of graph.nodes) byFileAndLabel.set(`${n.source_file}::${n.label}`, n)
|
|
39
|
-
return { nodes: graph.nodes, byFileAndLabel }
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Reuses the existing graph's node id for (relFile, name) when present; otherwise synthesizes one with
|
|
43
|
-
* the same scheme and marks it so a future graphify run can be checked against it. */
|
|
44
|
-
function nodeIdFor(existing, relFile, name) {
|
|
45
|
-
const found = existing.byFileAndLabel.get(`${relFile}::${name}()`)
|
|
46
|
-
if (found) return { id: found.id, synthesized: false }
|
|
47
|
-
return { id: `${slugFile(relFile)}_${slugSymbol(name)}`, synthesized: true }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function loadProgram() {
|
|
51
|
-
const tsconfigPath = join(corePkg, 'tsconfig.json')
|
|
52
|
-
const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile)
|
|
53
|
-
const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, corePkg)
|
|
54
|
-
const fileNames = parsed.fileNames.filter((f) => !/\.test\.ts$/.test(f) && !f.includes(`${corePkg}/test/`))
|
|
55
|
-
const program = ts.createProgram(fileNames, { ...parsed.options, noEmit: true })
|
|
56
|
-
return { program, fileNames }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function hasExportModifier(node) {
|
|
60
|
-
return (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) !== 0
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function isEffectReturnType(type) {
|
|
64
|
-
const target = type.aliasSymbol ?? type.target?.symbol ?? type.symbol
|
|
65
|
-
return target?.name === 'Effect' && !!(type).typeArguments
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Flattens a union of `Context.Tag`-shaped types (or a bare one) into distinct display names, dropping
|
|
69
|
-
* `never` (the honest "no requirements" answer, not an omission). */
|
|
70
|
-
function portNamesFromRType(checker, rType) {
|
|
71
|
-
if (!rType) return []
|
|
72
|
-
const parts = rType.isUnion?.() ? rType.types : [rType]
|
|
73
|
-
const names = new Set()
|
|
74
|
-
for (const t of parts) {
|
|
75
|
-
const text = checker.typeToString(t)
|
|
76
|
-
if (text === 'never' || text === 'unknown') continue
|
|
77
|
-
names.add(text)
|
|
78
|
-
}
|
|
79
|
-
return [...names]
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Visits every exported function-like declaration (function declarations and `export const f = (...) =>`)
|
|
83
|
-
* in a source file, at the top level and one level of nesting (namespaces are not used in this codebase). */
|
|
84
|
-
function* exportedFunctions(sf) {
|
|
85
|
-
for (const stmt of sf.statements) {
|
|
86
|
-
if (ts.isFunctionDeclaration(stmt) && hasExportModifier(stmt) && stmt.name) {
|
|
87
|
-
yield { name: stmt.name.text, decl: stmt }
|
|
88
|
-
} else if (ts.isVariableStatement(stmt) && hasExportModifier(stmt)) {
|
|
89
|
-
for (const d of stmt.declarationList.declarations) {
|
|
90
|
-
if (ts.isIdentifier(d.name) && d.initializer && (ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer))) {
|
|
91
|
-
yield { name: d.name.text, decl: d.initializer }
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function extractFunctionPorts(program, existing) {
|
|
99
|
-
const checker = program.getTypeChecker()
|
|
100
|
-
const functions = []
|
|
101
|
-
let scanned = 0
|
|
102
|
-
for (const sf of program.getSourceFiles()) {
|
|
103
|
-
if (!sf.fileName.startsWith(corePkg) || sf.fileName.includes('/node_modules/')) continue
|
|
104
|
-
const relFile = relative(root, sf.fileName)
|
|
105
|
-
for (const { name, decl } of exportedFunctions(sf)) {
|
|
106
|
-
scanned++
|
|
107
|
-
const sig = checker.getSignatureFromDeclaration(decl)
|
|
108
|
-
if (!sig) continue
|
|
109
|
-
const returnType = checker.getReturnTypeOfSignature(sig)
|
|
110
|
-
if (!isEffectReturnType(returnType)) continue
|
|
111
|
-
const typeArgs = checker.getTypeArguments(returnType)
|
|
112
|
-
const rType = typeArgs[2]
|
|
113
|
-
const ports = portNamesFromRType(checker, rType)
|
|
114
|
-
const { id, synthesized } = nodeIdFor(existing, relFile, name)
|
|
115
|
-
functions.push({ id, name, file: relFile, ports, synthesized })
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return { functions, scanned }
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function containingFunctionName(node) {
|
|
122
|
-
let cur = node.parent
|
|
123
|
-
while (cur) {
|
|
124
|
-
if ((ts.isFunctionDeclaration(cur) || ts.isFunctionExpression(cur) || ts.isArrowFunction(cur)) ) {
|
|
125
|
-
if (ts.isFunctionDeclaration(cur) && cur.name) return cur.name.text
|
|
126
|
-
if (ts.isVariableDeclaration(cur.parent) && ts.isIdentifier(cur.parent.name)) return cur.parent.name.text
|
|
127
|
-
}
|
|
128
|
-
cur = cur.parent
|
|
129
|
-
}
|
|
130
|
-
return null
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Resolves a `Context.GenericTag<X>(...)` (or `Context.Tag<X, ...>`) expression, following one level of
|
|
134
|
-
* identifier indirection, to the service type name `X`. */
|
|
135
|
-
function resolveTagTypeName(checker, expr) {
|
|
136
|
-
let target = expr
|
|
137
|
-
if (ts.isIdentifier(expr)) {
|
|
138
|
-
const sym = checker.getSymbolAtLocation(expr)
|
|
139
|
-
const decl = sym?.valueDeclaration
|
|
140
|
-
if (decl && ts.isVariableDeclaration(decl) && decl.initializer) target = decl.initializer
|
|
141
|
-
}
|
|
142
|
-
if (ts.isCallExpression(target) && target.typeArguments?.length) {
|
|
143
|
-
return target.typeArguments[0].getText()
|
|
144
|
-
}
|
|
145
|
-
return null
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const LAYER_CONSTRUCTORS = new Set(['succeed', 'effect', 'scoped', 'sync'])
|
|
149
|
-
|
|
150
|
-
function extractLayerBindings(program, existing) {
|
|
151
|
-
const checker = program.getTypeChecker()
|
|
152
|
-
const bindings = []
|
|
153
|
-
for (const sf of program.getSourceFiles()) {
|
|
154
|
-
if (!sf.fileName.startsWith(corePkg) || sf.fileName.includes('/node_modules/')) continue
|
|
155
|
-
const relFile = relative(root, sf.fileName)
|
|
156
|
-
const visit = (node) => {
|
|
157
|
-
if (
|
|
158
|
-
ts.isCallExpression(node)
|
|
159
|
-
&& ts.isPropertyAccessExpression(node.expression)
|
|
160
|
-
&& ts.isIdentifier(node.expression.expression)
|
|
161
|
-
&& node.expression.expression.text === 'Layer'
|
|
162
|
-
&& LAYER_CONSTRUCTORS.has(node.expression.name.text)
|
|
163
|
-
&& node.arguments.length > 0
|
|
164
|
-
) {
|
|
165
|
-
const tagName = resolveTagTypeName(checker, node.arguments[0])
|
|
166
|
-
if (tagName) {
|
|
167
|
-
const fnName = containingFunctionName(node) ?? '<module scope>'
|
|
168
|
-
const { id, synthesized } = nodeIdFor(existing, relFile, fnName)
|
|
169
|
-
bindings.push({
|
|
170
|
-
layerFnId: id,
|
|
171
|
-
layerFnName: fnName,
|
|
172
|
-
file: relFile,
|
|
173
|
-
line: sf.getLineAndCharacterOfPosition(node.getStart()).line + 1,
|
|
174
|
-
constructor: node.expression.name.text,
|
|
175
|
-
port: tagName,
|
|
176
|
-
synthesized,
|
|
177
|
-
})
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
ts.forEachChild(node, visit)
|
|
181
|
-
}
|
|
182
|
-
visit(sf)
|
|
183
|
-
}
|
|
184
|
-
return bindings
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// --- Pipeline step-order extraction (makes the validate-before-persist invariant askable as a graph query) ---
|
|
188
|
-
|
|
189
|
-
/** Maps every step-factory function name (e.g. `validateCreateStep`) to the runtime step name it registers
|
|
190
|
-
* via `syncStep('<name>', ...)` / `asyncStep('<name>', ...)`. A factory with more than one such call inside
|
|
191
|
-
* keeps only the first — every factory in this codebase registers exactly one step. */
|
|
192
|
-
function buildStepNameMap(program) {
|
|
193
|
-
const map = new Map()
|
|
194
|
-
for (const sf of program.getSourceFiles()) {
|
|
195
|
-
if (!sf.fileName.includes('/pipeline/steps/') || sf.fileName.includes('/node_modules/')) continue
|
|
196
|
-
for (const { name, decl } of exportedFunctions(sf)) {
|
|
197
|
-
let stepName = null
|
|
198
|
-
const visit = (node) => {
|
|
199
|
-
if (stepName) return
|
|
200
|
-
if (
|
|
201
|
-
ts.isCallExpression(node)
|
|
202
|
-
&& ts.isIdentifier(node.expression)
|
|
203
|
-
&& (node.expression.text === 'syncStep' || node.expression.text === 'asyncStep')
|
|
204
|
-
&& node.arguments[0] && ts.isStringLiteral(node.arguments[0])
|
|
205
|
-
) {
|
|
206
|
-
stepName = node.arguments[0].text
|
|
207
|
-
return
|
|
208
|
-
}
|
|
209
|
-
ts.forEachChild(node, visit)
|
|
210
|
-
}
|
|
211
|
-
visit(decl)
|
|
212
|
-
if (stepName) map.set(name, stepName)
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return map
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function extractPipelineStepChains(program) {
|
|
219
|
-
const stepNameMap = buildStepNameMap(program)
|
|
220
|
-
const pipelines = []
|
|
221
|
-
for (const sf of program.getSourceFiles()) {
|
|
222
|
-
if (!sf.fileName.startsWith(corePkg) || sf.fileName.includes('/node_modules/')) continue
|
|
223
|
-
const relFile = relative(root, sf.fileName)
|
|
224
|
-
const visit = (node) => {
|
|
225
|
-
if (
|
|
226
|
-
ts.isCallExpression(node)
|
|
227
|
-
&& ts.isIdentifier(node.expression)
|
|
228
|
-
&& node.expression.text === 'definePipeline'
|
|
229
|
-
&& node.arguments[0] && ts.isObjectLiteralExpression(node.arguments[0])
|
|
230
|
-
) {
|
|
231
|
-
const obj = node.arguments[0]
|
|
232
|
-
const nameProp = obj.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === 'name')
|
|
233
|
-
const stepsProp = obj.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === 'steps')
|
|
234
|
-
if (nameProp && ts.isStringLiteral(nameProp.initializer) && stepsProp && ts.isArrayLiteralExpression(stepsProp.initializer)) {
|
|
235
|
-
const steps = []
|
|
236
|
-
for (const el of stepsProp.initializer.elements) {
|
|
237
|
-
// `steps: deleteSteps` (an identifier referring to a separately declared array) is skipped —
|
|
238
|
-
// rollback/duplicate/delete are not in VALIDATING_WRITE_OPS, so the invariant query never needs it.
|
|
239
|
-
if (ts.isCallExpression(el) && ts.isIdentifier(el.expression)) {
|
|
240
|
-
const factoryName = el.expression.text
|
|
241
|
-
steps.push(stepNameMap.get(factoryName) ?? factoryName)
|
|
242
|
-
} else if (ts.isCallExpression(el) && ts.isPropertyAccessExpression(el.expression)) {
|
|
243
|
-
steps.push(el.expression.name.text)
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
pipelines.push({ name: nameProp.initializer.text, file: relFile, line: sf.getLineAndCharacterOfPosition(node.getStart()).line + 1, steps })
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
ts.forEachChild(node, visit)
|
|
250
|
-
}
|
|
251
|
-
visit(sf)
|
|
252
|
-
}
|
|
253
|
-
return pipelines
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** The validate-before-persist invariant, as a pure query over an ordered step-name chain: `validate` must occur, `persist`
|
|
257
|
-
* must occur, and `validate` must precede `persist`. Mirrors test/architecture/pipeline-invariants.test.ts'
|
|
258
|
-
* registry-based check, but over the statically extracted `definePipeline` step array instead of a
|
|
259
|
-
* runtime-composed `PipelineDescriptor`. */
|
|
260
|
-
export function stepOrderRespectsInvariant(stepNames) {
|
|
261
|
-
const validateAt = stepNames.indexOf('validate')
|
|
262
|
-
const persistAt = stepNames.indexOf('persist')
|
|
263
|
-
if (validateAt < 0 || persistAt < 0) return false
|
|
264
|
-
return validateAt < persistAt
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
export const VALIDATING_WRITE_OPS = new Set(['createOne', 'createMany', 'updateOne', 'updateMany'])
|
|
268
|
-
|
|
269
|
-
export function extractPortGraph() {
|
|
270
|
-
const { program } = loadProgram()
|
|
271
|
-
const existing = loadExistingGraph()
|
|
272
|
-
const { functions, scanned } = extractFunctionPorts(program, existing)
|
|
273
|
-
const bindings = extractLayerBindings(program, existing)
|
|
274
|
-
const pipelines = extractPipelineStepChains(program)
|
|
275
|
-
return { functions, scanned, bindings, pipelines }
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function buildEmittedGraph({ functions, bindings, pipelines }) {
|
|
279
|
-
const nodes = []
|
|
280
|
-
const links = []
|
|
281
|
-
const seenNodes = new Set()
|
|
282
|
-
const addNode = (node) => {
|
|
283
|
-
if (seenNodes.has(node.id)) return
|
|
284
|
-
seenNodes.add(node.id)
|
|
285
|
-
nodes.push(node)
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
for (const fn of functions) {
|
|
289
|
-
addNode({ id: fn.id, label: `${fn.name}()`, kind: 'function', source_file: fn.file, synthesized: fn.synthesized })
|
|
290
|
-
for (const port of fn.ports) {
|
|
291
|
-
const portId = `port_${slugSymbol(port)}`
|
|
292
|
-
addNode({ id: portId, label: port, kind: 'port' })
|
|
293
|
-
links.push({ relation: 'requires', source: fn.id, target: portId, source_file: fn.file })
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
for (const b of bindings) {
|
|
298
|
-
addNode({ id: b.layerFnId, label: `${b.layerFnName}()`, kind: 'function', source_file: b.file, synthesized: b.synthesized })
|
|
299
|
-
const portId = `port_${slugSymbol(b.port)}`
|
|
300
|
-
addNode({ id: portId, label: b.port, kind: 'port' })
|
|
301
|
-
links.push({ relation: 'provides', source: b.layerFnId, target: portId, source_file: b.file, source_location: `L${b.line}`, layer_constructor: b.constructor })
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
for (const [i, p] of pipelines.entries()) {
|
|
305
|
-
const pipelineId = `pipeline_${slugFile(p.file)}_${slugSymbol(p.name)}_${i}`
|
|
306
|
-
addNode({ id: pipelineId, label: `${p.name} (pipeline)`, kind: 'pipeline', source_file: p.file, source_location: `L${p.line}` })
|
|
307
|
-
p.steps.forEach((stepName, order) => {
|
|
308
|
-
const stepId = `pipeline_step_${slugSymbol(stepName)}`
|
|
309
|
-
addNode({ id: stepId, label: stepName, kind: 'pipeline_step' })
|
|
310
|
-
links.push({ relation: 'pipeline_step', source: pipelineId, target: stepId, order, source_file: p.file })
|
|
311
|
-
})
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return { nodes, links }
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function main() {
|
|
318
|
-
const { functions, scanned, bindings, pipelines } = extractPortGraph()
|
|
319
|
-
const nonEmpty = functions.filter((f) => f.ports.length > 0)
|
|
320
|
-
const totalPorts = new Set(functions.flatMap((f) => f.ports)).size
|
|
321
|
-
|
|
322
|
-
const emitted = buildEmittedGraph({ functions, bindings, pipelines })
|
|
323
|
-
const reportDir = join(root, 'reports/graph')
|
|
324
|
-
mkdirSync(reportDir, { recursive: true })
|
|
325
|
-
const outPath = join(reportDir, 'port-graph.json')
|
|
326
|
-
writeFileSync(outPath, JSON.stringify({ generated_at: new Date().toISOString(), nodes: emitted.nodes, links: emitted.links }, null, 2))
|
|
327
|
-
|
|
328
|
-
console.log(`port-graph: scanned ${scanned} exported functions in packages/kestrel-core/src`)
|
|
329
|
-
console.log(`port-graph: ${functions.length} have an Effect.Effect<A, E, R> return type`)
|
|
330
|
-
console.log(`port-graph: ${nonEmpty.length} have a non-empty (non-never) R channel; ${totalPorts} distinct ports found`)
|
|
331
|
-
console.log(`port-graph: ${bindings.length} Layer composition(s) -> adapter binding(s) found`)
|
|
332
|
-
console.log(`port-graph: ${pipelines.length} definePipeline() call(s) with a literal steps array extracted`)
|
|
333
|
-
console.log(`port-graph: wrote ${emitted.nodes.length} nodes, ${emitted.links.length} links to ${relative(root, outPath)}`)
|
|
334
|
-
|
|
335
|
-
if (nonEmpty.length === 0) {
|
|
336
|
-
console.log('port-graph: honest finding — every scanned function has R = never (DB/services are plain')
|
|
337
|
-
console.log('port-graph: parameters here, not Context.Tag requirements), so the R-channel port list is')
|
|
338
|
-
console.log('port-graph: empty by construction. The pipeline_step chains above make the validate-before-persist')
|
|
339
|
-
console.log('port-graph: invariant askable as a graph query at all; see test/architecture/port-graph-cross-check.test.ts.')
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
for (const p of pipelines) {
|
|
343
|
-
if (!VALIDATING_WRITE_OPS.has(p.name)) continue
|
|
344
|
-
const ok = stepOrderRespectsInvariant(p.steps)
|
|
345
|
-
console.log(`port-graph: pipeline "${p.name}" (${p.file}:L${p.line}) validate-before-persist = ${ok}`)
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const isMain = process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url
|
|
350
|
-
if (isMain) main()
|