@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,71 @@
1
+ import type { AttemptId, PoolName } from './types/core.ts';
2
+ import type { AnyNewFact, RowWrite, StoreChange } from './types/ports.ts';
3
+
4
+ export interface TakenSlot {
5
+ readonly holder: AttemptId;
6
+ readonly pool: PoolName;
7
+ readonly slot: number;
8
+ readonly gen: number;
9
+ }
10
+
11
+ export interface ChangeAssignments {
12
+ readonly taken: TakenSlot | null;
13
+
14
+ // 0 when the store holds no lever
15
+ readonly lastLeverSeq: number;
16
+ }
17
+
18
+ export function buildAppliedChange(
19
+ change: StoreChange,
20
+ assignments: ChangeAssignments,
21
+ ): Pick<StoreChange, 'writes' | 'facts'> {
22
+ const leverSeqs: number[] = [];
23
+
24
+ const writes = change.writes.map((write): RowWrite => {
25
+ if (write.op !== 'insert') {
26
+ return write;
27
+ }
28
+
29
+ if (write.table === 'levers') {
30
+ const seq = assignments.lastLeverSeq + leverSeqs.length + 1;
31
+
32
+ leverSeqs.push(seq);
33
+
34
+ return { ...write, row: { ...write.row, seq } };
35
+ }
36
+
37
+ if (write.table === 'attempts' && write.row.id === assignments.taken?.holder) {
38
+ const taken = assignments.taken;
39
+
40
+ return { ...write, row: { ...write.row, slot: taken.slot, slotGen: taken.gen } };
41
+ }
42
+
43
+ return write;
44
+ });
45
+
46
+ let leverFacts = 0;
47
+
48
+ const facts = change.facts.map((fact): AnyNewFact => {
49
+ if (fact.type === 'boxxy.lever.recorded') {
50
+ const seq = leverSeqs[leverFacts];
51
+
52
+ leverFacts += 1;
53
+
54
+ return seq === undefined ? fact : { ...fact, payload: { ...fact.payload, seq } };
55
+ }
56
+
57
+ if (
58
+ fact.type === 'boxxy.attempt.claimed' &&
59
+ assignments.taken !== null &&
60
+ fact.entity === `boxxy:attempt:${assignments.taken.holder}`
61
+ ) {
62
+ const taken = assignments.taken;
63
+
64
+ return { ...fact, payload: { ...fact.payload, slot: taken.slot, gen: taken.gen } };
65
+ }
66
+
67
+ return fact;
68
+ });
69
+
70
+ return { writes, facts };
71
+ }
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+ import { brandSchemas } from './brand-schemas.ts';
3
+ import { frictionSchema } from './friction-schema.ts';
4
+ import type { BuildCloseOut } from './types/facts.ts';
5
+
6
+ const reasonSchema = z.string().min(1);
7
+ const builtSchema = z.object({ kind: z.literal('built') });
8
+ const blockedSchema = z.object({ kind: z.literal('blocked'), reason: reasonSchema });
9
+ const abandonedSchema = z.object({ kind: z.literal('abandoned'), reason: reasonSchema });
10
+
11
+ const citedEvidenceSchema = z.object({
12
+ criterion: brandSchemas.criterionId,
13
+ evidence: brandSchemas.factId,
14
+ });
15
+
16
+ const openItemSchema = z.object({
17
+ kind: z.literal('core.issue'),
18
+ title: z.string().min(1),
19
+ body: z.string(),
20
+ });
21
+
22
+ export const buildCloseOutSchema: z.ZodType<BuildCloseOut> = z.object({
23
+ result: z.discriminatedUnion('kind', [builtSchema, blockedSchema, abandonedSchema]),
24
+ criteria: z.array(citedEvidenceSchema),
25
+ openItems: z.array(openItemSchema),
26
+ friction: z.array(frictionSchema),
27
+ frictionDropped: z.number().int().nonnegative(),
28
+ });
@@ -0,0 +1,128 @@
1
+ import { deriveItemState } from './derive-item-state.ts';
2
+ import { liveLevers } from './live-levers.ts';
3
+ import type {
4
+ AttemptId,
5
+ AttemptRow,
6
+ Instant,
7
+ ItemId,
8
+ ItemKindName,
9
+ ItemRow,
10
+ ItemRows,
11
+ ItemSetRows,
12
+ ItemState,
13
+ ItemWithLevers,
14
+ LeaseRow,
15
+ LeverRow,
16
+ LinkRow,
17
+ MaxAttemptsByKind,
18
+ PullRequestRow,
19
+ } from './types/core.ts';
20
+
21
+ export function buildItemViews(
22
+ rows: ItemSetRows,
23
+ now: Instant,
24
+ maxAttemptsByKind: MaxAttemptsByKind,
25
+ ): ItemWithLevers[] {
26
+ const groups = collectItemGroups(rows, now);
27
+
28
+ // A blocking source counts only when it is done or archived, and no link makes an item either,
29
+ // so each source's state derived without its links settles its targets.
30
+ const ownStates = new Map<ItemId, ItemState>(
31
+ rows.items.map((item) => [
32
+ item.id,
33
+ deriveItemState(
34
+ item,
35
+ buildItemRows(item, groups.get(item.id), maxAttemptsByKind, {
36
+ links: [],
37
+ blockerStates: new Map(),
38
+ }),
39
+ ).state,
40
+ ]),
41
+ );
42
+
43
+ return rows.items.map((item) => {
44
+ const group = groups.get(item.id);
45
+
46
+ const itemRows = buildItemRows(item, group, maxAttemptsByKind, {
47
+ links: group?.links ?? [],
48
+ blockerStates: ownStates,
49
+ });
50
+
51
+ return { ...item, derived: deriveItemState(item, itemRows), levers: group?.levers ?? [] };
52
+ });
53
+ }
54
+
55
+ interface ItemGroup {
56
+ readonly links: LinkRow[];
57
+ readonly attempts: AttemptRow[];
58
+ readonly pullRequests: PullRequestRow[];
59
+ levers: LeverRow[];
60
+ liveLease: LeaseRow | null;
61
+ }
62
+
63
+ function collectItemGroups(rows: ItemSetRows, now: Instant): Map<ItemId, ItemGroup> {
64
+ const groups = new Map<ItemId, ItemGroup>(
65
+ rows.items.map((item) => [
66
+ item.id,
67
+ { links: [], attempts: [], pullRequests: [], levers: [], liveLease: null },
68
+ ]),
69
+ );
70
+
71
+ const attemptItems = new Map<AttemptId, ItemId>();
72
+
73
+ for (const link of rows.links) {
74
+ groups.get(link.target)?.links.push(link);
75
+ }
76
+
77
+ for (const attempt of rows.attempts) {
78
+ attemptItems.set(attempt.id, attempt.item);
79
+ groups.get(attempt.item)?.attempts.push(attempt);
80
+ }
81
+
82
+ for (const pullRequest of rows.pullRequests) {
83
+ const item = attemptItems.get(pullRequest.attempt);
84
+
85
+ if (item !== undefined) {
86
+ groups.get(item)?.pullRequests.push(pullRequest);
87
+ }
88
+ }
89
+
90
+ for (const lease of rows.liveLeases) {
91
+ const group = groups.get(lease.item);
92
+
93
+ if (group !== undefined) {
94
+ group.liveLease = lease;
95
+ }
96
+ }
97
+
98
+ for (const [item, group] of groups) {
99
+ group.levers = liveLevers(
100
+ rows.levers.filter((lever) => lever.item === item),
101
+ now,
102
+ );
103
+ }
104
+
105
+ return groups;
106
+ }
107
+
108
+ function buildItemRows(
109
+ item: ItemRow,
110
+ group: ItemGroup | undefined,
111
+ maxAttemptsByKind: MaxAttemptsByKind,
112
+ blocking: Pick<ItemRows, 'links' | 'blockerStates'>,
113
+ ): ItemRows {
114
+ return {
115
+ ...blocking,
116
+ levers: group?.levers ?? [],
117
+ attempts: group?.attempts ?? [],
118
+ liveLease: group?.liveLease ?? null,
119
+ pullRequests: group?.pullRequests ?? [],
120
+ maxAttempts: pickMaxAttempts(item.kind, maxAttemptsByKind),
121
+ };
122
+ }
123
+
124
+ function pickMaxAttempts(kind: ItemKindName, maxAttemptsByKind: MaxAttemptsByKind): number {
125
+ const [first] = maxAttemptsByKind.get(kind)?.values() ?? [];
126
+
127
+ return first ?? Number.POSITIVE_INFINITY;
128
+ }
@@ -0,0 +1,6 @@
1
+ import type { Admission } from './types/planning.ts';
2
+ import type { Registered } from './types/registries.ts';
3
+
4
+ export function defineAdmission<P>(admission: Admission<P>): Registered<'admission'> {
5
+ return { kind: 'admission', name: admission.name, open: (use) => use(admission) };
6
+ }
@@ -0,0 +1,6 @@
1
+ import type { Gate } from './types/planning.ts';
2
+ import type { Registered } from './types/registries.ts';
3
+
4
+ export function defineGate<P>(gate: Gate<P>): Registered<'gate'> {
5
+ return { kind: 'gate', name: gate.name, open: (use) => use(gate) };
6
+ }
@@ -0,0 +1,6 @@
1
+ import type { Signal } from './types/planning.ts';
2
+ import type { Registered } from './types/registries.ts';
3
+
4
+ export function defineSignal<P>(signal: Signal<P>): Registered<'signal'> {
5
+ return { kind: 'signal', name: signal.name, open: (use) => use(signal) };
6
+ }
@@ -0,0 +1,76 @@
1
+ import type { AttemptRow, DerivedState, ItemRow, ItemRows } from './types/core.ts';
2
+
3
+ interface StateInput {
4
+ readonly item: ItemRow;
5
+ readonly rows: ItemRows;
6
+
7
+ // the succeeded and failed attempts that ended after the item's latest `item.retry` lever
8
+ readonly counted: readonly AttemptRow[];
9
+ }
10
+
11
+ interface StateRule {
12
+ readonly derived: DerivedState;
13
+ readonly holds: (input: StateInput) => boolean;
14
+ }
15
+
16
+ const stateRules: readonly StateRule[] = [
17
+ { derived: { state: 'archived' }, holds: (input) => input.item.closedAt !== null },
18
+ { derived: { state: 'done' }, holds: (input) => hasMergedPullRequest(input.rows) },
19
+ { derived: { state: 'running' }, holds: (input) => input.rows.liveLease !== null },
20
+ { derived: { state: 'waiting', reason: 'PR open' }, holds: hasOpenPullRequest },
21
+ { derived: { state: 'waiting', reason: 'no PR linked' }, holds: hasSucceededWithoutPullRequest },
22
+ { derived: { state: 'waiting', reason: 'attempts used up' }, holds: hasUsedUpAttempts },
23
+ { derived: { state: 'idea' }, holds: (input) => input.item.acceptance.length === 0 },
24
+ { derived: { state: 'blocked' }, holds: (input) => hasOpenBlocker(input.rows) },
25
+ ];
26
+
27
+ export function deriveItemState(item: ItemRow, rows: ItemRows): DerivedState {
28
+ const input = { item, rows, counted: collectCountedAttempts(rows) };
29
+
30
+ return stateRules.find((rule) => rule.holds(input))?.derived ?? { state: 'ready' };
31
+ }
32
+
33
+ function collectCountedAttempts(rows: ItemRows): AttemptRow[] {
34
+ const retryAt = Math.max(
35
+ Number.NEGATIVE_INFINITY,
36
+ ...rows.levers.filter((lever) => lever.kind === 'item.retry').map((lever) => lever.at),
37
+ );
38
+
39
+ return rows.attempts.filter((attempt) => attempt.endedAt !== null && attempt.endedAt > retryAt);
40
+ }
41
+
42
+ function hasMergedPullRequest(rows: ItemRows): boolean {
43
+ const succeeded = new Set(collectSucceeded(rows.attempts).map((attempt) => attempt.id));
44
+
45
+ return rows.pullRequests.some((pr) => pr.state === 'merged' && succeeded.has(pr.attempt));
46
+ }
47
+
48
+ function collectSucceeded(attempts: readonly AttemptRow[]): AttemptRow[] {
49
+ return attempts.filter((attempt) => attempt.state === 'succeeded');
50
+ }
51
+
52
+ function hasOpenPullRequest(input: StateInput): boolean {
53
+ const succeeded = new Set(collectSucceeded(input.counted).map((attempt) => attempt.id));
54
+
55
+ return input.rows.pullRequests.some((pr) => pr.state === 'open' && succeeded.has(pr.attempt));
56
+ }
57
+
58
+ function hasSucceededWithoutPullRequest(input: StateInput): boolean {
59
+ const linked = new Set(input.rows.pullRequests.map((pr) => pr.attempt));
60
+
61
+ return collectSucceeded(input.counted).some((attempt) => !linked.has(attempt.id));
62
+ }
63
+
64
+ function hasUsedUpAttempts(input: StateInput): boolean {
65
+ const failed = input.counted.filter((attempt) => attempt.state === 'failed');
66
+
67
+ return failed.length >= input.rows.maxAttempts;
68
+ }
69
+
70
+ function hasOpenBlocker(rows: ItemRows): boolean {
71
+ return rows.links.some((link) => {
72
+ const blockerState = rows.blockerStates.get(link.source);
73
+
74
+ return link.type === 'blocks' && blockerState !== 'done' && blockerState !== 'archived';
75
+ });
76
+ }
@@ -0,0 +1,132 @@
1
+ import { z } from 'zod';
2
+ import { actorRefSchema } from './actor-ref-schema.ts';
3
+ import { brandSchemas } from './brand-schemas.ts';
4
+ import { buildCloseOutSchema } from './build-close-out-schema.ts';
5
+ import { frictionSchema } from './friction-schema.ts';
6
+ import type { FactType, SliceOneFacts } from './types/facts.ts';
7
+
8
+ const criterionSchema = z.object({
9
+ id: brandSchemas.criterionId,
10
+ text: z.string().min(1),
11
+ evidence: z.array(brandSchemas.factId),
12
+ });
13
+
14
+ const linkPayloadSchema = z.object({
15
+ source: brandSchemas.itemId,
16
+ target: brandSchemas.itemId,
17
+ type: brandSchemas.linkTypeName,
18
+ });
19
+
20
+ const faultSchema = z.object({
21
+ plugin: z.string().min(1),
22
+ kind: z.enum(['signal', 'gate', 'waiver', 'admission', 'propagation', 'filter-field']),
23
+ item: brandSchemas.itemId.nullable(),
24
+ error: z.string(),
25
+ });
26
+
27
+ const copiedEvidenceSchema = z.object({
28
+ criterion: brandSchemas.criterionId,
29
+ evidence: z.array(brandSchemas.factId),
30
+ });
31
+
32
+ const pullRequestNumberSchema = z.number().int().positive();
33
+
34
+ export const factSchemas: { readonly [T in FactType]: z.ZodType<SliceOneFacts[T]> } = {
35
+ 'boxxy.item.filed': z.object({
36
+ id: brandSchemas.itemId,
37
+ kind: brandSchemas.itemKindName,
38
+ repo: brandSchemas.repoName,
39
+ title: z.string().min(1),
40
+ outcome: z.string().min(1),
41
+ acceptance: z.array(criterionSchema),
42
+ foundInAttempt: brandSchemas.attemptId.nullable(),
43
+ filedBy: actorRefSchema,
44
+ filedAt: brandSchemas.instant,
45
+ closedAt: brandSchemas.instant.nullable(),
46
+ closeReason: z.string().nullable(),
47
+ }),
48
+ 'boxxy.item.edited': z
49
+ .object({
50
+ title: z.string().min(1).exactOptional(),
51
+ outcome: z.string().min(1).exactOptional(),
52
+ acceptance: z.array(criterionSchema).exactOptional(),
53
+ })
54
+ .refine((fields) => Object.keys(fields).length > 0, 'an edit changes at least one field'),
55
+ 'boxxy.item.closed': z.object({ reason: z.string().min(1) }),
56
+ 'boxxy.link.added': linkPayloadSchema,
57
+ 'boxxy.link.removed': linkPayloadSchema,
58
+ 'boxxy.lever.recorded': z.object({
59
+ seq: z.number().int().nonnegative(),
60
+ kind: z.templateLiteral([z.string().min(1), '.', z.string().min(1)]),
61
+ item: brandSchemas.itemId,
62
+ until: brandSchemas.instant.nullable(),
63
+ expiresAt: brandSchemas.instant.nullable(),
64
+ reason: z.string().nullable(),
65
+ actor: actorRefSchema,
66
+ at: brandSchemas.instant,
67
+ }),
68
+ 'boxxy.attempt.claimed': z.object({
69
+ item: brandSchemas.itemId,
70
+ phase: brandSchemas.phaseName,
71
+ number: z.number().int().positive(),
72
+ pool: brandSchemas.poolName,
73
+ slot: z.number().int().nonnegative(),
74
+ gen: z.number().int().nonnegative(),
75
+ recipeHash: brandSchemas.recipeHash,
76
+ rulesVersion: brandSchemas.rulesVersion,
77
+ }),
78
+ 'boxxy.decision.recorded': z.object({
79
+ id: brandSchemas.decisionId,
80
+ attempt: brandSchemas.attemptId,
81
+ item: brandSchemas.itemId,
82
+ pool: brandSchemas.poolName,
83
+ rulesVersion: brandSchemas.rulesVersion,
84
+ snapshotHash: brandSchemas.snapshotHash,
85
+ storeVersion: z.number().int().nonnegative(),
86
+ pepysOffsets: z.record(brandSchemas.streamName, z.number().int().nonnegative()),
87
+ faults: z.array(faultSchema),
88
+ at: brandSchemas.instant,
89
+ }),
90
+ 'boxxy.attempt.started': z.object({ atcSession: brandSchemas.atcSessionId }),
91
+ 'boxxy.attempt.reconciling': z.object({ reason: z.string().min(1) }),
92
+ 'boxxy.attempt.reconciled': z.object({ atcSession: brandSchemas.atcSessionId }),
93
+ 'boxxy.attempt.session_bound': z.object({
94
+ atcSession: brandSchemas.atcSessionId,
95
+ agentSession: brandSchemas.agentSessionId,
96
+ seenAt: brandSchemas.instant,
97
+ }),
98
+ 'boxxy.attempt.closed_out': z.object({ closeOut: buildCloseOutSchema }),
99
+ 'boxxy.attempt.settled': z.object({
100
+ state: z.enum(['succeeded', 'failed']),
101
+ failure: z.string().min(1).nullable(),
102
+ endedAt: brandSchemas.instant,
103
+ criteria: z.array(copiedEvidenceSchema),
104
+ matched: z.array(brandSchemas.itemId),
105
+ frictionDropped: z.number().int().nonnegative(),
106
+ }),
107
+ 'boxxy.evidence.recorded': z.object({
108
+ kind: z.enum(['pr', 'commit', 'test', 'ci-run', 'live-check']),
109
+ ref: z.string().min(1),
110
+ observed: z.string().nullable(),
111
+ }),
112
+ 'boxxy.evidence.pr_linked': z.object({
113
+ repo: brandSchemas.repoName,
114
+ number: pullRequestNumberSchema,
115
+ url: z.url(),
116
+ }),
117
+ 'boxxy.evidence.commit_linked': z.object({
118
+ repo: brandSchemas.repoName,
119
+ sha: brandSchemas.commitSha,
120
+ pr: pullRequestNumberSchema,
121
+ }),
122
+ 'boxxy.pr.merged': z.object({
123
+ repo: brandSchemas.repoName,
124
+ number: pullRequestNumberSchema,
125
+ mergedAt: brandSchemas.instant,
126
+ mergeCommit: brandSchemas.commitSha,
127
+ }),
128
+ 'boxxy.pr.closed': z.object({ repo: brandSchemas.repoName, number: pullRequestNumberSchema }),
129
+ 'boxxy.proposal.recorded': frictionSchema,
130
+ 'boxxy.runner.paused': z.object({ runner: brandSchemas.runnerId }),
131
+ 'boxxy.runner.resumed': z.object({ runner: brandSchemas.runnerId }),
132
+ };
@@ -0,0 +1,12 @@
1
+ import invariant from 'tiny-invariant';
2
+ import type { RowWrite } from './types/ports.ts';
3
+
4
+ export type SlotTake = Extract<RowWrite, { op: 'take-slot' }>;
5
+
6
+ export function findSlotTake(writes: readonly RowWrite[]): SlotTake | null {
7
+ const takes = writes.filter((write): write is SlotTake => write.op === 'take-slot');
8
+
9
+ invariant(takes.length <= 1, 'a change takes one slot at most');
10
+
11
+ return takes.at(0) ?? null;
12
+ }
@@ -0,0 +1,19 @@
1
+ export function formatKeyPath(segments: readonly PropertyKey[]): string {
2
+ return segments.map((segment, index) => formatSegment(segment, index === 0)).join('');
3
+ }
4
+
5
+ const plainKeyPattern = /^[A-Za-z_][\w-]*$/u;
6
+
7
+ function formatSegment(segment: PropertyKey, isFirst: boolean): string {
8
+ if (typeof segment === 'number') {
9
+ return `[${segment}]`;
10
+ }
11
+
12
+ const key = String(segment);
13
+
14
+ if (!plainKeyPattern.test(key)) {
15
+ return `[${JSON.stringify(key)}]`;
16
+ }
17
+
18
+ return isFirst ? key : `.${key}`;
19
+ }
@@ -0,0 +1,10 @@
1
+ import { z } from 'zod';
2
+ import type { Friction } from './types/facts.ts';
3
+
4
+ export const frictionSchema: z.ZodType<Friction> = z.object({
5
+ target: z.object({
6
+ kind: z.enum(['skill', 'rule', 'lint', 'agents-md', 'tool']),
7
+ name: z.string().min(1),
8
+ }),
9
+ change: z.string().min(1),
10
+ });
package/src/index.ts CHANGED
@@ -1 +1,196 @@
1
- export { packageName } from './package-name.ts';
1
+ export type { ContentHash, JsonValue } from '@boxxy/content-hash';
2
+ export { brandSchemas } from './brand-schemas.ts';
3
+
4
+ export {
5
+ buildAppliedChange,
6
+ type ChangeAssignments,
7
+ type TakenSlot,
8
+ } from './build-applied-change.ts';
9
+
10
+ export { buildCloseOutSchema } from './build-close-out-schema.ts';
11
+ export { buildItemViews } from './build-item-views.ts';
12
+ export { defineAdmission } from './define-admission.ts';
13
+ export { defineGate } from './define-gate.ts';
14
+ export { defineSignal } from './define-signal.ts';
15
+ export { deriveItemState } from './derive-item-state.ts';
16
+ export { factSchemas } from './fact-schemas.ts';
17
+ export { findSlotTake, type SlotTake } from './find-slot-take.ts';
18
+ export { formatKeyPath } from './format-key-path.ts';
19
+ export { frictionSchema } from './friction-schema.ts';
20
+ export { liveLevers } from './live-levers.ts';
21
+ export { outboxFactSchema } from './outbox-fact-schema.ts';
22
+ export { rowSchemas } from './row-schemas.ts';
23
+ export { signalKey } from './signal-key.ts';
24
+ export { toJSONValue } from './to-json-value.ts';
25
+
26
+ export type {
27
+ Actor,
28
+ ActorRef,
29
+ AgentSessionId,
30
+ AtcSessionId,
31
+ AttemptId,
32
+ AttemptRow,
33
+ AttemptSessionRow,
34
+ AttemptState,
35
+ CommitSha,
36
+ ConceptPin,
37
+ Criterion,
38
+ CriterionId,
39
+ DecisionId,
40
+ DecisionRow,
41
+ DerivedState,
42
+ Effort,
43
+ EvidenceKind,
44
+ EvidenceRow,
45
+ EvidenceSource,
46
+ FactId,
47
+ FaultRecord,
48
+ ForcedFailure,
49
+ Instant,
50
+ ItemId,
51
+ ItemKindName,
52
+ ItemRow,
53
+ ItemRows,
54
+ ItemSetRows,
55
+ ItemState,
56
+ ItemView,
57
+ ItemWithLevers,
58
+ KnownFailure,
59
+ LeaseRow,
60
+ LeverKind,
61
+ LeverRow,
62
+ LinkRow,
63
+ LinkTypeName,
64
+ LiveAttemptState,
65
+ LoadedRecipe,
66
+ MaxAttemptsByKind,
67
+ Minutes,
68
+ ObservationBoundary,
69
+ PhaseName,
70
+ PipelineName,
71
+ PoolName,
72
+ PoolSlotRow,
73
+ PullRequestRef,
74
+ PullRequestRow,
75
+ Recipe,
76
+ RecipeHash,
77
+ RecipeMaterial,
78
+ RecipeName,
79
+ RepoName,
80
+ ResolvedRecipe,
81
+ RulesVersion,
82
+ RunnerId,
83
+ RunnerRow,
84
+ SnapshotHash,
85
+ SourceName,
86
+ StreamName,
87
+ WaitingReason,
88
+ WorkerKindName,
89
+ } from './types/core.ts';
90
+
91
+ export type {
92
+ Claim,
93
+ CloseOutContext,
94
+ CloseOutParse,
95
+ CloseOutParser,
96
+ CloseOutProblem,
97
+ ReconcileAction,
98
+ ReconcileTable,
99
+ SettleInput,
100
+ Settlement,
101
+ SettleRule,
102
+ SpawnContext,
103
+ } from './types/dispatch.ts';
104
+
105
+ export type {
106
+ BuildCloseOut,
107
+ EntityRef,
108
+ FactEnvelope,
109
+ FactType,
110
+ Friction,
111
+ FrictionTargetKind,
112
+ LinkPayload,
113
+ NewFact,
114
+ OpenItem,
115
+ OutboxFact,
116
+ SliceOneFacts,
117
+ } from './types/facts.ts';
118
+
119
+ export type {
120
+ Admission,
121
+ AdmissionGrant,
122
+ AdmissionName,
123
+ Decision,
124
+ Fault,
125
+ Frame,
126
+ FrameItem,
127
+ FrameValues,
128
+ Gate,
129
+ GateName,
130
+ Hold,
131
+ Lift,
132
+ Pick,
133
+ PlanContext,
134
+ Pool,
135
+ PropagationLine,
136
+ Signal,
137
+ SignalKey,
138
+ SignalName,
139
+ Snapshot,
140
+ View,
141
+ ViewName,
142
+ Waiver,
143
+ WaiverName,
144
+ } from './types/planning.ts';
145
+
146
+ export type {
147
+ AnyNewFact,
148
+ ApplyResult,
149
+ ClaimStore,
150
+ Clock,
151
+ CostReader,
152
+ CostToLandInput,
153
+ CostToLandRow,
154
+ FactSink,
155
+ Forge,
156
+ ItemDetailRows,
157
+ ItemFilter,
158
+ NewId,
159
+ PullRequestReading,
160
+ RowWrite,
161
+ SessionFault,
162
+ SessionReading,
163
+ SpawnRequest,
164
+ StartOutcome,
165
+ StoreChange,
166
+ StoreReader,
167
+ StoreTx,
168
+ TableName,
169
+ Tables,
170
+ WorkerKind,
171
+ } from './types/ports.ts';
172
+
173
+ export type {
174
+ Concept,
175
+ ConceptEntry,
176
+ ConceptName,
177
+ FilterField,
178
+ KindRules,
179
+ LoadedRules,
180
+ LoadRejection,
181
+ LoadRejectionReason,
182
+ LoadResult,
183
+ ParamKindName,
184
+ ParamKinds,
185
+ Pipeline,
186
+ PipelineStep,
187
+ PlainKinds,
188
+ PluginKindName,
189
+ PluginRef,
190
+ Registered,
191
+ Registry,
192
+ Resolved,
193
+ RulesFile,
194
+ Schema,
195
+ SignalRef,
196
+ } from './types/registries.ts';