@defrex/autobuild 0.8.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/LICENSE +201 -0
- package/README.md +192 -0
- package/SPEC.md +2269 -0
- package/bin/ab-build-runner.ts +47 -0
- package/bin/ab-dev.ts +27 -0
- package/bin/ab-dispatch-kernel.ts +92 -0
- package/bin/ab-harvest-runner.ts +29 -0
- package/bin/ab-init-probe.ts +19 -0
- package/bin/ab-process-group-reaper.ts +32 -0
- package/bin/ab.ts +6 -0
- package/bin/agent/ab +3 -0
- package/docs/answering-blocked-builds.md +89 -0
- package/docs/architecture.md +726 -0
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +1623 -0
- package/docs/hosted-dispatcher-cutover-validation.md +178 -0
- package/docs/hosted-dispatcher.md +204 -0
- package/docs/mcp.md +119 -0
- package/docs/operator-api.md +186 -0
- package/docs/remote-store-protocol.md +972 -0
- package/docs/remote-ticket-protocol.md +79 -0
- package/docs/setup.md +289 -0
- package/docs/spec-standard.md +63 -0
- package/docs/ticket-dependencies.md +59 -0
- package/docs/vercel-sandbox-rollout-probe.md +7 -0
- package/package.json +135 -0
- package/packages/core/src/cli/args.ts +74 -0
- package/packages/core/src/cli/artifact.ts +182 -0
- package/packages/core/src/cli/binary.ts +198 -0
- package/packages/core/src/cli/build-control.ts +789 -0
- package/packages/core/src/cli/build-progress.ts +55 -0
- package/packages/core/src/cli/bulk-control.ts +385 -0
- package/packages/core/src/cli/context.ts +470 -0
- package/packages/core/src/cli/dashboard/actions.ts +59 -0
- package/packages/core/src/cli/dashboard/cells.ts +92 -0
- package/packages/core/src/cli/dashboard/composer.ts +230 -0
- package/packages/core/src/cli/dashboard/detail.ts +100 -0
- package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
- package/packages/core/src/cli/dashboard/live.ts +152 -0
- package/packages/core/src/cli/dashboard/model.ts +1173 -0
- package/packages/core/src/cli/dashboard/poll.ts +196 -0
- package/packages/core/src/cli/dashboard/render.ts +1568 -0
- package/packages/core/src/cli/dashboard/selection.ts +66 -0
- package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
- package/packages/core/src/cli/dashboard/session-view.ts +506 -0
- package/packages/core/src/cli/dashboard/transcript.ts +108 -0
- package/packages/core/src/cli/dev.ts +46 -0
- package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
- package/packages/core/src/cli/dispatch-process.ts +259 -0
- package/packages/core/src/cli/dispatch.ts +3692 -0
- package/packages/core/src/cli/dotenv.ts +48 -0
- package/packages/core/src/cli/env.ts +206 -0
- package/packages/core/src/cli/harvest.ts +698 -0
- package/packages/core/src/cli/help.ts +713 -0
- package/packages/core/src/cli/init-agent.ts +106 -0
- package/packages/core/src/cli/init-validation.ts +583 -0
- package/packages/core/src/cli/init.ts +756 -0
- package/packages/core/src/cli/installation.ts +333 -0
- package/packages/core/src/cli/keyboard.ts +261 -0
- package/packages/core/src/cli/main.ts +1399 -0
- package/packages/core/src/cli/mcp.ts +279 -0
- package/packages/core/src/cli/models.ts +65 -0
- package/packages/core/src/cli/observe.ts +55 -0
- package/packages/core/src/cli/plugin.ts +227 -0
- package/packages/core/src/cli/pr-attachments.ts +138 -0
- package/packages/core/src/cli/pr-summary.ts +124 -0
- package/packages/core/src/cli/publication-settlement.ts +188 -0
- package/packages/core/src/cli/repo-state.ts +213 -0
- package/packages/core/src/cli/repository-status.ts +80 -0
- package/packages/core/src/cli/self-update.ts +443 -0
- package/packages/core/src/cli/status.ts +895 -0
- package/packages/core/src/cli/store-opening.contract.ts +289 -0
- package/packages/core/src/cli/store-opening.ts +167 -0
- package/packages/core/src/cli/store-ref.ts +33 -0
- package/packages/core/src/cli/terminal-restore.ts +174 -0
- package/packages/core/src/cli/terminal.ts +361 -0
- package/packages/core/src/cli/terminals.ts +995 -0
- package/packages/core/src/cli/testkit.ts +194 -0
- package/packages/core/src/cli/ticket.ts +484 -0
- package/packages/core/src/cli/upgrade-agent.ts +180 -0
- package/packages/core/src/cli/upgrade-commit.ts +468 -0
- package/packages/core/src/cli/upgrade-notice.ts +91 -0
- package/packages/core/src/cli/upgrade-progress.ts +110 -0
- package/packages/core/src/cli/upgrade.ts +888 -0
- package/packages/core/src/cli/wait.ts +587 -0
- package/packages/core/src/cli/watch.ts +932 -0
- package/packages/core/src/config/live.ts +255 -0
- package/packages/core/src/config/load.ts +98 -0
- package/packages/core/src/config/roles.ts +438 -0
- package/packages/core/src/config/schema.ts +774 -0
- package/packages/core/src/distribution.ts +24 -0
- package/packages/core/src/events/catalog.ts +201 -0
- package/packages/core/src/events/envelope.ts +59 -0
- package/packages/core/src/events/payloads.ts +618 -0
- package/packages/core/src/events/repository.ts +559 -0
- package/packages/core/src/events/sessions.ts +180 -0
- package/packages/core/src/harvest/schema.ts +147 -0
- package/packages/core/src/ids.ts +45 -0
- package/packages/core/src/integration/harness.ts +681 -0
- package/packages/core/src/kernel/auto-merge.ts +259 -0
- package/packages/core/src/kernel/converge.ts +117 -0
- package/packages/core/src/kernel/dispatch-settings.ts +60 -0
- package/packages/core/src/kernel/dispatch-status.ts +141 -0
- package/packages/core/src/kernel/engine.ts +1047 -0
- package/packages/core/src/kernel/harvest.ts +823 -0
- package/packages/core/src/kernel/origin.ts +69 -0
- package/packages/core/src/kernel/phases.ts +196 -0
- package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
- package/packages/core/src/kernel/pr-attachments.ts +82 -0
- package/packages/core/src/kernel/reducer.ts +752 -0
- package/packages/core/src/kernel/stall.ts +155 -0
- package/packages/core/src/kernel/verify-applicability.ts +52 -0
- package/packages/core/src/markdown.ts +67 -0
- package/packages/core/src/ontology.ts +288 -0
- package/packages/core/src/open-map.ts +203 -0
- package/packages/core/src/operator/annotations.ts +187 -0
- package/packages/core/src/operator/client.ts +311 -0
- package/packages/core/src/operator/control.ts +117 -0
- package/packages/core/src/operator/index.ts +62 -0
- package/packages/core/src/operator/presentation.ts +22 -0
- package/packages/core/src/operator/protocol.ts +98 -0
- package/packages/core/src/operator/query.ts +201 -0
- package/packages/core/src/operator/registry.ts +1043 -0
- package/packages/core/src/operator/requests.ts +138 -0
- package/packages/core/src/operator/sandbox.ts +458 -0
- package/packages/core/src/operator/server.ts +616 -0
- package/packages/core/src/operator/sessions.ts +223 -0
- package/packages/core/src/operator/tickets.ts +177 -0
- package/packages/core/src/plugin-sdk/index.ts +174 -0
- package/packages/core/src/plugins/contract-entry.ts +74 -0
- package/packages/core/src/plugins/contract-env.ts +3 -0
- package/packages/core/src/plugins/load.ts +259 -0
- package/packages/core/src/plugins/manifest.ts +236 -0
- package/packages/core/src/plugins/registry.ts +274 -0
- package/packages/core/src/plugins/runtimes.ts +75 -0
- package/packages/core/src/ports/forge/contract.ts +258 -0
- package/packages/core/src/ports/forge/create.ts +74 -0
- package/packages/core/src/ports/forge/fake.ts +488 -0
- package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
- package/packages/core/src/ports/forge/github-transport.ts +352 -0
- package/packages/core/src/ports/forge/github.ts +1162 -0
- package/packages/core/src/ports/forge/local-git.ts +568 -0
- package/packages/core/src/ports/runner/claude.ts +873 -0
- package/packages/core/src/ports/runner/cli-stream.ts +90 -0
- package/packages/core/src/ports/runner/codex.ts +873 -0
- package/packages/core/src/ports/runner/contract.ts +492 -0
- package/packages/core/src/ports/runner/fake.ts +213 -0
- package/packages/core/src/ports/runner/json-record.ts +2 -0
- package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
- package/packages/core/src/ports/runner/one-shot.ts +29 -0
- package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
- package/packages/core/src/ports/runner/pi-bridge.js +6 -0
- package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
- package/packages/core/src/ports/runner/pi.ts +617 -0
- package/packages/core/src/ports/runner/production.ts +74 -0
- package/packages/core/src/ports/runner/provider-error.ts +121 -0
- package/packages/core/src/ports/runner/routing.ts +234 -0
- package/packages/core/src/ports/runner/runtime.ts +214 -0
- package/packages/core/src/ports/runner/session-env.ts +34 -0
- package/packages/core/src/ports/runner/stream-parts.ts +103 -0
- package/packages/core/src/ports/tickets/contract.ts +570 -0
- package/packages/core/src/ports/tickets/create.ts +135 -0
- package/packages/core/src/ports/tickets/fake.ts +215 -0
- package/packages/core/src/ports/tickets/file.ts +559 -0
- package/packages/core/src/ports/tickets/linear.ts +811 -0
- package/packages/core/src/ports/tickets/operations.ts +161 -0
- package/packages/core/src/ports/tickets/remote-index.ts +13 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
- package/packages/core/src/ports/tickets/remote-server.ts +185 -0
- package/packages/core/src/ports/tickets/remote.ts +130 -0
- package/packages/core/src/ports/tickets/update.ts +51 -0
- package/packages/core/src/ports/types.ts +528 -0
- package/packages/core/src/ports/workspace/build-execution.ts +123 -0
- package/packages/core/src/ports/workspace/contract.ts +136 -0
- package/packages/core/src/ports/workspace/create.ts +132 -0
- package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
- package/packages/core/src/ports/workspace/fake.ts +432 -0
- package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
- package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
- package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
- package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
- package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
- package/packages/core/src/processes/build-child-exit.ts +78 -0
- package/packages/core/src/processes/build-child.ts +119 -0
- package/packages/core/src/processes/build-execution-state.ts +95 -0
- package/packages/core/src/processes/build-parent-watch.ts +35 -0
- package/packages/core/src/processes/build-runner.ts +2614 -0
- package/packages/core/src/processes/dispatcher.ts +2825 -0
- package/packages/core/src/processes/execution-settlement.ts +171 -0
- package/packages/core/src/processes/harvest-child.ts +115 -0
- package/packages/core/src/processes/harvest-execution-state.ts +121 -0
- package/packages/core/src/processes/harvest-runner.ts +1656 -0
- package/packages/core/src/processes/harvest.ts +595 -0
- package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
- package/packages/core/src/processes/phase-failure-budget.ts +52 -0
- package/packages/core/src/processes/process-group-reaper.ts +35 -0
- package/packages/core/src/processes/process-group.ts +65 -0
- package/packages/core/src/processes/publication-loss.ts +62 -0
- package/packages/core/src/processes/publication-state.ts +93 -0
- package/packages/core/src/processes/sandbox-state.ts +101 -0
- package/packages/core/src/registry.ts +24 -0
- package/packages/core/src/sessions/reducer.ts +185 -0
- package/packages/core/src/skills.ts +7 -0
- package/packages/core/src/spec-standard.ts +47 -0
- package/packages/core/src/store/adapter.ts +80 -0
- package/packages/core/src/store/build-scope.ts +272 -0
- package/packages/core/src/store/contract.ts +2581 -0
- package/packages/core/src/store/local/blobs.ts +38 -0
- package/packages/core/src/store/local/schema.ts +180 -0
- package/packages/core/src/store/local/store.ts +1713 -0
- package/packages/core/src/store/memory.ts +1138 -0
- package/packages/core/src/store/phase-session.ts +425 -0
- package/packages/core/src/store/remote/client.ts +780 -0
- package/packages/core/src/store/remote/index.ts +12 -0
- package/packages/core/src/store/remote/protocol.ts +348 -0
- package/packages/core/src/store/remote/server.ts +923 -0
- package/packages/core/src/store/remote/token.ts +137 -0
- package/packages/core/src/store/remote/version.ts +10 -0
- package/packages/core/src/store/retention.ts +91 -0
- package/packages/core/src/store/session-handle.ts +265 -0
- package/packages/core/src/store/streams/assemble.ts +79 -0
- package/packages/core/src/store/streams/lock.ts +42 -0
- package/packages/core/src/store/streams/session-writer.ts +306 -0
- package/packages/core/src/store/streams/types.ts +171 -0
- package/packages/core/src/store/streams/wait.ts +127 -0
- package/packages/core/src/store/subscribe.ts +105 -0
- package/packages/core/src/store/types.ts +466 -0
- package/packages/core/src/testing/fixed.ts +44 -0
- package/packages/core/src/testing/store-failures.ts +32 -0
- package/packages/core/src/zod-issues.ts +91 -0
- package/skills/code-review/SKILL.md +100 -0
- package/skills/finalize/SKILL.md +44 -0
- package/skills/guide/SKILL.md +1781 -0
- package/skills/guide/references/operator-api.md +164 -0
- package/skills/guide/references/plugin-authoring.md +354 -0
- package/skills/guide/references/remote-store-protocol.md +972 -0
- package/skills/guide/references/remote-ticket-protocol.md +79 -0
- package/skills/guide/references/setup.md +289 -0
- package/skills/guide/references/spec-standard.md +63 -0
- package/skills/guide/references/ticket-dependencies.md +59 -0
- package/skills/harvest/SKILL.md +66 -0
- package/skills/harvest-review/SKILL.md +63 -0
- package/skills/implement/SKILL.md +82 -0
- package/skills/plan/SKILL.md +123 -0
- package/skills/plan-review/SKILL.md +121 -0
- package/skills/reconcile/SKILL.md +65 -0
- package/skills/spec/SKILL.md +132 -0
- package/skills/tickets/SKILL.md +139 -0
- package/templates/autobuild.toml +36 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence-chain mechanics for stall detection (SPEC §15.4, D4).
|
|
3
|
+
*
|
|
4
|
+
* The split follows the constitution: deciding whether a new finding is *the
|
|
5
|
+
* same disagreement* as an earlier one is fuzzy → judgment → the reviewer
|
|
6
|
+
* (fresh each round) marks `persists`. Everything in this module is the
|
|
7
|
+
* deterministic half: following the marked links and applying the threshold.
|
|
8
|
+
*
|
|
9
|
+
* Chain model:
|
|
10
|
+
* - A chain is identified by its root — a finding whose `persists` resolves to
|
|
11
|
+
* no earlier-round finding. `persists` links are honored only when the
|
|
12
|
+
* target exists in a strictly earlier round; unknown ids and same/later
|
|
13
|
+
* round targets are not links (ids are deposit-stamped, so a reviewer can
|
|
14
|
+
* only ever reference rounds it was shown — §8.3).
|
|
15
|
+
* - Branching persists (two findings continuing one ancestor) extend the same
|
|
16
|
+
* chain; a finding with multiple `persists` entries joins every chain its
|
|
17
|
+
* ancestors belong to.
|
|
18
|
+
* - A chain's `rounds` is the longest run of *consecutive* rounds it occupies
|
|
19
|
+
* (SPEC §15.4: a chain "survives" N rounds). A round in which no reviewer
|
|
20
|
+
* continued the chain breaks the streak: that round's fresh skeptic saw all
|
|
21
|
+
* prior findings (§8.3) and judged the disagreement resolved, so a later
|
|
22
|
+
* re-raise starts surviving anew — though the chain's `ids` keep the full
|
|
23
|
+
* history.
|
|
24
|
+
*/
|
|
25
|
+
import type { Finding } from '../ontology'
|
|
26
|
+
|
|
27
|
+
export interface FindingChain {
|
|
28
|
+
/** Member finding ids in round order, chain root first. */
|
|
29
|
+
ids: string[]
|
|
30
|
+
/** Count of distinct consecutive rounds the chain spans (longest streak). */
|
|
31
|
+
rounds: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ChainMembers {
|
|
35
|
+
ids: string[]
|
|
36
|
+
/** Parallel to `ids`: the 1-based round each member was found in. */
|
|
37
|
+
memberRounds: number[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Follow `persists` links across rounds (`roundFindings[0]` is round 1) and
|
|
42
|
+
* group findings into chains, one per root. Single fresh findings are chains
|
|
43
|
+
* of `rounds: 1` — staying below any sane threshold is what "fresh" means.
|
|
44
|
+
*/
|
|
45
|
+
export function persistenceChains(roundFindings: Finding[][]): FindingChain[] {
|
|
46
|
+
return buildChains(roundFindings).map((chain) => ({
|
|
47
|
+
ids: chain.ids,
|
|
48
|
+
rounds: longestConsecutiveRun(chain.memberRounds),
|
|
49
|
+
}))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Chains whose streak has reached `stallRounds` (SPEC §15.4) — minus chains a
|
|
54
|
+
* human already resolved via `escalation.answered {dismiss-finding}`
|
|
55
|
+
* (§15.6-B).
|
|
56
|
+
*
|
|
57
|
+
* Dismissal semantics: a chain is human-resolved when every finding in its
|
|
58
|
+
* most recent round (its tip — the live continuations of the disagreement) is
|
|
59
|
+
* dismissed. Earlier members are history and need no dismissal. If a later
|
|
60
|
+
* round persists into a dismissed chain with a finding that is not itself
|
|
61
|
+
* dismissed, the tip moves and the chain resurrects — with its full streak
|
|
62
|
+
* intact, so a reviewer overriding a human dismissal re-escalates at the very
|
|
63
|
+
* next revise rather than burning `stallRounds` more rounds.
|
|
64
|
+
*/
|
|
65
|
+
export function stalledChains(
|
|
66
|
+
roundFindings: Finding[][],
|
|
67
|
+
stallRounds: number,
|
|
68
|
+
dismissedIds?: ReadonlySet<string>,
|
|
69
|
+
): FindingChain[] {
|
|
70
|
+
const stalled: FindingChain[] = []
|
|
71
|
+
for (const chain of buildChains(roundFindings)) {
|
|
72
|
+
const rounds = longestConsecutiveRun(chain.memberRounds)
|
|
73
|
+
if (rounds < stallRounds) continue
|
|
74
|
+
if (dismissedIds !== undefined && tipDismissed(chain, dismissedIds)) continue
|
|
75
|
+
stalled.push({ ids: chain.ids, rounds })
|
|
76
|
+
}
|
|
77
|
+
return stalled
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildChains(roundFindings: Finding[][]): ChainMembers[] {
|
|
81
|
+
const byId = new Map<string, { finding: Finding; round: number }>()
|
|
82
|
+
const traversal: string[] = []
|
|
83
|
+
roundFindings.forEach((findings, index) => {
|
|
84
|
+
for (const finding of findings) {
|
|
85
|
+
if (byId.has(finding.id)) continue
|
|
86
|
+
byId.set(finding.id, { finding, round: index + 1 })
|
|
87
|
+
traversal.push(finding.id)
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Links point strictly to earlier rounds, so recursion terminates; memoized
|
|
92
|
+
// so shared ancestors are resolved once.
|
|
93
|
+
const rootsMemo = new Map<string, ReadonlySet<string>>()
|
|
94
|
+
function rootsOf(id: string): ReadonlySet<string> {
|
|
95
|
+
const memoized = rootsMemo.get(id)
|
|
96
|
+
if (memoized !== undefined) return memoized
|
|
97
|
+
const node = byId.get(id)
|
|
98
|
+
if (node === undefined) return new Set()
|
|
99
|
+
const parents = node.finding.persists.filter((parentId) => {
|
|
100
|
+
const parent = byId.get(parentId)
|
|
101
|
+
return parent !== undefined && parent.round < node.round
|
|
102
|
+
})
|
|
103
|
+
const roots = new Set<string>()
|
|
104
|
+
if (parents.length === 0) {
|
|
105
|
+
roots.add(id)
|
|
106
|
+
} else {
|
|
107
|
+
for (const parentId of parents) {
|
|
108
|
+
for (const root of rootsOf(parentId)) roots.add(root)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
rootsMemo.set(id, roots)
|
|
112
|
+
return roots
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Traversal is round-major, and a root is always its chain's earliest
|
|
116
|
+
// member, so each chain lists its root first and members in round order.
|
|
117
|
+
const chains = new Map<string, ChainMembers>()
|
|
118
|
+
for (const id of traversal) {
|
|
119
|
+
const node = byId.get(id)
|
|
120
|
+
if (node === undefined) continue
|
|
121
|
+
for (const root of rootsOf(id)) {
|
|
122
|
+
let chain = chains.get(root)
|
|
123
|
+
if (chain === undefined) {
|
|
124
|
+
chain = { ids: [], memberRounds: [] }
|
|
125
|
+
chains.set(root, chain)
|
|
126
|
+
}
|
|
127
|
+
chain.ids.push(id)
|
|
128
|
+
chain.memberRounds.push(node.round)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return [...chains.values()]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function longestConsecutiveRun(rounds: readonly number[]): number {
|
|
135
|
+
const occupied = new Set(rounds)
|
|
136
|
+
let longest = 0
|
|
137
|
+
for (const round of occupied) {
|
|
138
|
+
if (occupied.has(round - 1)) continue
|
|
139
|
+
let length = 1
|
|
140
|
+
while (occupied.has(round + length)) length += 1
|
|
141
|
+
if (length > longest) longest = length
|
|
142
|
+
}
|
|
143
|
+
return longest
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function tipDismissed(chain: ChainMembers, dismissedIds: ReadonlySet<string>): boolean {
|
|
147
|
+
const tipRound = Math.max(...chain.memberRounds)
|
|
148
|
+
for (let i = 0; i < chain.ids.length; i += 1) {
|
|
149
|
+
const id = chain.ids[i]
|
|
150
|
+
if (chain.memberRounds[i] === tipRound && id !== undefined && !dismissedIds.has(id)) {
|
|
151
|
+
return false
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return true
|
|
155
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { verifyPathGlobError } from '../config/schema'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure verify-applicability rule. `always` is explicit rather than represented
|
|
5
|
+
* by an empty selector list, so mandatory-gate precedence cannot depend on
|
|
6
|
+
* truthiness or ordering.
|
|
7
|
+
*/
|
|
8
|
+
export type VerifyApplicabilityRule =
|
|
9
|
+
| { kind: 'always' }
|
|
10
|
+
| { kind: 'paths'; step: string; paths: readonly string[] }
|
|
11
|
+
|
|
12
|
+
export type VerifyApplicabilityResult = { applies: true } | { applies: false; reason: string }
|
|
13
|
+
|
|
14
|
+
/** Stable, queryable reason used by the kernel-authored skipped outcome. */
|
|
15
|
+
export function pathExclusionReason(step: string, paths: readonly string[]): string {
|
|
16
|
+
return `excluded by [verify.${step}].paths: no changed path matched ${JSON.stringify(paths)}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Evaluate one rule against Git's repository-relative, `/`-separated changed
|
|
21
|
+
* paths. Matching is case-sensitive and OR-shaped: one selector matching one
|
|
22
|
+
* changed path is enough. No I/O and no event writes occur here.
|
|
23
|
+
*/
|
|
24
|
+
export function evaluateVerifyApplicability(
|
|
25
|
+
rule: VerifyApplicabilityRule,
|
|
26
|
+
changedPaths: readonly string[],
|
|
27
|
+
): VerifyApplicabilityResult {
|
|
28
|
+
if (rule.kind === 'always') return { applies: true }
|
|
29
|
+
|
|
30
|
+
// Parsed Config is the normal caller and has already validated these. Keep
|
|
31
|
+
// this exported pure seam fail-closed for direct callers too: Bun.Glob is
|
|
32
|
+
// never asked to interpret syntax outside the documented subset.
|
|
33
|
+
for (const path of rule.paths) {
|
|
34
|
+
const error = verifyPathGlobError(path)
|
|
35
|
+
if (error !== undefined) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`invalid [verify.${rule.step}].paths selector ${JSON.stringify(path)}: ${error}`,
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const globs = rule.paths.map((path) => new Bun.Glob(path))
|
|
43
|
+
for (const changedPath of changedPaths) {
|
|
44
|
+
for (const glob of globs) {
|
|
45
|
+
if (glob.match(changedPath)) return { applies: true }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
applies: false,
|
|
50
|
+
reason: pathExclusionReason(rule.step, rule.paths),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approximate Markdown reference extraction, shared by the checks that ask
|
|
3
|
+
* "does this link point at something that exists?".
|
|
4
|
+
*
|
|
5
|
+
* These are regexes, not a CommonMark parser. Reference-style targets
|
|
6
|
+
* (`![alt][ref]`) and targets containing parentheses are invisible to them.
|
|
7
|
+
* Both callers are link-existence checks, where an unseen reference fails
|
|
8
|
+
* toward a loud complaint rather than a silent pass, so the approximation is
|
|
9
|
+
* safe — but a false report is a signal to widen the parser, never to delete
|
|
10
|
+
* the thing it failed to see.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Blanks out fenced code blocks, preserving line count. A target inside a fence
|
|
15
|
+
* is sample text, not a rendered reference. Backtick and tilde fences are
|
|
16
|
+
* tracked separately so a `~~~` line inside a ``` block does not close it.
|
|
17
|
+
*/
|
|
18
|
+
export function withoutFencedCode(markdown: string): string {
|
|
19
|
+
const lines = markdown.split('\n')
|
|
20
|
+
let fence: '`' | '~' | undefined
|
|
21
|
+
return lines
|
|
22
|
+
.map((line) => {
|
|
23
|
+
const opening = line.match(/^\s*(`{3,}|~{3,})/)
|
|
24
|
+
if (opening) {
|
|
25
|
+
const marker = opening[1]![0] as '`' | '~'
|
|
26
|
+
if (fence === undefined) fence = marker
|
|
27
|
+
else if (fence === marker) fence = undefined
|
|
28
|
+
return ''
|
|
29
|
+
}
|
|
30
|
+
return fence === undefined ? line : ''
|
|
31
|
+
})
|
|
32
|
+
.join('\n')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Inline link and image targets, in source order. Angle brackets are unwrapped
|
|
37
|
+
* and a title suffix is dropped, so `[a](<b.md> "T")` yields `b.md`.
|
|
38
|
+
*/
|
|
39
|
+
export function markdownTargets(markdown: string): string[] {
|
|
40
|
+
const targets: string[] = []
|
|
41
|
+
for (const match of markdown.matchAll(/!?\[[^\]]*\]\(([^)]+)\)/g)) {
|
|
42
|
+
const inline = match[1]!.trim()
|
|
43
|
+
// An angle-bracketed destination runs to its closing `>`, and whatever
|
|
44
|
+
// follows is the title. The two must be read together: unwrapping first
|
|
45
|
+
// fails whenever a title is present (the text no longer ends in `>`), and
|
|
46
|
+
// splitting the title off first would truncate a bracketed destination that
|
|
47
|
+
// legitimately contains whitespace and a quote. A `<` with no closing `>`
|
|
48
|
+
// is malformed, and falls through to the unbracketed reading.
|
|
49
|
+
const closing = inline.startsWith('<') ? inline.indexOf('>') : -1
|
|
50
|
+
targets.push(closing === -1 ? inline.split(/\s+["']/u, 1)[0]! : inline.slice(1, closing))
|
|
51
|
+
}
|
|
52
|
+
return targets
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* `src` attributes of raw `<img>` tags, in source order. Kept separate from
|
|
57
|
+
* `markdownTargets` so callers that only care about Markdown syntax keep their
|
|
58
|
+
* existing behaviour: Markdown permits raw HTML, and this repository's README
|
|
59
|
+
* already mixes it in, so a centred hero image would otherwise be invisible.
|
|
60
|
+
*/
|
|
61
|
+
export function htmlImageTargets(markdown: string): string[] {
|
|
62
|
+
const targets: string[] = []
|
|
63
|
+
for (const match of markdown.matchAll(/<img\b[^>]*?\ssrc\s*=\s*("([^"]*)"|'([^']*)')/giu)) {
|
|
64
|
+
targets.push((match[2] ?? match[3])!.trim())
|
|
65
|
+
}
|
|
66
|
+
return targets
|
|
67
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ontology (SPEC §4): one name, used everywhere; every noun lives in
|
|
3
|
+
* exactly one layer. Schemas are the source of truth — the `ab` CLI and the
|
|
4
|
+
* store validate deposits against them (D6), and the TypeScript types are
|
|
5
|
+
* inferred.
|
|
6
|
+
*
|
|
7
|
+
* Reserved-word discipline: **build** names the whole pipeline execution and
|
|
8
|
+
* nothing else; the coding phase is **implement**.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from 'zod'
|
|
11
|
+
|
|
12
|
+
// ── Phases (SPEC §5) ─────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export const CORE_PHASES = [
|
|
15
|
+
'plan',
|
|
16
|
+
'plan-review',
|
|
17
|
+
'implement',
|
|
18
|
+
'code-review',
|
|
19
|
+
'finalize',
|
|
20
|
+
'reconcile',
|
|
21
|
+
] as const
|
|
22
|
+
export type CorePhase = (typeof CORE_PHASES)[number]
|
|
23
|
+
|
|
24
|
+
/** Verify steps are config-defined — an open set (SPEC §15.2.1). */
|
|
25
|
+
export type VerifyPhase = `verify:${string}`
|
|
26
|
+
export type Phase = CorePhase | VerifyPhase
|
|
27
|
+
|
|
28
|
+
export function isCorePhase(value: string): value is CorePhase {
|
|
29
|
+
return (CORE_PHASES as readonly string[]).includes(value)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isVerifyPhase(value: string): value is VerifyPhase {
|
|
33
|
+
return value.startsWith('verify:') && value.length > 'verify:'.length
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isPhase(value: string): value is Phase {
|
|
37
|
+
return isCorePhase(value) || isVerifyPhase(value)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `verify:e2e` → `e2e` */
|
|
41
|
+
export function verifyStep(phase: VerifyPhase): string {
|
|
42
|
+
return phase.slice('verify:'.length)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function verifyPhase(step: string): VerifyPhase {
|
|
46
|
+
return `verify:${step}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const phaseSchema = z.custom<Phase>((value) => typeof value === 'string' && isPhase(value), {
|
|
50
|
+
message: 'not a phase: expected a core phase or "verify:<step>"',
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// ── Findings (SPEC §15.4) ────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
export const severitySchema = z.enum(['blocking', 'important', 'minor'])
|
|
56
|
+
export type Severity = z.infer<typeof severitySchema>
|
|
57
|
+
|
|
58
|
+
export const findingSchema = z.strictObject({
|
|
59
|
+
/** Kernel-assigned at deposit, stable for the build (e.g. `f_3a91`). */
|
|
60
|
+
id: z.string().min(1),
|
|
61
|
+
severity: severitySchema,
|
|
62
|
+
file: z.string().optional(),
|
|
63
|
+
lines: z.array(z.number().int()).optional(),
|
|
64
|
+
summary: z.string().min(1),
|
|
65
|
+
detail: z.string().optional(),
|
|
66
|
+
/** Reviewer-marked: ids of earlier findings this one continues (stall input). */
|
|
67
|
+
persists: z.array(z.string()).default([]),
|
|
68
|
+
})
|
|
69
|
+
export type Finding = z.infer<typeof findingSchema>
|
|
70
|
+
|
|
71
|
+
/** What an agent submits — ids are stamped at deposit, never self-assigned. */
|
|
72
|
+
export const findingDraftSchema = findingSchema.omit({ id: true })
|
|
73
|
+
export type FindingDraft = z.infer<typeof findingDraftSchema>
|
|
74
|
+
|
|
75
|
+
// ── Verdicts (SPEC §4, §10) ──────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/** Review-phase vocabulary (`plan-review`, `code-review`). */
|
|
78
|
+
export const reviewVerdictKindSchema = z.enum(['approve', 'revise', 'escalate'])
|
|
79
|
+
export type ReviewVerdictKind = z.infer<typeof reviewVerdictKindSchema>
|
|
80
|
+
|
|
81
|
+
/** Durable outcome of any `verify.completed` fact. */
|
|
82
|
+
export const verifyOutcomeSchema = z.enum(['pass', 'fail', 'skipped'])
|
|
83
|
+
export type VerifyOutcome = z.infer<typeof verifyOutcomeSchema>
|
|
84
|
+
|
|
85
|
+
/** Agent-verify CLI vocabulary (`verify:*` steps of kind "agent"). */
|
|
86
|
+
export const verifyVerdictKindSchema = z.enum(['pass', 'fail', 'skip'])
|
|
87
|
+
export type VerifyVerdictKind = z.infer<typeof verifyVerdictKindSchema>
|
|
88
|
+
|
|
89
|
+
/** Structured outcome of a review, as consumed by `converge` (SPEC §10). */
|
|
90
|
+
export type Verdict =
|
|
91
|
+
| { verdict: 'approve' }
|
|
92
|
+
| { verdict: 'revise'; findings: Finding[] }
|
|
93
|
+
| { verdict: 'escalate'; reason: string }
|
|
94
|
+
|
|
95
|
+
// ── Observations (SPEC §4, §12) ──────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
export const observationKindSchema = z.enum(['followup', 'refactor', 'latent-bug'])
|
|
98
|
+
export type ObservationKind = z.infer<typeof observationKindSchema>
|
|
99
|
+
|
|
100
|
+
// ── Escalations (SPEC §11, §15.3) ────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
export const escalationSourceSchema = z.enum(['agent', 'stall', 'policy'])
|
|
103
|
+
export type EscalationSource = z.infer<typeof escalationSourceSchema>
|
|
104
|
+
|
|
105
|
+
/** Closed identity for every policy-authored escalation condition. `round`
|
|
106
|
+
* scopes an occurrence; it does not identify which policy guard raised it. */
|
|
107
|
+
export const policyEscalationCauseSchema = z.enum([
|
|
108
|
+
'review-round-limit',
|
|
109
|
+
'verify-failure-limit',
|
|
110
|
+
'reconcile-no-progress',
|
|
111
|
+
'setup-failure-limit',
|
|
112
|
+
'infrastructure-failure-limit',
|
|
113
|
+
'phase-attempt-limit',
|
|
114
|
+
'non-retryable-phase-failure',
|
|
115
|
+
])
|
|
116
|
+
export type PolicyEscalationCause = z.infer<typeof policyEscalationCauseSchema>
|
|
117
|
+
|
|
118
|
+
/** Setup may block a build through the escalation protocol without becoming a
|
|
119
|
+
* pipeline phase. Keeping this target separate preserves the fixed phase
|
|
120
|
+
* grammar while allowing the ordinary answer/retry machinery to re-arm setup. */
|
|
121
|
+
export type EscalationTarget = Phase | 'setup'
|
|
122
|
+
export const escalationTargetSchema = z.custom<EscalationTarget>(
|
|
123
|
+
(value) => value === 'setup' || (typeof value === 'string' && isPhase(value)),
|
|
124
|
+
{ message: 'not an escalation target: expected "setup", a core phase, or "verify:<step>"' },
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
export const escalationResolutionSchema = z.enum([
|
|
128
|
+
'guidance',
|
|
129
|
+
'dismiss-finding',
|
|
130
|
+
'revise-spec',
|
|
131
|
+
'abort',
|
|
132
|
+
/** Bare re-attempt with no phase guidance: human UI, or policy-only startup. */
|
|
133
|
+
'retry',
|
|
134
|
+
])
|
|
135
|
+
export type EscalationResolution = z.infer<typeof escalationResolutionSchema>
|
|
136
|
+
|
|
137
|
+
// ── Tickets ──────────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
export const ticketRefSchema = z.strictObject({
|
|
140
|
+
source: z.string().min(1),
|
|
141
|
+
id: z.string().min(1),
|
|
142
|
+
url: z.string().optional(),
|
|
143
|
+
title: z.string().optional(),
|
|
144
|
+
})
|
|
145
|
+
export type TicketRef = z.infer<typeof ticketRefSchema>
|
|
146
|
+
|
|
147
|
+
// ── Artifacts ────────────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Revisions are 0-based per kind: the first deposit of a kind is rev 0
|
|
151
|
+
* (normative in SPEC §6.3 — the spec is "kind `spec`, revision 0"), and every
|
|
152
|
+
* later deposit of the same kind is max+1.
|
|
153
|
+
*/
|
|
154
|
+
export const artifactRefSchema = z.strictObject({
|
|
155
|
+
kind: z.string().min(1),
|
|
156
|
+
rev: z.number().int().nonnegative(),
|
|
157
|
+
})
|
|
158
|
+
export type ArtifactRef = z.infer<typeof artifactRefSchema>
|
|
159
|
+
|
|
160
|
+
/** Closed set of core kinds; verify reports are per-step (open set). */
|
|
161
|
+
export const CORE_ARTIFACT_KINDS = [
|
|
162
|
+
'spec',
|
|
163
|
+
'plan',
|
|
164
|
+
'plan-review',
|
|
165
|
+
'implement-notes',
|
|
166
|
+
'code-review',
|
|
167
|
+
'pr-description',
|
|
168
|
+
'reconcile-notes',
|
|
169
|
+
'transcript',
|
|
170
|
+
] as const
|
|
171
|
+
export type CoreArtifactKind = (typeof CORE_ARTIFACT_KINDS)[number]
|
|
172
|
+
|
|
173
|
+
/** `e2e` → `verify-report:e2e` */
|
|
174
|
+
export function verifyReportKind(step: string): string {
|
|
175
|
+
return `verify-report:${step}`
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Pull-request attachments ─────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
/** Deliberately narrower than GitHub's full repository-name policy: one
|
|
181
|
+
* non-blank, whitespace-free `owner/repo` pair is enough for deterministic
|
|
182
|
+
* routing without pretending to validate a remote identifier locally. */
|
|
183
|
+
export const githubRepositorySchema = z
|
|
184
|
+
.string()
|
|
185
|
+
.regex(
|
|
186
|
+
/^[^/\s]+\/[^/\s]+$/,
|
|
187
|
+
'expected exactly one non-blank GitHub repository pair in "owner/repo" form',
|
|
188
|
+
)
|
|
189
|
+
export type GitHubRepository = z.infer<typeof githubRepositorySchema>
|
|
190
|
+
|
|
191
|
+
/** Optional, frozen destination for review-window image copies. */
|
|
192
|
+
export const prImageHostSchema = z.strictObject({
|
|
193
|
+
provider: z.literal('github-release'),
|
|
194
|
+
repository: githubRepositorySchema,
|
|
195
|
+
releaseId: z.number().int().positive(),
|
|
196
|
+
})
|
|
197
|
+
export type PrImageHostTarget = z.infer<typeof prImageHostSchema>
|
|
198
|
+
|
|
199
|
+
/** A basename safe to render and pass back to `ab artifact download`. */
|
|
200
|
+
export const prAttachmentFilenameSchema = z
|
|
201
|
+
.string()
|
|
202
|
+
.min(1)
|
|
203
|
+
.max(255)
|
|
204
|
+
.refine((value) => value.trim().length > 0, 'attachment filename must be non-blank')
|
|
205
|
+
.refine((value) => value !== '.' && value !== '..', 'attachment filename must name a file')
|
|
206
|
+
.refine(
|
|
207
|
+
(value) => !/[\\/\u0000-\u001f\u007f]/.test(value),
|
|
208
|
+
'attachment filename must be a basename without path separators or control characters',
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
/** Normalized MIME media type: lowercase type/subtype with no parameters. */
|
|
212
|
+
export const mediaTypeSchema = z
|
|
213
|
+
.string()
|
|
214
|
+
.regex(
|
|
215
|
+
/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/,
|
|
216
|
+
'expected a normalized lowercase media type without parameters',
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
/** One exact BuildStore artifact explicitly designated for PR projection. */
|
|
220
|
+
export const prAttachmentSchema = z.strictObject({
|
|
221
|
+
artifact: artifactRefSchema,
|
|
222
|
+
filename: prAttachmentFilenameSchema,
|
|
223
|
+
mediaType: mediaTypeSchema,
|
|
224
|
+
})
|
|
225
|
+
export type PrAttachment = z.infer<typeof prAttachmentSchema>
|
|
226
|
+
|
|
227
|
+
/** Durable deletion handle returned after one image attachment is hosted. */
|
|
228
|
+
export const hostedPrAttachmentAssetSchema = z.strictObject({
|
|
229
|
+
provider: z.literal('github-release'),
|
|
230
|
+
repository: githubRepositorySchema,
|
|
231
|
+
releaseId: z.number().int().positive(),
|
|
232
|
+
assetId: z.number().int().positive(),
|
|
233
|
+
url: z
|
|
234
|
+
.string()
|
|
235
|
+
.url()
|
|
236
|
+
.refine((url) => url.startsWith('https://'), 'hosted PR attachment URL must use HTTPS'),
|
|
237
|
+
})
|
|
238
|
+
export type HostedPrAttachmentAsset = z.infer<typeof hostedPrAttachmentAssetSchema>
|
|
239
|
+
|
|
240
|
+
// ── Builds ───────────────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
export const buildOutcomeSchema = z.enum(['merged', 'closed-unmerged', 'abandoned', 'discarded'])
|
|
243
|
+
export type BuildOutcome = z.infer<typeof buildOutcomeSchema>
|
|
244
|
+
|
|
245
|
+
/** Derived status — a reduction of events, never stored (SPEC §15.5). */
|
|
246
|
+
export const BUILD_STATUSES = ['queued', 'running', 'paused', 'blocked', 'done', 'aborted'] as const
|
|
247
|
+
export type BuildStatus = (typeof BUILD_STATUSES)[number]
|
|
248
|
+
|
|
249
|
+
export const commitRangeSchema = z.strictObject({
|
|
250
|
+
base: z.string().min(1),
|
|
251
|
+
head: z.string().min(1),
|
|
252
|
+
})
|
|
253
|
+
export type CommitRange = z.infer<typeof commitRangeSchema>
|
|
254
|
+
|
|
255
|
+
/** How a workspace's branch tip was selected at provision time. */
|
|
256
|
+
export const workspaceBaseSchema = z.discriminatedUnion('source', [
|
|
257
|
+
z.strictObject({
|
|
258
|
+
source: z.literal('remote'),
|
|
259
|
+
sha: z.string().min(1),
|
|
260
|
+
}),
|
|
261
|
+
z.strictObject({
|
|
262
|
+
source: z.literal('local'),
|
|
263
|
+
sha: z.string().min(1),
|
|
264
|
+
remoteError: z.string().min(1),
|
|
265
|
+
}),
|
|
266
|
+
z.strictObject({
|
|
267
|
+
source: z.literal('existing'),
|
|
268
|
+
sha: z.string().min(1),
|
|
269
|
+
}),
|
|
270
|
+
])
|
|
271
|
+
export type WorkspaceBase = z.infer<typeof workspaceBaseSchema>
|
|
272
|
+
|
|
273
|
+
// ── Feedback (phase-occurrence input — SPEC §10, §15.3 started events) ─────
|
|
274
|
+
|
|
275
|
+
export const feedbackSchema = z.union([
|
|
276
|
+
/** Findings from a revise verdict, by id (payloads carry refs, not blobs). */
|
|
277
|
+
z.strictObject({ findings: z.array(z.string()) }),
|
|
278
|
+
/** A verify failure report routed back into the code loop (SPEC §5). */
|
|
279
|
+
z.strictObject({
|
|
280
|
+
verify: z.strictObject({ step: z.string(), report: artifactRefSchema }),
|
|
281
|
+
}),
|
|
282
|
+
/** A human escalation answer fed to a producer or agent verifier as
|
|
283
|
+
* authoritative feedback (§15.6-B). */
|
|
284
|
+
z.strictObject({
|
|
285
|
+
guidance: z.strictObject({ escalation: z.string(), answer: z.string() }),
|
|
286
|
+
}),
|
|
287
|
+
])
|
|
288
|
+
export type Feedback = z.infer<typeof feedbackSchema>
|