@pmelab/gtd 27.8.0 → 27.9.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/.claude-plugin/plugin.json +1 -1
- package/README.md +9 -4
- package/dist/chunks/{wire-C12dahrH.mjs → wire-D3Rc5r22.mjs} +14 -1
- package/dist/gtd.bundle.mjs +131 -33
- package/dist/testkit.mjs +115 -34
- package/dist/wire.mjs +2 -2
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +39 -3
- package/src/cli/Cli.ts +23 -2
- package/src/cli/Program.test.ts +3 -1
- package/src/cli/Program.ts +22 -0
- package/src/edge/Edge.test.ts +209 -54
- package/src/edge/Edge.ts +121 -42
- package/src/edge/index.ts +1 -0
- package/src/replay/Replay.test.ts +32 -0
- package/src/replay/Replay.ts +3 -1
- package/src/replay/Trailers.test.ts +11 -0
- package/src/replay/Trailers.ts +3 -1
- package/src/replay/index.ts +1 -1
- package/src/wire/Slices.ts +25 -0
- package/src/wire/WireSchema.test.ts +18 -0
- package/src/wire/golden/1/slices.json +11 -0
- package/src/wire/index.ts +1 -0
package/src/edge/Edge.ts
CHANGED
|
@@ -43,7 +43,6 @@ import {
|
|
|
43
43
|
type JudgeVerdict,
|
|
44
44
|
type ReachedStep,
|
|
45
45
|
type ReplayOutcome,
|
|
46
|
-
type SliceRange,
|
|
47
46
|
type TreeView,
|
|
48
47
|
} from "../replay/index.js"
|
|
49
48
|
import type { Mark, ScopeAccess } from "../flows/index.js"
|
|
@@ -53,6 +52,7 @@ import {
|
|
|
53
52
|
UNATTRIBUTED_MODEL,
|
|
54
53
|
type ApprovalEntry,
|
|
55
54
|
type ModelCost,
|
|
55
|
+
type SliceEntry,
|
|
56
56
|
type StepAccess,
|
|
57
57
|
} from "../wire/index.js"
|
|
58
58
|
|
|
@@ -476,26 +476,21 @@ const runAtTip = (
|
|
|
476
476
|
return yield* runOf([...before, ...closing], { ...location }, tip.hash)
|
|
477
477
|
})
|
|
478
478
|
|
|
479
|
+
const runAtHead = (git: GitOperations, def: WorkflowDefinition) =>
|
|
480
|
+
Effect.gen(function* () {
|
|
481
|
+
// Cheap: just HEAD's own subject, never the commits before it.
|
|
482
|
+
return yield* runAtTip(git, def, (yield* git.subjectHistory(1, 0))[0])
|
|
483
|
+
})
|
|
484
|
+
|
|
479
485
|
/** The process HEAD closes or sits inside — `gtd summary`'s run. */
|
|
480
486
|
export const summaryRun: Effect.Effect<ProcessRun, Error, ConfigRequirements> = Effect.gen(
|
|
481
487
|
function* () {
|
|
482
488
|
const git = yield* GitService
|
|
483
489
|
const def = (yield* (yield* ConfigService).load).workflow
|
|
484
|
-
|
|
485
|
-
return yield* runAtTip(git, def, (yield* git.subjectHistory(1, 0))[0])
|
|
490
|
+
return yield* runAtHead(git, def)
|
|
486
491
|
},
|
|
487
492
|
)
|
|
488
493
|
|
|
489
|
-
/** The process whose finishing commit is `tip` — reads from `tip`'s own ancestry, so it works when no branch reaches it. */
|
|
490
|
-
export const closedRunAt = (tip: string): Effect.Effect<ProcessRun, Error, ConfigRequirements> =>
|
|
491
|
-
Effect.gen(function* () {
|
|
492
|
-
const git = yield* GitService
|
|
493
|
-
const def = (yield* (yield* ConfigService).load).workflow
|
|
494
|
-
const hash = yield* git.resolveRef(tip)
|
|
495
|
-
const subject = yield* git.lastCommitSubject(hash)
|
|
496
|
-
return yield* runAtTip(git, def, { hash, subject })
|
|
497
|
-
})
|
|
498
|
-
|
|
499
494
|
// ── Trees ───────────────────────────────────────────────────────────────────
|
|
500
495
|
|
|
501
496
|
const commitTree = (workspace: WorkspaceOps, hash: string): TreeView => {
|
|
@@ -897,7 +892,6 @@ interface Rest extends ResolvedRest {
|
|
|
897
892
|
/** Total recorded cost of the process, and its per-model breakdown. */
|
|
898
893
|
readonly cost: { readonly total: number; readonly byModel: readonly ModelCost[] }
|
|
899
894
|
readonly setup: ReplaySetup
|
|
900
|
-
readonly slices: readonly SliceRange[]
|
|
901
895
|
}
|
|
902
896
|
|
|
903
897
|
/** Per-model token totals, highest-cost first (ties broken by model name). */
|
|
@@ -1072,12 +1066,74 @@ export const restAt = (
|
|
|
1072
1066
|
byModel: costByModel(run.costEntries),
|
|
1073
1067
|
},
|
|
1074
1068
|
setup,
|
|
1075
|
-
slices: sliceRangesOf(outcome.slices, run),
|
|
1076
1069
|
}
|
|
1077
1070
|
})
|
|
1078
1071
|
|
|
1079
1072
|
export const currentRest: Effect.Effect<Rest, Error, RestRequirements> = restAt(undefined)
|
|
1080
1073
|
|
|
1074
|
+
export interface ProcessSlices {
|
|
1075
|
+
readonly slices: readonly SliceEntry[]
|
|
1076
|
+
readonly versionWarning?: string
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* The slices of the process HEAD closes or sits inside, current episode only,
|
|
1081
|
+
* stack order. Reads like `historyTrail`, never `restAt`: it narrates and
|
|
1082
|
+
* writes nothing, and returns a version warning for a closed run.
|
|
1083
|
+
*/
|
|
1084
|
+
export const processSlices: Effect.Effect<ProcessSlices, Error, RestRequirements> = Effect.gen(
|
|
1085
|
+
function* () {
|
|
1086
|
+
const git = yield* GitService
|
|
1087
|
+
const config = yield* (yield* ConfigService).load
|
|
1088
|
+
const workspace = yield* Workspace
|
|
1089
|
+
const host = yield* Host
|
|
1090
|
+
const run = yield* runAtHead(git, config.workflow)
|
|
1091
|
+
if (run.trace.length === 0) {
|
|
1092
|
+
return yield* Effect.fail(
|
|
1093
|
+
new Error("gtd slices: refused — HEAD neither closes nor sits inside a process"),
|
|
1094
|
+
)
|
|
1095
|
+
}
|
|
1096
|
+
const closing = run.closingHash
|
|
1097
|
+
let outcome: ReplayOutcome
|
|
1098
|
+
let versionWarning: string | undefined
|
|
1099
|
+
if (closing !== undefined) {
|
|
1100
|
+
const replayed = yield* replayFinishedProcess(config, run, closing, {
|
|
1101
|
+
command: "gtd slices",
|
|
1102
|
+
configReason:
|
|
1103
|
+
"HEAD's process replays under today's config; revert or stash the edit to list its slices",
|
|
1104
|
+
notEnded: (o) => slicesDivergence(run, o),
|
|
1105
|
+
})
|
|
1106
|
+
outcome = replayed.outcome
|
|
1107
|
+
versionWarning = replayed.versionWarning
|
|
1108
|
+
} else {
|
|
1109
|
+
const { setup } = yield* setupFor(config, run, workspace, host)
|
|
1110
|
+
outcome = yield* Effect.promise(() => replayFor(setup))
|
|
1111
|
+
if (outcome.kind !== "rest") return yield* Effect.fail(slicesDivergence(run, outcome))
|
|
1112
|
+
}
|
|
1113
|
+
return {
|
|
1114
|
+
slices: sliceRangesOf(outcome.slices, run).map(({ name, base, tip, open }) => ({
|
|
1115
|
+
name,
|
|
1116
|
+
base,
|
|
1117
|
+
tip,
|
|
1118
|
+
closed: closing !== undefined || !open,
|
|
1119
|
+
})),
|
|
1120
|
+
...(versionWarning !== undefined ? { versionWarning } : {}),
|
|
1121
|
+
}
|
|
1122
|
+
},
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
type EndedOutcome = Extract<ReplayOutcome, { kind: "ended" }>
|
|
1126
|
+
type UnendedOutcome = Exclude<ReplayOutcome, { kind: "ended" }>
|
|
1127
|
+
|
|
1128
|
+
/** Every `gtd slices` refusal reads `gtd slices: refused — <reason>`; replay's own `gtd: ` lead is dropped. */
|
|
1129
|
+
const slicesDivergence = (run: ProcessRun, outcome: ReplayOutcome): Error => {
|
|
1130
|
+
const reason =
|
|
1131
|
+
outcome.kind === "rest" && run.closingHash !== undefined
|
|
1132
|
+
? `${run.closingHash.slice(0, 7)} closes a process that does not end under today's config`
|
|
1133
|
+
: (replayError(outcome)?.message.replace(/^gtd: /, "") ?? "replay failed")
|
|
1134
|
+
return new Error(`gtd slices: refused — ${reason}`)
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1081
1137
|
/**
|
|
1082
1138
|
* Whether `gtd --workflow <name>` can start a process here: the workflow must
|
|
1083
1139
|
* reach a step from the tree the opening commit would capture. That step is
|
|
@@ -1231,7 +1287,7 @@ const multilineRefusal = (vars: Record<string, string>): Landing | undefined =>
|
|
|
1231
1287
|
}
|
|
1232
1288
|
|
|
1233
1289
|
const landingTarget = (
|
|
1234
|
-
outcome:
|
|
1290
|
+
outcome: ReplayOutcome,
|
|
1235
1291
|
rest: Rest,
|
|
1236
1292
|
): Effect.Effect<StateName | Landing, Error> => {
|
|
1237
1293
|
if (outcome.kind === "refused")
|
|
@@ -1442,10 +1498,14 @@ const inRepoConfigPaths = (
|
|
|
1442
1498
|
return [...paths].filter((p) => p !== "" && !p.startsWith("..") && !isAbsolute(p)).sort()
|
|
1443
1499
|
}
|
|
1444
1500
|
|
|
1445
|
-
const configRefusal = (
|
|
1501
|
+
const configRefusal = (
|
|
1502
|
+
command: string,
|
|
1503
|
+
tip: string,
|
|
1504
|
+
paths: readonly string[],
|
|
1505
|
+
reason = "A history ref replays under today's config, so it is unreadable after the config changes",
|
|
1506
|
+
): Error =>
|
|
1446
1507
|
new Error(
|
|
1447
|
-
|
|
1448
|
-
`A history ref replays under today's config, so it is unreadable after the config changes`,
|
|
1508
|
+
`${command}: refused — config differs at ${tip.slice(0, 7)}: ${paths.join(", ")}. ${reason}`,
|
|
1449
1509
|
)
|
|
1450
1510
|
|
|
1451
1511
|
const differingConfigPaths = (
|
|
@@ -1469,10 +1529,7 @@ const differingConfigPaths = (
|
|
|
1469
1529
|
return differing
|
|
1470
1530
|
})
|
|
1471
1531
|
|
|
1472
|
-
const notClosedError = (
|
|
1473
|
-
hash: string,
|
|
1474
|
-
outcome: Exclude<Awaited<ReturnType<typeof replayFor>>, { kind: "ended" }>,
|
|
1475
|
-
): Error =>
|
|
1532
|
+
const notClosedError = (hash: string, outcome: UnendedOutcome): Error =>
|
|
1476
1533
|
outcome.kind === "rest"
|
|
1477
1534
|
? new Error(`gtd history: ${hash.slice(0, 7)} does not close a process under today's config`)
|
|
1478
1535
|
: (replayError(outcome) ?? new Error("gtd history: replay failed"))
|
|
@@ -1507,30 +1564,52 @@ export const historyTrail = (tip: string): Effect.Effect<HistoryTrail, Error, Re
|
|
|
1507
1564
|
Effect.gen(function* () {
|
|
1508
1565
|
const git = yield* GitService
|
|
1509
1566
|
const config = yield* (yield* ConfigService).load
|
|
1510
|
-
const workspace = yield* Workspace
|
|
1511
|
-
const host = yield* Host
|
|
1512
1567
|
const hash = yield* git.resolveRef(tip)
|
|
1513
|
-
const
|
|
1514
|
-
|
|
1515
|
-
const
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
yield* workspace.episodeTrees(
|
|
1520
|
-
run.episode.base,
|
|
1521
|
-
run.episode.commits.map((c) => c.hash),
|
|
1522
|
-
)
|
|
1523
|
-
const outcome = yield* Effect.promise(() => replayFor(setup))
|
|
1524
|
-
if (outcome.kind !== "ended") return yield* Effect.fail(notClosedError(hash, outcome))
|
|
1525
|
-
|
|
1526
|
-
const steps = historySteps(run)
|
|
1527
|
-
const recorded = parseCommitMessage(yield* git.lastCommitMessage(hash)).version
|
|
1528
|
-
const versionWarning = versionWarningFor(recorded)
|
|
1568
|
+
const subject = yield* git.lastCommitSubject(hash)
|
|
1569
|
+
const run = yield* runAtTip(git, config.workflow, { hash, subject })
|
|
1570
|
+
const { outcome, recorded, versionWarning } = yield* replayFinishedProcess(config, run, hash, {
|
|
1571
|
+
command: "gtd history",
|
|
1572
|
+
notEnded: (o) => notClosedError(hash, o),
|
|
1573
|
+
})
|
|
1529
1574
|
return {
|
|
1530
1575
|
tip: hash,
|
|
1531
|
-
steps,
|
|
1576
|
+
steps: historySteps(run),
|
|
1532
1577
|
ended: outcome.via,
|
|
1533
1578
|
version: { recorded, running: GTD_VERSION },
|
|
1534
1579
|
...(versionWarning !== undefined ? { versionWarning } : {}),
|
|
1535
1580
|
}
|
|
1536
1581
|
})
|
|
1582
|
+
|
|
1583
|
+
interface FinishedReplay {
|
|
1584
|
+
readonly outcome: EndedOutcome
|
|
1585
|
+
readonly recorded: string | undefined
|
|
1586
|
+
readonly versionWarning?: string
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/** Refuses on config drift before replaying, so a stale config never runs the flow. */
|
|
1590
|
+
const replayFinishedProcess = (
|
|
1591
|
+
config: ConfigOperations,
|
|
1592
|
+
run: ProcessRun,
|
|
1593
|
+
hash: string,
|
|
1594
|
+
how: {
|
|
1595
|
+
readonly command: string
|
|
1596
|
+
readonly configReason?: string
|
|
1597
|
+
readonly notEnded: (outcome: UnendedOutcome) => Error
|
|
1598
|
+
},
|
|
1599
|
+
): Effect.Effect<FinishedReplay, Error, RestRequirements> =>
|
|
1600
|
+
Effect.gen(function* () {
|
|
1601
|
+
const git = yield* GitService
|
|
1602
|
+
const workspace = yield* Workspace
|
|
1603
|
+
const host = yield* Host
|
|
1604
|
+
const differing = yield* differingConfigPaths(git, workspace, host.root, config, run, hash)
|
|
1605
|
+
if (differing.length > 0)
|
|
1606
|
+
return yield* Effect.fail(configRefusal(how.command, hash, differing, how.configReason))
|
|
1607
|
+
const { setup } = yield* setupFor(config, run, workspace, host)
|
|
1608
|
+
const outcome = yield* Effect.promise(() => replayFor(setup))
|
|
1609
|
+
if (outcome.kind !== "ended") return yield* Effect.fail(how.notEnded(outcome))
|
|
1610
|
+
const recorded = parseCommitMessage(
|
|
1611
|
+
run.episode.commits.find((c) => c.hash === hash)?.message ?? "",
|
|
1612
|
+
).version
|
|
1613
|
+
const versionWarning = versionWarningFor(recorded)
|
|
1614
|
+
return { outcome, recorded, ...(versionWarning !== undefined ? { versionWarning } : {}) }
|
|
1615
|
+
})
|
package/src/edge/index.ts
CHANGED
|
@@ -1231,6 +1231,38 @@ describe("slice()", () => {
|
|
|
1231
1231
|
])
|
|
1232
1232
|
})
|
|
1233
1233
|
|
|
1234
|
+
it("an ended outcome carries every span of the episode that just ended", async () => {
|
|
1235
|
+
const wf = async () => {
|
|
1236
|
+
await slice("one", async () => {
|
|
1237
|
+
await human("a")
|
|
1238
|
+
})
|
|
1239
|
+
await slice("two", async () => {
|
|
1240
|
+
await human("b")
|
|
1241
|
+
})
|
|
1242
|
+
}
|
|
1243
|
+
const h = new History().land("human", "a", 1, "b").land("human", "b", 1, "done")
|
|
1244
|
+
const outcome = await replayOf(wf, h)
|
|
1245
|
+
expect(outcome).toMatchObject({ kind: "ended", via: "return" })
|
|
1246
|
+
expect(spansOf(outcome)).toEqual([
|
|
1247
|
+
{ name: "one", enteredAt: hashOf(0), leftAt: hashOf(1) },
|
|
1248
|
+
{ name: "two", enteredAt: hashOf(1), leftAt: hashOf(2) },
|
|
1249
|
+
])
|
|
1250
|
+
})
|
|
1251
|
+
|
|
1252
|
+
it("restart() inside a slice ends via restart with that span's leftAt undefined", async () => {
|
|
1253
|
+
const wf = async () => {
|
|
1254
|
+
await human("a")
|
|
1255
|
+
await slice("risky", async () => {
|
|
1256
|
+
await human("b")
|
|
1257
|
+
await restart()
|
|
1258
|
+
})
|
|
1259
|
+
}
|
|
1260
|
+
const h = new History().land("human", "a", 1, "b").land("human", "b", 1, "done")
|
|
1261
|
+
const outcome = await replayOf(wf, h)
|
|
1262
|
+
expect(outcome).toMatchObject({ kind: "ended", via: "restart" })
|
|
1263
|
+
expect(spansOf(outcome)).toEqual([{ name: "risky", enteredAt: hashOf(1), leftAt: undefined }])
|
|
1264
|
+
})
|
|
1265
|
+
|
|
1234
1266
|
it("keeps leftAt undefined when the process rests inside the slice", async () => {
|
|
1235
1267
|
const wf = async () => {
|
|
1236
1268
|
await slice("refactor", async () => {
|
package/src/replay/Replay.ts
CHANGED
|
@@ -126,6 +126,8 @@ export type ReplayOutcome =
|
|
|
126
126
|
readonly trace: readonly ReachedStep[]
|
|
127
127
|
readonly landed: ReachedStep | undefined
|
|
128
128
|
readonly marks: readonly Mark[]
|
|
129
|
+
/** Spans of the episode that just ended; a restart() inside a slice leaves its leftAt undefined. */
|
|
130
|
+
readonly slices: readonly SliceSpan[]
|
|
129
131
|
}
|
|
130
132
|
| { readonly kind: "divergence"; readonly message: string }
|
|
131
133
|
| { readonly kind: "refused"; readonly message: string }
|
|
@@ -613,7 +615,7 @@ export const replay = async (input: ReplayInput): Promise<ReplayOutcome> => {
|
|
|
613
615
|
kind: "divergence",
|
|
614
616
|
message: `gtd: the flow ended, but commit ${short(commits[cursor]!.hash)} continues the episode — ${DIVERGED}`,
|
|
615
617
|
}
|
|
616
|
-
: { kind: "ended", via, trace, landed, marks }
|
|
618
|
+
: { kind: "ended", via, trace, landed, marks, slices: slices.map((s) => ({ ...s })) }
|
|
617
619
|
|
|
618
620
|
const changesSince = (hash: string): readonly Change[] => {
|
|
619
621
|
const tree = treeAt(hash)
|
|
@@ -115,6 +115,17 @@ describe("the commit-message codec", () => {
|
|
|
115
115
|
expect(parsed.version).toBe("1.2.3")
|
|
116
116
|
})
|
|
117
117
|
|
|
118
|
+
it("reads a Gtd-Version carrying control bytes as unknown, and keeps spaces and parens", () => {
|
|
119
|
+
const withVersion = (value: string) =>
|
|
120
|
+
formatCommitMessage({ actor: "a", to: "b" }).replace(
|
|
121
|
+
"Gtd-Format: 1",
|
|
122
|
+
`Gtd-Version: ${value}\nGtd-Format: 1`,
|
|
123
|
+
)
|
|
124
|
+
expect(parseCommitMessage(withVersion("1.0\u001b]0;x\u0007")).version).toBeUndefined()
|
|
125
|
+
expect(parseCommitMessage(withVersion("1.0\u009b")).version).toBeUndefined()
|
|
126
|
+
expect(parseCommitMessage(withVersion("0.0.1 (dev)")).version).toBe("0.0.1 (dev)")
|
|
127
|
+
})
|
|
128
|
+
|
|
118
129
|
it("omits Gtd-Branch and Gtd-Version when unset, and reads them as undefined", () => {
|
|
119
130
|
const message = formatCommitMessage({ actor: "agent", to: "idle" })
|
|
120
131
|
expect(message).not.toContain("Gtd-Branch")
|
package/src/replay/Trailers.ts
CHANGED
|
@@ -196,7 +196,9 @@ const TRAILER_READERS: Readonly<Record<string, (value: string, into: Trailers) =
|
|
|
196
196
|
if (value !== "") into.branch ??= value
|
|
197
197
|
},
|
|
198
198
|
"Gtd-Version": (value, into) => {
|
|
199
|
-
|
|
199
|
+
// Untrusted (fetched history): control bytes would reach the terminal; a rejected value reads as unknown.
|
|
200
|
+
// eslint-disable-next-line no-control-regex
|
|
201
|
+
if (value !== "" && !/[\u0000-\u001f\u007f-\u009f]/.test(value)) into.version ??= value
|
|
200
202
|
},
|
|
201
203
|
"Gtd-Kept": (value, into) => {
|
|
202
204
|
if (HASH_RE.test(value)) into.kept ??= value
|
package/src/replay/index.ts
CHANGED
|
@@ -12,4 +12,4 @@ export {
|
|
|
12
12
|
export { accessShapeFault, foldAccess } from "./Access.js"
|
|
13
13
|
export { formatFault } from "./Format.js"
|
|
14
14
|
export { refNameFault } from "./RefName.js"
|
|
15
|
-
export { sliceRanges
|
|
15
|
+
export { sliceRanges } from "./Slices.js"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { WIRE_SCHEMA } from "./constants.js"
|
|
2
|
+
|
|
3
|
+
export interface SliceEntry {
|
|
4
|
+
readonly name: string
|
|
5
|
+
/** Full hash, exclusive: git's base..tip. */
|
|
6
|
+
readonly base: string
|
|
7
|
+
/** Full hash, inclusive; HEAD's hash for the open slice. */
|
|
8
|
+
readonly tip: string
|
|
9
|
+
/** false only for the last slice of an underway process. */
|
|
10
|
+
readonly closed: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SlicesDocument {
|
|
14
|
+
readonly schema: number
|
|
15
|
+
readonly slices: readonly SliceEntry[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The ONLY place `gtd slices --json`'s shape is built; copies exactly the four fields, in order. */
|
|
19
|
+
export const slicesDocument = (slices: readonly SliceEntry[]): SlicesDocument => ({
|
|
20
|
+
schema: WIRE_SCHEMA,
|
|
21
|
+
slices: slices.map(({ name, base, tip, closed }) => ({ name, base, tip, closed })),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const renderSlicesJson = (document: SlicesDocument): string =>
|
|
25
|
+
JSON.stringify(document) + "\n"
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
landFields,
|
|
10
10
|
renderBeatJson,
|
|
11
11
|
renderLandJson,
|
|
12
|
+
renderSlicesJson,
|
|
13
|
+
slicesDocument,
|
|
12
14
|
statusOf,
|
|
13
15
|
WIRE_SCHEMA,
|
|
14
16
|
type BeatKind,
|
|
@@ -116,6 +118,9 @@ const documents: Readonly<Record<string, string>> = {
|
|
|
116
118
|
}),
|
|
117
119
|
),
|
|
118
120
|
"judge.json": JUDGE,
|
|
121
|
+
"slices.json": renderSlicesJson(
|
|
122
|
+
slicesDocument([{ name: "build", base: "a".repeat(40), tip: "b".repeat(40), closed: true }]),
|
|
123
|
+
),
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
describe(`wire schema ${WIRE_SCHEMA}`, () => {
|
|
@@ -139,6 +144,19 @@ describe(`wire schema ${WIRE_SCHEMA}`, () => {
|
|
|
139
144
|
}
|
|
140
145
|
})
|
|
141
146
|
|
|
147
|
+
describe("slicesDocument", () => {
|
|
148
|
+
it("stamps WIRE_SCHEMA and drops any field beyond the four", () => {
|
|
149
|
+
const doc = slicesDocument([
|
|
150
|
+
{ name: "n", base: "b", tip: "t", closed: false, extra: 1 } as never,
|
|
151
|
+
])
|
|
152
|
+
expect(doc).toEqual({
|
|
153
|
+
schema: WIRE_SCHEMA,
|
|
154
|
+
slices: [{ name: "n", base: "b", tip: "t", closed: false }],
|
|
155
|
+
})
|
|
156
|
+
expect(Object.keys(doc.slices[0]!)).toEqual(["name", "base", "tip", "closed"])
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
142
160
|
describe("the shape check", () => {
|
|
143
161
|
const land = JSON.parse(documents["land.json"]!) as Record<string, unknown>
|
|
144
162
|
const { subject, ...rest } = land
|
package/src/wire/index.ts
CHANGED