@boxxy/model 0.1.0 → 0.1.2

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.
Files changed (80) hide show
  1. package/README.md +27 -2
  2. package/dist/actor-ref-schema.d.ts +2 -0
  3. package/dist/brand-schemas.d.ts +26 -0
  4. package/dist/build-applied-change.d.ts +13 -0
  5. package/dist/build-close-out-schema.d.ts +3 -0
  6. package/dist/build-item-views.d.ts +2 -0
  7. package/dist/define-admission.d.ts +3 -0
  8. package/dist/define-gate.d.ts +3 -0
  9. package/dist/define-signal.d.ts +3 -0
  10. package/dist/derive-item-state.d.ts +2 -0
  11. package/dist/fact-schemas.d.ts +5 -0
  12. package/dist/find-slot-take.d.ts +5 -0
  13. package/dist/format-key-path.d.ts +1 -0
  14. package/dist/friction-schema.d.ts +3 -0
  15. package/dist/index.d.ts +24 -1
  16. package/dist/live-levers.d.ts +2 -0
  17. package/dist/outbox-fact-schema.d.ts +3 -0
  18. package/dist/row-schemas.d.ts +5 -0
  19. package/dist/signal-key.d.ts +3 -0
  20. package/dist/test-utils/build-mock-attempt-row.d.ts +2 -0
  21. package/dist/test-utils/build-mock-evidence-row.d.ts +2 -0
  22. package/dist/test-utils/build-mock-fact-id.d.ts +2 -0
  23. package/dist/test-utils/build-mock-frame-item.d.ts +2 -0
  24. package/dist/test-utils/build-mock-item-row.d.ts +2 -0
  25. package/dist/test-utils/build-mock-item-rows.d.ts +2 -0
  26. package/dist/test-utils/build-mock-item-view.d.ts +2 -0
  27. package/dist/test-utils/build-mock-lease-row.d.ts +2 -0
  28. package/dist/test-utils/build-mock-lever-row.d.ts +2 -0
  29. package/dist/test-utils/build-mock-link-row.d.ts +2 -0
  30. package/dist/test-utils/build-mock-pull-request-row.d.ts +2 -0
  31. package/dist/test-utils/build-mock-snapshot.d.ts +2 -0
  32. package/dist/test-utils/seed-faker.d.ts +1 -0
  33. package/dist/to-json-value.d.ts +2 -0
  34. package/dist/types/core.d.ts +233 -0
  35. package/dist/types/dispatch.d.ts +82 -0
  36. package/dist/types/facts.d.ts +154 -0
  37. package/dist/types/planning.d.ts +94 -0
  38. package/dist/types/ports.d.ts +218 -0
  39. package/dist/types/registries.d.ts +150 -0
  40. package/package.json +9 -2
  41. package/src/actor-ref-schema.ts +9 -0
  42. package/src/brand-schemas.ts +46 -0
  43. package/src/build-applied-change.ts +71 -0
  44. package/src/build-close-out-schema.ts +28 -0
  45. package/src/build-item-views.ts +128 -0
  46. package/src/define-admission.ts +6 -0
  47. package/src/define-gate.ts +6 -0
  48. package/src/define-signal.ts +6 -0
  49. package/src/derive-item-state.ts +76 -0
  50. package/src/fact-schemas.ts +132 -0
  51. package/src/find-slot-take.ts +12 -0
  52. package/src/format-key-path.ts +19 -0
  53. package/src/friction-schema.ts +10 -0
  54. package/src/index.ts +196 -1
  55. package/src/live-levers.ts +22 -0
  56. package/src/outbox-fact-schema.ts +25 -0
  57. package/src/row-schemas.ts +89 -0
  58. package/src/signal-key.ts +7 -0
  59. package/src/test-utils/build-mock-attempt-row.ts +37 -0
  60. package/src/test-utils/build-mock-evidence-row.ts +17 -0
  61. package/src/test-utils/build-mock-fact-id.ts +7 -0
  62. package/src/test-utils/build-mock-frame-item.ts +9 -0
  63. package/src/test-utils/build-mock-item-row.ts +22 -0
  64. package/src/test-utils/build-mock-item-rows.ts +14 -0
  65. package/src/test-utils/build-mock-item-view.ts +8 -0
  66. package/src/test-utils/build-mock-lease-row.ts +15 -0
  67. package/src/test-utils/build-mock-lever-row.ts +17 -0
  68. package/src/test-utils/build-mock-link-row.ts +15 -0
  69. package/src/test-utils/build-mock-pull-request-row.ts +16 -0
  70. package/src/test-utils/build-mock-snapshot.ts +5 -0
  71. package/src/test-utils/seed-faker.ts +3 -0
  72. package/src/to-json-value.ts +25 -0
  73. package/src/types/core.ts +299 -0
  74. package/src/types/dispatch.ts +95 -0
  75. package/src/types/facts.ts +166 -0
  76. package/src/types/planning.ts +136 -0
  77. package/src/types/ports.ts +262 -0
  78. package/src/types/registries.ts +185 -0
  79. package/dist/package-name.d.ts +0 -1
  80. package/src/package-name.ts +0 -1
@@ -0,0 +1,22 @@
1
+ import type { Instant, LeverKind, LeverRow } from './types/core.ts';
2
+
3
+ export function liveLevers(levers: readonly LeverRow[], now: Instant): LeverRow[] {
4
+ return levers.filter((lever) => !isExpired(lever, now) && !isUndone(lever, levers));
5
+ }
6
+
7
+ function isExpired(lever: LeverRow, now: Instant): boolean {
8
+ return lever.expiresAt !== null && lever.expiresAt <= now;
9
+ }
10
+
11
+ const undoKinds: ReadonlyMap<LeverKind, LeverKind> = new Map([
12
+ ['plan.defer', 'plan.undefer'],
13
+ ['plan.expedite', 'plan.unexpedite'],
14
+ ]);
15
+
16
+ function isUndone(lever: LeverRow, levers: readonly LeverRow[]): boolean {
17
+ const undoKind = undoKinds.get(lever.kind);
18
+
19
+ return levers.some(
20
+ (later) => later.item === lever.item && later.kind === undoKind && later.seq > lever.seq,
21
+ );
22
+ }
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod';
2
+ import { actorRefSchema } from './actor-ref-schema.ts';
3
+ import { brandSchemas } from './brand-schemas.ts';
4
+ import type { OutboxFact } from './types/facts.ts';
5
+
6
+ const idSchema = z.string();
7
+
8
+ const entityRefSchema = z.union([
9
+ z.templateLiteral(['boxxy:item:', idSchema]),
10
+ z.templateLiteral(['boxxy:link:', idSchema, ':', idSchema, ':', idSchema]),
11
+ z.templateLiteral(['boxxy:attempt:', idSchema]),
12
+ z.templateLiteral(['boxxy:decision:', idSchema]),
13
+ z.templateLiteral(['boxxy:runner:', idSchema]),
14
+ ]);
15
+
16
+ export const outboxFactSchema: z.ZodType<OutboxFact> = z.object({
17
+ seq: z.number().int().positive(),
18
+ factId: brandSchemas.factId,
19
+ type: z.string().min(1),
20
+ entity: entityRefSchema,
21
+ actor: actorRefSchema,
22
+ at: brandSchemas.instant,
23
+ payload: z.json(),
24
+ sentAt: brandSchemas.instant.nullable(),
25
+ });
@@ -0,0 +1,89 @@
1
+ import { contentHashSchema } from '@boxxy/content-hash';
2
+ import { z } from 'zod';
3
+ import { actorRefSchema } from './actor-ref-schema.ts';
4
+ import { brandSchemas } from './brand-schemas.ts';
5
+ import { buildCloseOutSchema } from './build-close-out-schema.ts';
6
+ import { factSchemas } from './fact-schemas.ts';
7
+ import type { TableName, Tables } from './types/ports.ts';
8
+
9
+ const countSchema = z.number().int().nonnegative();
10
+
11
+ export const rowSchemas: { readonly [T in TableName]: z.ZodType<Tables[T]> } = {
12
+ items: factSchemas['boxxy.item.filed'],
13
+ links: z.object({
14
+ source: brandSchemas.itemId,
15
+ target: brandSchemas.itemId,
16
+ type: brandSchemas.linkTypeName,
17
+ addedBy: actorRefSchema,
18
+ addedAt: brandSchemas.instant,
19
+ removedAt: brandSchemas.instant.nullable(),
20
+ }),
21
+ levers: factSchemas['boxxy.lever.recorded'],
22
+ attempts: z.object({
23
+ id: brandSchemas.attemptId,
24
+ item: brandSchemas.itemId,
25
+ phase: brandSchemas.phaseName,
26
+ number: z.number().int().positive(),
27
+ runner: brandSchemas.runnerId,
28
+ recipeHash: brandSchemas.recipeHash,
29
+ rulesVersion: brandSchemas.rulesVersion,
30
+ pool: brandSchemas.poolName,
31
+ slot: countSchema,
32
+ slotGen: countSchema,
33
+ leaseGen: countSchema,
34
+ spawnRequest: z.json(),
35
+ spawnRequestHash: contentHashSchema,
36
+ state: z.enum(['starting', 'running', 'reconciling', 'succeeded', 'failed']),
37
+ failure: z.string().nullable(),
38
+ atcSession: brandSchemas.atcSessionId.nullable(),
39
+ closeOut: buildCloseOutSchema.nullable(),
40
+ claimedAt: brandSchemas.instant,
41
+ startedAt: brandSchemas.instant.nullable(),
42
+ endedAt: brandSchemas.instant.nullable(),
43
+ }),
44
+ attempt_sessions: z.object({
45
+ attempt: brandSchemas.attemptId,
46
+ agentSession: brandSchemas.agentSessionId,
47
+ atcSession: brandSchemas.atcSessionId,
48
+ seenAt: brandSchemas.instant,
49
+ }),
50
+ leases: z.object({
51
+ item: brandSchemas.itemId,
52
+ phase: brandSchemas.phaseName,
53
+ attempt: brandSchemas.attemptId,
54
+ gen: countSchema,
55
+ takenAt: brandSchemas.instant,
56
+ releasedAt: brandSchemas.instant.nullable(),
57
+ }),
58
+ pool_slots: z.object({
59
+ pool: brandSchemas.poolName,
60
+ slot: countSchema,
61
+ holder: brandSchemas.attemptId.nullable(),
62
+ gen: countSchema,
63
+ retired: z.boolean(),
64
+ }),
65
+ decisions: factSchemas['boxxy.decision.recorded'],
66
+ evidence: z.object({
67
+ factId: brandSchemas.factId,
68
+ attempt: brandSchemas.attemptId,
69
+ kind: z.enum(['pr', 'commit', 'test', 'ci-run', 'live-check']),
70
+ ref: z.string().min(1),
71
+ observed: z.string().nullable(),
72
+ source: z.enum(['hook', 'trailer', 'agent', 'watch']),
73
+ at: brandSchemas.instant,
74
+ }),
75
+ pull_requests: z.object({
76
+ repo: brandSchemas.repoName,
77
+ number: z.number().int().positive(),
78
+ attempt: brandSchemas.attemptId,
79
+ state: z.enum(['open', 'closed', 'merged']),
80
+ mergedAt: brandSchemas.instant.nullable(),
81
+ mergeCommit: brandSchemas.commitSha.nullable(),
82
+ checkedAt: brandSchemas.instant,
83
+ }),
84
+ runners: z.object({
85
+ id: brandSchemas.runnerId,
86
+ heartbeatAt: brandSchemas.instant,
87
+ paused: z.boolean(),
88
+ }),
89
+ };
@@ -0,0 +1,7 @@
1
+ import type { JsonValue } from '@boxxy/content-hash';
2
+ import { contentHash } from '@boxxy/content-hash';
3
+ import type { SignalKey, SignalName } from './types/planning.ts';
4
+
5
+ export function signalKey(name: SignalName, params: JsonValue): SignalKey {
6
+ return `${name}#${contentHash(params)}`;
7
+ }
@@ -0,0 +1,37 @@
1
+ import { contentHash } from '@boxxy/content-hash';
2
+ import { faker } from '@faker-js/faker';
3
+ import { brandSchemas } from '../brand-schemas.ts';
4
+ import type { AttemptRow } from '../types/core.ts';
5
+
6
+ const hashOptions = { length: 64, casing: 'lower', prefix: '' } as const;
7
+
8
+ export function buildMockAttemptRow(overrides: Partial<AttemptRow> = {}): AttemptRow {
9
+ const spawnRequest =
10
+ overrides.spawnRequest === undefined
11
+ ? { prompt: faker.lorem.sentence() }
12
+ : overrides.spawnRequest;
13
+
14
+ return {
15
+ id: brandSchemas.attemptId.parse(faker.string.uuid({ version: 7 })),
16
+ item: brandSchemas.itemId.parse(faker.string.uuid({ version: 7 })),
17
+ phase: brandSchemas.phaseName.parse('build'),
18
+ number: 1,
19
+ runner: brandSchemas.runnerId.parse('runner-1'),
20
+ recipeHash: brandSchemas.recipeHash.parse(faker.string.hexadecimal(hashOptions)),
21
+ rulesVersion: brandSchemas.rulesVersion.parse(faker.string.hexadecimal(hashOptions)),
22
+ pool: brandSchemas.poolName.parse('build'),
23
+ slot: 1,
24
+ slotGen: 1,
25
+ leaseGen: 1,
26
+ spawnRequest,
27
+ spawnRequestHash: contentHash(spawnRequest),
28
+ state: 'succeeded',
29
+ failure: null,
30
+ atcSession: brandSchemas.atcSessionId.parse(faker.string.uuid()),
31
+ closeOut: null,
32
+ claimedAt: brandSchemas.instant.parse(faker.number.int({ max: 1_800_000_000_000 })),
33
+ startedAt: brandSchemas.instant.parse(faker.number.int({ max: 1_800_000_000_000 })),
34
+ endedAt: brandSchemas.instant.parse(faker.number.int({ max: 1_800_000_000_000 })),
35
+ ...overrides,
36
+ };
37
+ }
@@ -0,0 +1,17 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { brandSchemas } from '../brand-schemas.ts';
3
+ import type { EvidenceRow } from '../types/core.ts';
4
+ import { buildMockFactId } from './build-mock-fact-id.ts';
5
+
6
+ export function buildMockEvidenceRow(overrides: Partial<EvidenceRow> = {}): EvidenceRow {
7
+ return {
8
+ factId: buildMockFactId(),
9
+ attempt: brandSchemas.attemptId.parse(faker.string.uuid({ version: 7 })),
10
+ kind: 'test',
11
+ ref: faker.system.filePath(),
12
+ observed: null,
13
+ source: 'agent',
14
+ at: brandSchemas.instant.parse(faker.number.int({ max: 1_800_000_000_000 })),
15
+ ...overrides,
16
+ };
17
+ }
@@ -0,0 +1,7 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { brandSchemas } from '../brand-schemas.ts';
3
+ import type { FactId } from '../types/core.ts';
4
+
5
+ export function buildMockFactId(): FactId {
6
+ return brandSchemas.factId.parse(`manual:${faker.string.uuid({ version: 7 })}`);
7
+ }
@@ -0,0 +1,9 @@
1
+ import { brandSchemas } from '../brand-schemas.ts';
2
+ import type { FrameItem } from '../types/planning.ts';
3
+ import { buildMockItemView } from './build-mock-item-view.ts';
4
+
5
+ export function buildMockFrameItem(overrides: Partial<FrameItem> = {}): FrameItem {
6
+ const { phase, ...view } = overrides;
7
+
8
+ return { ...buildMockItemView(view), phase: phase ?? brandSchemas.phaseName.parse('build') };
9
+ }
@@ -0,0 +1,22 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { brandSchemas } from '../brand-schemas.ts';
3
+ import type { ItemRow } from '../types/core.ts';
4
+
5
+ export function buildMockItemRow(overrides: Partial<ItemRow> = {}): ItemRow {
6
+ return {
7
+ id: brandSchemas.itemId.parse(faker.string.uuid({ version: 7 })),
8
+ kind: brandSchemas.itemKindName.parse('core.issue'),
9
+ repo: brandSchemas.repoName.parse(`${faker.string.alpha(8)}/${faker.string.alpha(8)}`),
10
+ title: faker.lorem.sentence(),
11
+ outcome: faker.lorem.sentence(),
12
+ acceptance: [
13
+ { id: brandSchemas.criterionId.parse('a1'), text: faker.lorem.sentence(), evidence: [] },
14
+ ],
15
+ foundInAttempt: null,
16
+ filedBy: 'boxxy:owner:local',
17
+ filedAt: brandSchemas.instant.parse(faker.number.int({ max: 1_800_000_000_000 })),
18
+ closedAt: null,
19
+ closeReason: null,
20
+ ...overrides,
21
+ };
22
+ }
@@ -0,0 +1,14 @@
1
+ import type { ItemRows } from '../types/core.ts';
2
+
3
+ export function buildMockItemRows(overrides: Partial<ItemRows> = {}): ItemRows {
4
+ return {
5
+ links: [],
6
+ blockerStates: new Map(),
7
+ levers: [],
8
+ attempts: [],
9
+ liveLease: null,
10
+ pullRequests: [],
11
+ maxAttempts: 2,
12
+ ...overrides,
13
+ };
14
+ }
@@ -0,0 +1,8 @@
1
+ import type { ItemView } from '../types/core.ts';
2
+ import { buildMockItemRow } from './build-mock-item-row.ts';
3
+
4
+ export function buildMockItemView(overrides: Partial<ItemView> = {}): ItemView {
5
+ const { derived, ...row } = overrides;
6
+
7
+ return { ...buildMockItemRow(row), derived: derived ?? { state: 'ready' } };
8
+ }
@@ -0,0 +1,15 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { brandSchemas } from '../brand-schemas.ts';
3
+ import type { LeaseRow } from '../types/core.ts';
4
+
5
+ export function buildMockLeaseRow(overrides: Partial<LeaseRow> = {}): LeaseRow {
6
+ return {
7
+ item: brandSchemas.itemId.parse(faker.string.uuid({ version: 7 })),
8
+ phase: brandSchemas.phaseName.parse('build'),
9
+ attempt: brandSchemas.attemptId.parse(faker.string.uuid({ version: 7 })),
10
+ gen: 1,
11
+ takenAt: brandSchemas.instant.parse(faker.number.int({ max: 1_800_000_000_000 })),
12
+ releasedAt: null,
13
+ ...overrides,
14
+ };
15
+ }
@@ -0,0 +1,17 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { brandSchemas } from '../brand-schemas.ts';
3
+ import type { LeverRow } from '../types/core.ts';
4
+
5
+ export function buildMockLeverRow(overrides: Partial<LeverRow> = {}): LeverRow {
6
+ return {
7
+ seq: faker.number.int({ min: 1, max: 10_000 }),
8
+ kind: 'plan.defer',
9
+ item: brandSchemas.itemId.parse(faker.string.uuid({ version: 7 })),
10
+ until: null,
11
+ expiresAt: null,
12
+ reason: faker.lorem.sentence(),
13
+ actor: 'boxxy:owner:local',
14
+ at: brandSchemas.instant.parse(faker.number.int({ max: 1_800_000_000_000 })),
15
+ ...overrides,
16
+ };
17
+ }
@@ -0,0 +1,15 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { brandSchemas } from '../brand-schemas.ts';
3
+ import type { LinkRow } from '../types/core.ts';
4
+
5
+ export function buildMockLinkRow(overrides: Partial<LinkRow> = {}): LinkRow {
6
+ return {
7
+ source: brandSchemas.itemId.parse(faker.string.uuid({ version: 7 })),
8
+ target: brandSchemas.itemId.parse(faker.string.uuid({ version: 7 })),
9
+ type: brandSchemas.linkTypeName.parse('blocks'),
10
+ addedBy: 'boxxy:owner:local',
11
+ addedAt: brandSchemas.instant.parse(faker.number.int({ max: 1_800_000_000_000 })),
12
+ removedAt: null,
13
+ ...overrides,
14
+ };
15
+ }
@@ -0,0 +1,16 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { brandSchemas } from '../brand-schemas.ts';
3
+ import type { PullRequestRow } from '../types/core.ts';
4
+
5
+ export function buildMockPullRequestRow(overrides: Partial<PullRequestRow> = {}): PullRequestRow {
6
+ return {
7
+ repo: brandSchemas.repoName.parse(`${faker.string.alpha(8)}/${faker.string.alpha(8)}`),
8
+ number: faker.number.int({ min: 1, max: 10_000 }),
9
+ attempt: brandSchemas.attemptId.parse(faker.string.uuid({ version: 7 })),
10
+ state: 'open',
11
+ mergedAt: null,
12
+ mergeCommit: null,
13
+ checkedAt: brandSchemas.instant.parse(faker.number.int({ max: 1_800_000_000_000 })),
14
+ ...overrides,
15
+ };
16
+ }
@@ -0,0 +1,5 @@
1
+ import type { Snapshot } from '../types/planning.ts';
2
+
3
+ export function buildMockSnapshot(overrides: Partial<Snapshot> = {}): Snapshot {
4
+ return { storeVersion: 1, items: [], links: [], levers: [], freeSlots: {}, ...overrides };
5
+ }
@@ -0,0 +1,3 @@
1
+ import { faker } from '@faker-js/faker';
2
+
3
+ faker.seed(1);
@@ -0,0 +1,25 @@
1
+ import type { JsonValue } from '@boxxy/content-hash';
2
+ import invariant from 'tiny-invariant';
3
+
4
+ export function toJSONValue(value: unknown): JsonValue {
5
+ if (
6
+ value === null ||
7
+ typeof value === 'boolean' ||
8
+ typeof value === 'number' ||
9
+ typeof value === 'string'
10
+ ) {
11
+ return value;
12
+ }
13
+
14
+ if (Array.isArray(value)) {
15
+ return value.map((entry: unknown) => toJSONValue(entry));
16
+ }
17
+
18
+ invariant(typeof value === 'object', `a ${typeof value} has no JSON form`);
19
+
20
+ return Object.fromEntries(
21
+ Object.entries(value)
22
+ .filter(([, entry]: [string, unknown]) => entry !== undefined)
23
+ .map(([key, entry]: [string, unknown]) => [key, toJSONValue(entry)]),
24
+ );
25
+ }
@@ -0,0 +1,299 @@
1
+ import type { ContentHash, JsonValue } from '@boxxy/content-hash';
2
+ import type { z } from 'zod';
3
+ import type { brandSchemas } from '../brand-schemas.ts';
4
+ import type { BuildCloseOut } from './facts.ts';
5
+
6
+ type BrandOf<K extends keyof typeof brandSchemas> = z.output<(typeof brandSchemas)[K]>;
7
+
8
+ export type Instant = BrandOf<'instant'>;
9
+
10
+ export type Minutes = number;
11
+
12
+ export type ItemId = BrandOf<'itemId'>;
13
+
14
+ export type AttemptId = BrandOf<'attemptId'>;
15
+
16
+ export type DecisionId = BrandOf<'decisionId'>;
17
+
18
+ export type FactId = BrandOf<'factId'>;
19
+
20
+ export type RunnerId = BrandOf<'runnerId'>;
21
+
22
+ export type AtcSessionId = BrandOf<'atcSessionId'>;
23
+
24
+ export type AgentSessionId = BrandOf<'agentSessionId'>;
25
+
26
+ export type CommitSha = BrandOf<'commitSha'>;
27
+
28
+ export type RepoName = BrandOf<'repoName'>;
29
+
30
+ export type CriterionId = BrandOf<'criterionId'>;
31
+
32
+ export interface PullRequestRef {
33
+ readonly repo: RepoName;
34
+ readonly number: number;
35
+ }
36
+
37
+ export type ItemKindName = BrandOf<'itemKindName'>;
38
+
39
+ export type LinkTypeName = BrandOf<'linkTypeName'>;
40
+
41
+ export type PhaseName = BrandOf<'phaseName'>;
42
+
43
+ export type PipelineName = BrandOf<'pipelineName'>;
44
+
45
+ export type PoolName = BrandOf<'poolName'>;
46
+
47
+ export type RecipeName = BrandOf<'recipeName'>;
48
+
49
+ export type WorkerKindName = BrandOf<'workerKindName'>;
50
+
51
+ export type SourceName = BrandOf<'sourceName'>;
52
+
53
+ export type StreamName = BrandOf<'streamName'>;
54
+
55
+ export type LeverKind = `${string}.${string}`;
56
+
57
+ export type RecipeHash = BrandOf<'recipeHash'>;
58
+
59
+ export type SnapshotHash = BrandOf<'snapshotHash'>;
60
+
61
+ export type RulesVersion = BrandOf<'rulesVersion'>;
62
+
63
+ export interface ConceptPin {
64
+ readonly ref: `${string}@${number}`;
65
+ readonly sourceHash: ContentHash;
66
+ }
67
+
68
+ export type Actor =
69
+ | { readonly kind: 'owner' }
70
+ | { readonly kind: 'attempt'; readonly attempt: AttemptId }
71
+ | { readonly kind: 'runner'; readonly runner: RunnerId }
72
+ | { readonly kind: 'hook'; readonly attempt: AttemptId };
73
+
74
+ export type ActorRef =
75
+ | 'boxxy:owner:local'
76
+ | `boxxy:runner:${string}`
77
+ | `boxxy:attempt:${string}`
78
+ | `boxxy:hook:${string}`;
79
+
80
+ export type EvidenceKind = 'pr' | 'commit' | 'test' | 'ci-run' | 'live-check';
81
+
82
+ export interface Criterion {
83
+ readonly id: CriterionId;
84
+ readonly text: string;
85
+ readonly evidence: readonly FactId[];
86
+ }
87
+
88
+ export interface ItemRow {
89
+ readonly id: ItemId;
90
+ readonly kind: ItemKindName;
91
+ readonly repo: RepoName;
92
+ readonly title: string;
93
+ readonly outcome: string;
94
+ readonly acceptance: readonly Criterion[];
95
+ readonly foundInAttempt: AttemptId | null;
96
+ readonly filedBy: ActorRef;
97
+ readonly filedAt: Instant;
98
+ readonly closedAt: Instant | null;
99
+ readonly closeReason: string | null;
100
+ }
101
+
102
+ export type ItemState = 'idea' | 'ready' | 'running' | 'blocked' | 'waiting' | 'done' | 'archived';
103
+
104
+ export type WaitingReason = 'PR open' | 'no PR linked' | 'attempts used up';
105
+
106
+ export type DerivedState =
107
+ | { readonly state: Exclude<ItemState, 'waiting'> }
108
+ | { readonly state: 'waiting'; readonly reason: WaitingReason };
109
+
110
+ export interface ItemView extends ItemRow {
111
+ readonly derived: DerivedState;
112
+ }
113
+
114
+ export interface ItemRows {
115
+ // live links whose target is the item
116
+ readonly links: readonly LinkRow[];
117
+ readonly blockerStates: ReadonlyMap<ItemId, ItemState>;
118
+ readonly levers: readonly LeverRow[];
119
+ readonly attempts: readonly AttemptRow[];
120
+ readonly liveLease: LeaseRow | null;
121
+ readonly pullRequests: readonly PullRequestRow[];
122
+
123
+ // the kind's `maxAttempts` for the item's current phase
124
+ readonly maxAttempts: number;
125
+ }
126
+
127
+ // the rows a store reads to derive the state of a set of items, with each blocking source item
128
+ export interface ItemSetRows {
129
+ readonly items: readonly ItemRow[];
130
+
131
+ // live links only
132
+ readonly links: readonly LinkRow[];
133
+ readonly levers: readonly LeverRow[];
134
+ readonly attempts: readonly AttemptRow[];
135
+ readonly liveLeases: readonly LeaseRow[];
136
+ readonly pullRequests: readonly PullRequestRow[];
137
+ }
138
+
139
+ export type MaxAttemptsByKind = ReadonlyMap<ItemKindName, ReadonlyMap<PhaseName, number>>;
140
+
141
+ export interface ItemWithLevers extends ItemView {
142
+ readonly levers: readonly LeverRow[];
143
+ }
144
+
145
+ export interface LinkRow {
146
+ readonly source: ItemId;
147
+ readonly target: ItemId;
148
+ readonly type: LinkTypeName;
149
+ readonly addedBy: ActorRef;
150
+ readonly addedAt: Instant;
151
+ readonly removedAt: Instant | null;
152
+ }
153
+
154
+ export interface LeverRow {
155
+ readonly seq: number;
156
+ readonly kind: LeverKind;
157
+ readonly item: ItemId;
158
+ readonly until: Instant | null;
159
+ readonly expiresAt: Instant | null;
160
+ readonly reason: string | null;
161
+ readonly actor: ActorRef;
162
+ readonly at: Instant;
163
+ }
164
+
165
+ export type AttemptState = 'starting' | 'running' | 'reconciling' | 'succeeded' | 'failed';
166
+
167
+ export type LiveAttemptState = Extract<AttemptState, 'starting' | 'running' | 'reconciling'>;
168
+
169
+ export type KnownFailure = 'spawn-refused' | 'session-lost' | 'spawn-lost' | 'closeout-missing';
170
+
171
+ export type ForcedFailure = Extract<KnownFailure, 'spawn-refused' | 'session-lost' | 'spawn-lost'>;
172
+
173
+ export interface AttemptRow {
174
+ readonly id: AttemptId;
175
+ readonly item: ItemId;
176
+ readonly phase: PhaseName;
177
+ readonly number: number;
178
+ readonly runner: RunnerId;
179
+ readonly recipeHash: RecipeHash;
180
+ readonly rulesVersion: RulesVersion;
181
+ readonly pool: PoolName;
182
+ readonly slot: number;
183
+ readonly slotGen: number;
184
+ readonly leaseGen: number;
185
+ readonly spawnRequest: JsonValue;
186
+ readonly spawnRequestHash: ContentHash;
187
+ readonly state: AttemptState;
188
+ readonly failure: string | null;
189
+ readonly atcSession: AtcSessionId | null;
190
+ readonly closeOut: BuildCloseOut | null;
191
+ readonly claimedAt: Instant;
192
+ readonly startedAt: Instant | null;
193
+ readonly endedAt: Instant | null;
194
+ }
195
+
196
+ export interface AttemptSessionRow {
197
+ readonly attempt: AttemptId;
198
+ readonly agentSession: AgentSessionId;
199
+ readonly atcSession: AtcSessionId;
200
+ readonly seenAt: Instant;
201
+ }
202
+
203
+ export interface LeaseRow {
204
+ readonly item: ItemId;
205
+ readonly phase: PhaseName;
206
+ readonly attempt: AttemptId;
207
+ readonly gen: number;
208
+ readonly takenAt: Instant;
209
+ readonly releasedAt: Instant | null;
210
+ }
211
+
212
+ export interface PoolSlotRow {
213
+ readonly pool: PoolName;
214
+ readonly slot: number;
215
+ readonly holder: AttemptId | null;
216
+ readonly gen: number;
217
+ readonly retired: boolean;
218
+ }
219
+
220
+ export interface ObservationBoundary {
221
+ readonly storeVersion: number;
222
+ readonly pepysOffsets: Readonly<Record<StreamName, number>>;
223
+ }
224
+
225
+ export interface DecisionRow {
226
+ readonly id: DecisionId;
227
+ readonly attempt: AttemptId;
228
+ readonly item: ItemId;
229
+ readonly pool: PoolName;
230
+ readonly rulesVersion: RulesVersion;
231
+ readonly snapshotHash: SnapshotHash;
232
+ readonly storeVersion: number;
233
+ readonly pepysOffsets: ObservationBoundary['pepysOffsets'];
234
+ readonly faults: readonly FaultRecord[];
235
+ readonly at: Instant;
236
+ }
237
+
238
+ export interface FaultRecord {
239
+ // the signal, gate, waiver or filter field name, or `propagation:<signal>:<link>`
240
+ readonly plugin: string;
241
+ readonly kind: 'signal' | 'gate' | 'waiver' | 'admission' | 'propagation' | 'filter-field';
242
+ readonly item: ItemId | null;
243
+ readonly error: string;
244
+ }
245
+
246
+ export type EvidenceSource = 'hook' | 'trailer' | 'agent' | 'watch';
247
+
248
+ export interface EvidenceRow {
249
+ readonly factId: FactId;
250
+ readonly attempt: AttemptId;
251
+ readonly kind: EvidenceKind;
252
+ readonly ref: string;
253
+ readonly observed: string | null;
254
+ readonly source: EvidenceSource;
255
+ readonly at: Instant;
256
+ }
257
+
258
+ export interface PullRequestRow {
259
+ readonly repo: RepoName;
260
+ readonly number: number;
261
+ readonly attempt: AttemptId;
262
+ readonly state: 'open' | 'closed' | 'merged';
263
+ readonly mergedAt: Instant | null;
264
+ readonly mergeCommit: CommitSha | null;
265
+ readonly checkedAt: Instant;
266
+ }
267
+
268
+ export interface RunnerRow {
269
+ readonly id: RunnerId;
270
+ readonly heartbeatAt: Instant;
271
+ readonly paused: boolean;
272
+ }
273
+
274
+ export type Effort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';
275
+
276
+ export interface Recipe {
277
+ readonly agent: string;
278
+ readonly model: string;
279
+ readonly effort: Effort;
280
+
281
+ // the prompt template's path in the rules checkout
282
+ readonly prompt: string;
283
+ readonly source: SourceName;
284
+ }
285
+
286
+ export interface RecipeMaterial {
287
+ readonly name: RecipeName;
288
+ readonly recipe: Recipe;
289
+ readonly promptHash: ContentHash;
290
+ readonly pluginFolderHash: ContentHash;
291
+ readonly rulesVersion: RulesVersion;
292
+ }
293
+
294
+ // the loader can't see the plugin folder, so claimNext adds its hash
295
+ export type LoadedRecipe = Omit<RecipeMaterial, 'pluginFolderHash'>;
296
+
297
+ export interface ResolvedRecipe extends RecipeMaterial {
298
+ readonly hash: RecipeHash;
299
+ }