Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
862 changes: 829 additions & 33 deletions packages/api-client-generated/src/generated/@tanstack/react-query.gen.ts

Large diffs are not rendered by default.

696 changes: 695 additions & 1 deletion packages/api-client-generated/src/generated/sdk.gen.ts

Large diffs are not rendered by default.

9,147 changes: 5,887 additions & 3,260 deletions packages/api-client-generated/src/generated/types.gen.ts

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions packages/api-routes/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import { measurementPlanRoutes } from './measurement-plan.js'
import type { MeasurementPlanRoutesOptions } from './measurement-plan.js'
import { measurementServiceRoutes } from './measurement-service.js'
import type { MeasurementServiceRoutesOptions } from './measurement-service.js'
import { measurementDraftRoutes } from './measurement-draft.js'
import { measurementDiscoveryV2Routes } from './measurement-discovery-v2.js'
import { measurementOverviewRoutes } from './measurement-overview.js'
import { applyRoutes } from './apply.js'
import type { ApplyRoutesOptions } from './apply.js'
import { historyRoutes } from './history.js'
Expand Down Expand Up @@ -412,6 +415,11 @@ export async function apiRoutes(app: FastifyInstance, opts: ApiRoutesOptions) {
await api.register(measurementServiceRoutes, {
fetchSitemap: opts.fetchMeasurementSitemap,
} satisfies MeasurementServiceRoutesOptions)
// Advanced Measurement v2. Registered here so the slices that fill in the
// handlers never have to touch this file or `openapi.ts`.
await api.register(measurementDraftRoutes)
await api.register(measurementDiscoveryV2Routes)
await api.register(measurementOverviewRoutes)
await api.register(applyRoutes, {
onScheduleUpdated: opts.onScheduleUpdated,
onProjectUpserted: opts.onProjectUpserted,
Expand Down
23 changes: 23 additions & 0 deletions packages/api-routes/src/measurement-discovery-v2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { FastifyInstance } from 'fastify'
import { notImplemented } from '@ainyc/canonry-contracts'

/**
* Sitemap import and identity rebinding against a setup draft.
*
* These are draft actions, but they belong here rather than in
* `measurement-draft.ts` because the fetch they perform is the hardened one:
* the server dereferences an operator-supplied URL while sitting on a tailnet
* beside internal services. Both produce review proposals only — neither
* publishes a plan nor starts a run.
*
* Registered now, with the bodies landing alongside the hardened fetch.
*/
export async function measurementDiscoveryV2Routes(app: FastifyInstance) {
const pending = (action: string): never => {
throw notImplemented(`Measurement ${action} is not available yet.`)
}

app.post<{ Params: { name: string } }>('/projects/:name/measurement-plan/draft/actions/import-sitemap', async () => pending('sitemap import'))

app.post<{ Params: { name: string } }>('/projects/:name/measurement-plan/draft/actions/apply-sitemap-selection', async () => pending('sitemap selection'))
}
74 changes: 74 additions & 0 deletions packages/api-routes/src/measurement-draft.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { FastifyInstance } from 'fastify'
import { notImplemented } from '@ainyc/canonry-contracts'

/**
* Server-side setup draft and the query assets it authors from.
*
* The routes are registered and fully described in `openapi.ts` so the
* generated client and every parity gate already know the surface; the handler
* bodies, their scope checks and their ETag/idempotency enforcement land with
* the draft service. Until then each one answers 501 rather than 404, so a
* caller can tell "not built yet" from "wrong URL".
*/
export async function measurementDraftRoutes(app: FastifyInstance) {
const pending = (action: string): never => {
throw notImplemented(`Measurement ${action} is not available yet.`)
}

app.get<{ Params: { name: string } }>('/projects/:name/measurement-setup', async () => pending('setup state'))

app.get<{ Params: { name: string } }>('/projects/:name/measurement-plan/draft', async () => pending('draft reads'))

app.get<{ Params: { name: string } }>('/projects/:name/measurement-plan/draft/targets', async () => pending('draft Target paging'))

app.get<{ Params: { name: string } }>('/projects/:name/measurement-plan/draft/assignments', async () => pending('draft assignment paging'))

app.get<{ Params: { name: string } }>('/projects/:name/measurement-plan/draft/groups', async () => pending('draft group paging'))

for (const action of [
'create',
'upsert-target',
'rename-target',
'merge-targets',
'exclude-target',
'rebind-target',
'apply-assignments',
'remove-assignment',
'clear-assignments',
'classify-assignments',
'upsert-group',
'remove-group',
'upsert-competitor',
'remove-competitor',
'publish',
'discard',
]) {
app.post<{ Params: { name: string } }>(`/projects/:name/measurement-plan/draft/actions/${action}`, async () => pending(`draft action "${action}"`))
}

// Both previews are POSTs only because the draft they compile is far too
// large for a URL, and neither writes a row — see `readSemantic` in `auth.ts`.
for (const action of ['compile-preview', 'diff-preview']) {
app.post<{ Params: { name: string } }>(`/projects/:name/measurement-plan/draft/actions/${action}`, {
config: { readSemantic: true },
}, async () => pending(`draft action "${action}"`))
}

app.post<{ Params: { name: string } }>('/projects/:name/measurement-plan/actions/deactivate', async () => pending('plan deactivation'))

app.get<{ Params: { name: string } }>('/projects/:name/measurement-query-sets', async () => pending('query sets'))

app.get<{ Params: { name: string; setId: string } }>('/projects/:name/measurement-query-sets/:setId', async () => pending('query sets'))

app.put<{ Params: { name: string; setId: string } }>('/projects/:name/measurement-query-sets/:setId', async () => pending('query sets'))

app.delete<{ Params: { name: string; setId: string } }>('/projects/:name/measurement-query-sets/:setId', async () => pending('query sets'))

app.get<{ Params: { name: string } }>('/projects/:name/measurement-query-templates', async () => pending('query templates'))

app.put<{ Params: { name: string; templateId: string } }>('/projects/:name/measurement-query-templates/:templateId', async () => pending('query templates'))

app.delete<{ Params: { name: string; templateId: string } }>('/projects/:name/measurement-query-templates/:templateId', async () => pending('query templates'))

app.post<{ Params: { name: string; templateId: string } }>('/projects/:name/measurement-query-templates/:templateId/apply', async () => pending('query template expansion'))
}
15 changes: 15 additions & 0 deletions packages/api-routes/src/measurement-overview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { FastifyInstance } from 'fastify'
import { notImplemented } from '@ainyc/canonry-contracts'

/**
* Scoped Advanced aggregates over one run's evidence.
*
* Run selection, the `brandPresence`/`sov` pair and group-only Named Share of
* Voice all land with the reporting slice. The route is registered now so the
* generated client carries the shape from the start.
*/
export async function measurementOverviewRoutes(app: FastifyInstance) {
app.get<{ Params: { name: string } }>('/projects/:name/measurement-overview', async () => {
throw notImplemented('Measurement overview is not available yet.')
})
}
37 changes: 30 additions & 7 deletions packages/api-routes/src/measurement-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import {
measurementPlanPublishRequestSchema,
measurementPlanRevisionConflict,
notFound,
parseStoredMeasurementPlan,
parseStoredMeasurementPlanAnyVersion,
validationError,
type MeasurementPlan,
type StoredMeasurementPlan,
type MeasurementPlanContext,
type MeasurementPlanCounts,
type MeasurementPlanInput,
Expand Down Expand Up @@ -43,8 +44,26 @@ type SemanticSelection = {
queryIds: string[]
}

function parseStoredPlan(canonicalJson: unknown): MeasurementPlan {
return parseStoredMeasurementPlan(canonicalJson)
/**
* Reads a revision of any schema version. Every read surface here must use this
* rather than the v1-only reader: the moment a v2 revision exists, decoding it
* through `parseStoredPlan` throws, and a route that lets that escape answers
* 500 on a perfectly valid stored plan.
*/
function parseStoredPlanAnyVersion(canonicalJson: unknown): StoredMeasurementPlan {
return parseStoredMeasurementPlanAnyVersion(canonicalJson)
}

/**
* A v1 candidate cannot be diffed against a v2 active revision: they do not
* share an assignment model, so any diff would be invented rather than
* computed. Refusing is the honest answer until the v2 diff lands.
*/
function requireV1Plan(plan: StoredMeasurementPlan, what: string): MeasurementPlan {
if (plan.schemaVersion !== 1) {
throw validationError(`${what} is not available for a schema v${plan.schemaVersion} revision.`)
}
return plan
}

function versionDto(row: typeof measurementPlanVersions.$inferSelect, active: boolean) {
Expand All @@ -53,7 +72,9 @@ function versionDto(row: typeof measurementPlanVersions.$inferSelect, active: bo
checksum: row.checksum,
createdAt: row.createdAt,
active,
plan: parseStoredPlan(row.canonicalJson),
// Any schema version: a stored revision must remain readable through the
// revision-detail surfaces regardless of which compiler wrote it.
plan: parseStoredPlanAnyVersion(row.canonicalJson),
}
}

Expand Down Expand Up @@ -292,7 +313,7 @@ function assertNoRetiredSegmentReuse(
}
}

function activePlanContainsKey(plan: MeasurementPlan, stableKey: string): boolean {
function activePlanContainsKey(plan: StoredMeasurementPlan, stableKey: string): boolean {
return plan.targets.some(target => target.stableKey === stableKey)
|| plan.groups.some(group => group.stableKey === stableKey)
}
Expand Down Expand Up @@ -324,7 +345,9 @@ export async function measurementPlanRoutes(app: FastifyInstance, opts: Measurem
const preview = compileMeasurementPlanPreview(request.body, compileContextForProject(app, project, opts))
if (!preview.ok) return { ...preview, diff: null }
const active = activePlanVersion(app, project.id)
const activePlan = active ? parseStoredPlan(active.version.canonicalJson) : null
const activePlan = active
? requireV1Plan(parseStoredPlanAnyVersion(active.version.canonicalJson), 'Plan diff')
: null
return {
...preview,
diff: planDiff(activePlan, preview.plan, active?.version.revision ?? null),
Expand Down Expand Up @@ -430,7 +453,7 @@ export async function measurementPlanRoutes(app: FastifyInstance, opts: Measurem
if (segment.retiredAt !== null) return reply.send({ stableKey: segment.stableKey, retiredAt: segment.retiredAt })

const active = activePlanVersion(app, project.id)
if (active && activePlanContainsKey(parseStoredPlan(active.version.canonicalJson), segment.stableKey)) {
if (active && activePlanContainsKey(parseStoredPlanAnyVersion(active.version.canonicalJson), segment.stableKey)) {
throw validationError(`Measurement segment "${segment.stableKey}" is in the active plan. Publish a revision without it before retiring it.`)
}

Expand Down
9 changes: 7 additions & 2 deletions packages/api-routes/src/measurement-report-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
filterCapturedCitedUrls,
isVertexGroundingRedirect,
parseMeasurementRunManifestV1,
parseStoredMeasurementPlan,
parseStoredMeasurementPlanAnyVersion,
RunKinds,
RunStatuses,
RunTriggers,
Expand Down Expand Up @@ -297,7 +297,12 @@ export function buildStoredMeasurementReport(db: DatabaseClient, projectId: stri
inArray(runs.status, [RunStatuses.completed, RunStatuses.partial]),
ne(runs.trigger, RunTriggers.probe),
)).orderBy(desc(runs.createdAt), desc(runs.id)).get()
const plan = parseStoredMeasurementPlan(version.canonicalJson)
// Slice 5 replaces this branch with the v2 report path. Until then a v2
// revision is reported as not-yet-reportable rather than decoded through the
// v1 reader, which would throw and surface as a 500.
const stored = parseStoredMeasurementPlanAnyVersion(version.canonicalJson)
if (stored.schemaVersion !== 1) return { kind: 'no-plan', revision }
const plan = stored
let selectedRun = run
let manifest: MeasurementRunManifestV1
let legacy = false
Expand Down
76 changes: 76 additions & 0 deletions packages/api-routes/src/openapi-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,53 @@ import {
locationContextSchema,
measurementDiscoveryRequestSchema,
measurementDiscoveryResponseSchema,
measurementDraftApplyAssignmentsRequestSchema,
measurementDraftApplySitemapSelectionRequestSchema,
measurementDraftAssignmentPageSchema,
measurementDraftClassifyAssignmentsRequestSchema,
measurementDraftClearAssignmentsRequestSchema,
measurementDraftCompilePreviewResponseSchema,
measurementDraftCreateRequestSchema,
measurementDraftDiffPreviewResponseSchema,
measurementDraftDiscardResponseSchema,
measurementDraftExcludeTargetRequestSchema,
measurementDraftGroupPageSchema,
measurementDraftImportSitemapRequestSchema,
measurementDraftMergeTargetsRequestSchema,
measurementDraftMutationResponseSchema,
measurementDraftPublishRequestSchema,
measurementDraftRebindTargetRequestSchema,
measurementDraftRemoveAssignmentRequestSchema,
measurementDraftRemoveCompetitorRequestSchema,
measurementDraftRemoveGroupRequestSchema,
measurementDraftRenameTargetRequestSchema,
measurementDraftResponseSchema,
measurementDraftTargetPageSchema,
measurementDraftUpsertCompetitorRequestSchema,
measurementDraftUpsertGroupRequestSchema,
measurementDraftUpsertTargetRequestSchema,
measurementOverviewResponseSchema,
measurementPlanAuthoringSchema,
measurementPlanCompilePreviewResponseSchema,
measurementPlanDeactivateRequestSchema,
measurementPlanDeactivateResponseSchema,
measurementPlanDiffPreviewResponseSchema,
measurementPlanPublishRequestSchema,
measurementPlanResponseSchema,
measurementPlanV2PublishResponseSchema,
measurementPlanVersionResponseSchema,
measurementPlanVersionsResponseSchema,
measurementQuerySetDetailSchema,
measurementQuerySetListResponseSchema,
measurementQuerySetUpsertRequestSchema,
measurementQueryTemplateApplyRequestSchema,
measurementQueryTemplateApplyResponseSchema,
measurementQueryTemplateListResponseSchema,
measurementQueryTemplateSchema,
measurementQueryTemplateUpsertRequestSchema,
measurementReportResponseSchema,
measurementSegmentRetirementResponseSchema,
measurementSetupResponseSchema,
notificationDtoSchema,
onboardingTelemetryEventSchema,
organicEvidenceDtoSchema,
Expand Down Expand Up @@ -290,15 +328,53 @@ const SCHEMA_TABLE = {
LocationContext: locationContextSchema,
MeasurementDiscoveryRequest: measurementDiscoveryRequestSchema,
MeasurementDiscoveryResponse: measurementDiscoveryResponseSchema,
MeasurementDraftApplyAssignmentsRequest: measurementDraftApplyAssignmentsRequestSchema,
MeasurementDraftApplySitemapSelectionRequest: measurementDraftApplySitemapSelectionRequestSchema,
MeasurementDraftAssignmentPage: measurementDraftAssignmentPageSchema,
MeasurementDraftClassifyAssignmentsRequest: measurementDraftClassifyAssignmentsRequestSchema,
MeasurementDraftClearAssignmentsRequest: measurementDraftClearAssignmentsRequestSchema,
MeasurementDraftCompilePreviewResponse: measurementDraftCompilePreviewResponseSchema,
MeasurementDraftCreateRequest: measurementDraftCreateRequestSchema,
MeasurementDraftDiffPreviewResponse: measurementDraftDiffPreviewResponseSchema,
MeasurementDraftDiscardResponse: measurementDraftDiscardResponseSchema,
MeasurementDraftExcludeTargetRequest: measurementDraftExcludeTargetRequestSchema,
MeasurementDraftGroupPage: measurementDraftGroupPageSchema,
MeasurementDraftImportSitemapRequest: measurementDraftImportSitemapRequestSchema,
MeasurementDraftMergeTargetsRequest: measurementDraftMergeTargetsRequestSchema,
MeasurementDraftMutationResponse: measurementDraftMutationResponseSchema,
MeasurementDraftPublishRequest: measurementDraftPublishRequestSchema,
MeasurementDraftRebindTargetRequest: measurementDraftRebindTargetRequestSchema,
MeasurementDraftRemoveAssignmentRequest: measurementDraftRemoveAssignmentRequestSchema,
MeasurementDraftRemoveCompetitorRequest: measurementDraftRemoveCompetitorRequestSchema,
MeasurementDraftRemoveGroupRequest: measurementDraftRemoveGroupRequestSchema,
MeasurementDraftRenameTargetRequest: measurementDraftRenameTargetRequestSchema,
MeasurementDraftResponse: measurementDraftResponseSchema,
MeasurementDraftTargetPage: measurementDraftTargetPageSchema,
MeasurementDraftUpsertCompetitorRequest: measurementDraftUpsertCompetitorRequestSchema,
MeasurementDraftUpsertGroupRequest: measurementDraftUpsertGroupRequestSchema,
MeasurementDraftUpsertTargetRequest: measurementDraftUpsertTargetRequestSchema,
MeasurementOverviewResponse: measurementOverviewResponseSchema,
MeasurementPlanCompilePreviewResponse: measurementPlanCompilePreviewResponseSchema,
MeasurementPlanDeactivateRequest: measurementPlanDeactivateRequestSchema,
MeasurementPlanDeactivateResponse: measurementPlanDeactivateResponseSchema,
MeasurementPlanDiffPreviewResponse: measurementPlanDiffPreviewResponseSchema,
MeasurementPlanInput: measurementPlanAuthoringSchema,
MeasurementPlanPublishRequest: measurementPlanPublishRequestSchema,
MeasurementPlanResponse: measurementPlanResponseSchema,
MeasurementPlanV2PublishResponse: measurementPlanV2PublishResponseSchema,
MeasurementPlanVersionResponse: measurementPlanVersionResponseSchema,
MeasurementPlanVersionsResponse: measurementPlanVersionsResponseSchema,
MeasurementQuerySetDetail: measurementQuerySetDetailSchema,
MeasurementQuerySetListResponse: measurementQuerySetListResponseSchema,
MeasurementQuerySetUpsertRequest: measurementQuerySetUpsertRequestSchema,
MeasurementQueryTemplateApplyRequest: measurementQueryTemplateApplyRequestSchema,
MeasurementQueryTemplateApplyResponse: measurementQueryTemplateApplyResponseSchema,
MeasurementQueryTemplateDto: measurementQueryTemplateSchema,
MeasurementQueryTemplateListResponse: measurementQueryTemplateListResponseSchema,
MeasurementQueryTemplateUpsertRequest: measurementQueryTemplateUpsertRequestSchema,
MeasurementReportResponse: measurementReportResponseSchema,
MeasurementSegmentRetirementResponse: measurementSegmentRetirementResponseSchema,
MeasurementSetupResponse: measurementSetupResponseSchema,
NotificationDto: notificationDtoSchema,
OnboardingTelemetryEvent: onboardingTelemetryEventSchema,
OrganicEvidenceDto: organicEvidenceDtoSchema,
Expand Down
Loading
Loading