diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d36835f7..0822aaa3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,7 @@ Useful everyday commands: ```bash pnpm test:watch pnpm dev:ui # Vite on :4782, proxied to a `weft ui` daemon -pnpm lint # Biome checks +pnpm lint # Biome checks — warnings fail, same as CI pnpm lint:fix # Biome writes lint fixes pnpm format # Biome formatting pnpm build # package dist/ output and workflow-manager bundle @@ -46,6 +46,13 @@ pnpm clean # remove package dist/ output pnpm verify:examples # ensure every public Ctx API has a runnable example ``` +`biome.json` enables 96 rules beyond Biome's `recommended` preset, including the type-aware +`noFloatingPromises` / `noMisusedPromises` / `useExhaustiveSwitchCases`. Most were adopted because +they already fired zero times — they exist to keep it that way. Before adding a rule, measure it; +before switching one off, read +[`docs/reviews/2026-08-lint-strictness.md`](docs/reviews/2026-08-lint-strictness.md), which records +what each rule cost and why the rejected ones were rejected. + The test harness is deliberately model-free. Use `@techery/weft-testing` fixtures matched on stable step keys; they execute the real engine validation and journal path. A test should assert the observable workflow result or journal behavior, rather than only that a mock was called. Use `mockTaskEnvelope` when a test intentionally diff --git a/apps/ui/src/domain/views.ts b/apps/ui/src/domain/views.ts index c118f5ca..e92c8d4e 100644 --- a/apps/ui/src/domain/views.ts +++ b/apps/ui/src/domain/views.ts @@ -23,7 +23,7 @@ export function runTabs(run: Run, pendingGate: boolean): RunTabDef[] { } export function isRunTab(value: string | undefined): value is RunTab { - return !!value && (RUN_TABS as readonly string[]).includes(value); + return value !== undefined && (RUN_TABS as readonly string[]).includes(value); } /** Fall back to the pending gate, then to whatever step the run recorded first. */ diff --git a/biome.json b/biome.json index 4cf2e3e5..602500ef 100644 --- a/biome.json +++ b/biome.json @@ -18,18 +18,127 @@ "enabled": true, "rules": { "preset": "recommended", + "suspicious": { "noExplicitAny": "off", - "noAssignInExpressions": "off" + + "noAlert": "error", + "noArrayIndexKey": "error", + "noBitwiseOperators": "error", + "noConstantBinaryExpressions": "error", + "noDuplicateDependencies": "error", + "noDuplicateTestHooks": "error", + "noDuplicatedSpreadProps": "error", + "noEmptySource": "error", + "noEqualsToNull": "error", + "noExportsInTest": "error", + "noFocusedTests": "error", + "noForIn": "error", + "noLeakedRender": "error", + "noParametersOnlyUsedInRecursion": "error", + "noReactForwardRef": "error", + "noReturnAssign": "error", + "noSkippedTests": "error", + "noUnassignedVariables": "error", + "noUnusedExpressions": "error", + "noVar": "error", + "useDeprecatedDate": "error", + "useErrorMessage": "error", + "useNumberToFixedDigitsArgument": "error", + "useStaticResponseMethods": "error" }, + + "correctness": { + "noGlobalDirnameFilename": "error", + "noPrivateImports": "error", + "noUndeclaredVariables": "error", + "noUnusedInstantiation": "error", + "useImportExtensions": "error", + "useJsonImportAttributes": "error", + "useSingleJsDocAsterisk": "error", + + "noChildrenProp": "error", + "noNestedComponentDefinitions": "error", + "noReactPropAssignments": "error", + "noRenderReturnValue": "error", + "useExhaustiveDependencies": "error", + "useHookAtTopLevel": "error", + "useJsxKeyInIterable": "error" + }, + "style": { "noNonNullAssertion": "off", - "noParameterAssign": "off", - "useTemplate": "off" + + "noCommonJs": "error", + "noDoneCallback": "error", + "noEnum": "error", + "noInferrableTypes": "error", + "noMultiAssign": "error", + "noParameterAssign": "error", + "noSubstr": "error", + "noUselessElse": "error", + "noYodaExpression": "error", + "useAsConstAssertion": "error", + "useCollapsedElseIf": "error", + "useConsistentBuiltinInstantiation": "error", + "useConsistentEnumValueType": "error", + "useConsistentObjectDefinitions": "error", + "useDefaultParameterLast": "error", + "useEnumInitializers": "error", + "useForOf": "error", + "useFragmentSyntax": "error", + "useGroupedAccessorPairs": "error", + "useNodeAssertStrict": "error", + "useNumberNamespace": "error", + "useObjectSpread": "error", + "useSelfClosingElements": "error", + "useShorthandAssign": "error", + "useSpreadOverApply": "error", + "useSymbolDescription": "error", + "useThrowNewError": "error", + "useThrowOnlyError": "error", + "useTrimStartEnd": "error" }, + "complexity": { "noForEach": "off", - "useLiteralKeys": "off" + "useLiteralKeys": "off", + + "noDivRegex": "error", + "noExcessiveNestedTestSuites": "error", + "noImplicitCoercions": "error", + "noRedundantDefaultExport": "error", + "noUselessCatchBinding": "error", + "noUselessStringConcat": "error", + "noUselessReturn": "error", + "useArrayFind": "error", + "useWhile": "error" + }, + + "security": { + "noDangerouslySetInnerHtml": "error", + "noDangerouslySetInnerHtmlWithChildren": "error" + }, + + "nursery": { + "noEmptyObjectKeys": "error", + "noExcessiveNestedCallbacks": "error", + "noExtendNative": "error", + "noFloatingPromises": "error", + "noIdenticalTestTitle": "error", + "noMisusedPromises": "error", + "noNegationInEqualityCheck": "error", + "noUnnecessaryTemplateExpression": "error", + "noUnsafePlusOperands": "error", + "noUselessTypeConversion": "error", + "useArraySome": "error", + "useExhaustiveSwitchCases": "error", + "useIncludes": "error", + "useReduceTypeParameter": "error", + "useRegexpTest": "error", + "useStringStartsEndsWith": "error", + "useTestHooksInOrder": "error", + "useTestHooksOnTop": "error" } } }, @@ -38,5 +147,17 @@ "quoteStyle": "double", "semicolons": "always" } - } + }, + "overrides": [ + { + "includes": ["apps/ui/**"], + "linter": { + "rules": { + "correctness": { + "useImportExtensions": "off" + } + } + } + } + ] } diff --git a/docs/reviews/2026-08-lint-strictness.md b/docs/reviews/2026-08-lint-strictness.md new file mode 100644 index 00000000..dbb0ff99 --- /dev/null +++ b/docs/reviews/2026-08-lint-strictness.md @@ -0,0 +1,193 @@ +# Lint strictness review — Biome 2.5.10 + +_August 2026. Measured against `d9b7bdf` (67.5k LOC of TS/TSX across 18 packages, `apps/ui`, +`examples/`, `.weft/`)._ + +## What was measured + +Biome 2.5.10 ships **484 lint rules**; the `recommended` preset turns on 182 of them. To find out +what the remaining 302 would cost, every one was enabled at `error` in a throwaway config and the +whole repo was linted with `--reporter=json`. That produced **13,690 diagnostics across 111 rules** +— and, more usefully, **196 rules that fire zero times**. + +A rule that fires zero times is free to turn on. It changes no code today; it stops the pattern +from arriving tomorrow. That is where the bulk of the win was. + +## What changed + +`biome.json` now enables **96 rules beyond `recommended`**, and `pnpm lint` fails on warnings. + +### 1. `pnpm lint` now fails on warnings + +This was the largest single soundness hole, and it had nothing to do with which rules were on. +`biome check` exits `0` when every diagnostic is a warning or info, and **87 of the 182 recommended +rules default to `warn` (57) or `info` (30) severity** — `useConst`, `noApproximativeNumericConstant`, +`useDefaultSwitchClauseLast`, `noConstEnum`, and friends. Verified directly: a file with a +`style/useConst` violation lints "Found 1 warning" and exits `0`. Every one of those rules was +advisory in CI. + +```diff +-"lint": "biome check ." ++"lint": "biome check --error-on-warnings ." +``` + +The repo passes clean under the stricter gate, so nothing had to be fixed to land this — but from +here a warning-severity regression can no longer merge. + +### 2. Type-aware rules, all at zero cost + +Biome 2.5's type inference is good enough to run these, and the codebase is already clean under +every one of them: + +| Rule | Catches | +| --- | --- | +| `nursery/noFloatingPromises` | an async call whose rejection nobody handles | +| `nursery/noMisusedPromises` | a `Promise` used where a boolean or `void` is expected | +| `nursery/useExhaustiveSwitchCases` | a `switch` over a union that misses a member | +| `nursery/noUnsafePlusOperands` | `+` across mismatched types | +| `nursery/noUselessTypeConversion` | `String(x)` on a `string`, `Number(n)` on a `number` | + +`noFloatingPromises` returning zero on a codebase this async-heavy was surprising enough to be +worth double-checking, so it was verified against a synthetic floating promise — the rule fires, +the repo is simply clean. Locking that in is the most valuable single line in the diff: this +engine's correctness rests on awaited journal writes. + +These rules do **not** need `linter.domains.project` (confirmed with `project: "none"`), which +matters — enabling that domain would also drag in `noUnresolvedImports` and +`noUndeclaredDependencies`, both of which are unusable here (see below). + +### 3. `correctness/useImportExtensions`, scoped + +`packages/*/src` is already **100% extension-ful** (293 `.ts` / 1 `.tsx` relative imports, zero +extensionless) because the published packages are `NodeNext` ESM, where a missing extension is a +runtime `ERR_MODULE_NOT_FOUND`. All 251 violations of this rule live in `apps/ui`, which is +Vite-bundled and idiomatically extensionless. + +So the rule is on globally and off for `apps/ui` via an override. Zero code changed; a whole class +of "works in tests, breaks on install" bugs is now unrepresentable in the published packages. + +### 4. Two recommended rules taken off the disabled list + +`biome.json` had switched off four recommended rules plus three non-recommended ones. Measuring +them showed three were nearly free: + +| Rule | Violations | Action | +| --- | --- | --- | +| `suspicious/noAssignInExpressions` | 2 | fixed, re-enabled | +| `style/useTemplate` | 4 | fixed, re-enabled | +| `complexity/noUselessStringConcat` | 1 | fixed, enabled | +| `style/noParameterAssign` | 4 | fixed, enabled (was never on — it is not in `recommended`) | +| `complexity/noForEach` | 0 | left off — zero violations, but it is a style call, not a soundness one | +| `complexity/useLiteralKeys` | 34 | left off | +| `suspicious/noExplicitAny` | 49 | left off — see below | +| `style/noNonNullAssertion` | 241 | left off — see below | + +The `noAssignInExpressions` / `noParameterAssign` fixes were the same two sites in +`packages/core/src/jsonschema.ts`: `const seen = (ancestors ??= new WeakSet())`. The mutation was +dead — recursion already passes `seen` down explicitly — so it became `ancestors ?? new WeakSet()`. + +### 5. Test-suite integrity rules + +Four real defects surfaced, each a single site, each now enforced: + +- `packages/core/test/review-regressions.test.ts` had **two `describe` blocks titled + "codex review findings, round 33 (PR #1)"** (`nursery/noIdenticalTestTitle`). +- `packages/host/test/task-removal-durability.test.ts` declared `afterAll` before `beforeEach` + (`nursery/useTestHooksInOrder`). +- `packages/daemon/test/api.test.ts` exported a type from a test file + (`suspicious/noExportsInTest`). +- `packages/gate/test/gate.test.ts` used `Array(4)` rather than `new Array(4)` + (`style/useConsistentBuiltinInstantiation`). + +Alongside them, the zero-violation ratchets that keep a green suite honest: +`suspicious/noFocusedTests` (a stray `.only` silently shrinks CI to one test), +`suspicious/noSkippedTests`, `suspicious/noDuplicateTestHooks`, +`complexity/noExcessiveNestedTestSuites`, `nursery/useTestHooksOnTop`, `style/noDoneCallback`. + +### 6. The rest of the zero-cost set + +Roughly 70 more rules, all at zero violations. The ones that earn their place on soundness rather +than taste: + +- **Silent-wrong-answer guards** — `suspicious/noConstantBinaryExpressions`, + `suspicious/noUnusedExpressions`, `suspicious/noReturnAssign`, + `suspicious/noUnassignedVariables`, `suspicious/noParametersOnlyUsedInRecursion`, + `suspicious/noEqualsToNull`, `suspicious/noBitwiseOperators`, + `suspicious/useNumberToFixedDigitsArgument`, `nursery/noNegationInEqualityCheck`. +- **Error handling** — `style/useThrowOnlyError`, `style/useThrowNewError`, + `suspicious/useErrorMessage`, `complexity/noUselessCatchBinding`. A thrown non-`Error` loses its + stack, and this engine journals `cause` chains. +- **ESM / Node correctness** — `correctness/noGlobalDirnameFilename` (`__dirname` does not exist + in ESM), `correctness/useJsonImportAttributes` (`NodeNext` requires `with { type: "json" }`), + `style/noCommonJs`, `suspicious/noVar`. +- **Package boundaries** — `correctness/noPrivateImports`, `correctness/noUndeclaredVariables`, + `correctness/noUnusedInstantiation`, `complexity/noRedundantDefaultExport`. +- **React (`apps/ui`, `packages/design-system`)** — `correctness/useExhaustiveDependencies`, + `correctness/useHookAtTopLevel`, `correctness/useJsxKeyInIterable`, + `correctness/noNestedComponentDefinitions`, `correctness/noReactPropAssignments`, + `suspicious/noArrayIndexKey`, `suspicious/noLeakedRender`, + `security/noDangerouslySetInnerHtml{,WithChildren}`. +- **Idiom lock-ins** — `useForOf`, `useObjectSpread`, `useAsConstAssertion`, `noInferrableTypes`, + `noUselessElse`, `useCollapsedElseIf`, `noYodaExpression`, `noSubstr`, `useTrimStartEnd`, + `useNumberNamespace`, `nursery/useIncludes`, `nursery/useStringStartsEndsWith`, + `nursery/useRegexpTest`, `nursery/useArraySome`, `nursery/useReduceTypeParameter`. + +### Cost + +Lint wall-clock went from **0.65s to 7s** — the type-inference scanner. That is the price of +`noFloatingPromises` and it is worth paying in a 10-minute CI job. + +## Rules deliberately not adopted + +### Not sound enough yet — Biome's inference produces false positives + +These are the ones worth re-testing on each Biome upgrade. Every finding below was read in context +and is wrong: + +| Rule | Hits | Why it was rejected | +| --- | --- | --- | +| `correctness/noUnresolvedImports` | 74 | Claims `react` has no export named `StrictMode` or `Fragment`, and that `node:sqlite` and the workspace packages cannot be resolved. 53 of the 74 are `*.module.css` imports typed by `vite/client`, which Biome does not read. All false. | +| `suspicious/noUnnecessaryConditions` | 61 | Calls `if (readyTimer.current)` "always falsy" — it cannot model a React ref mutated elsewhere. | +| `nursery/noUnsafeTypeAssertion` | 757 | Directionally right, but at this volume it is a rewrite, not a lint pass. | +| `nursery/useAwaitThenable` | 5 | All five are `await` on a `T \| Promise` union (fixture hooks, `canUseTool`, `act`). Awaiting is correct. | +| `nursery/noBaseToString` | 5 | Loses `string[]` through `[...new Set(…)]` and flags `dirs.join(", ")`. | +| `nursery/useNullishCoalescing` | 1 | `state.createdAt \|\| steps[0]?.startedAt` is deliberate: `0` must fall through. | +| `suspicious/useArraySortCompare` | 14 | Fires on `string[]`, where the default comparator is the intended one. | +| `nursery/noLoopFunc` | 5 | All are `new Promise(resolve => …)` inside a `while` — no capture hazard. | +| `correctness/noUndeclaredDependencies` | 50 | Correct in principle: test files import `vitest` and workspace packages that the per-package `package.json` does not declare. Adopting it means adding devDependencies to ~15 packages. Worth doing on its own, not here. | + +### Real debt, deferred + +Sound rules with a cost that needs a decision, ranked by value: + +| Rule | Hits | Note | +| --- | --- | --- | +| `style/useErrorCause` | 21 | Real: 21 places rethrow without `{ cause }`, discarding the original stack. The highest-value item on this list. | +| `suspicious/noEvolvingTypes` | 10 | Implicit `any` evolution; 8 of 10 in tests. | +| `suspicious/noShadow` | 36 | 8 in `packages/cli/src/commands/task.ts` alone. | +| `suspicious/noEmptyBlockStatements` | 11 | Mostly deliberate `catch {}`; each wants a comment or a `void`. | +| `suspicious/noExplicitAny` | 49 | Concentrated in 8 files, ~41 of them in `packages/sdk/src/{types,composition,define}.ts` and `core/src/ctx.ts` — generic-variance escape hatches that `unknown` cannot express. Adoptable as a file-scoped `overrides` entry rather than a blanket `off`. | +| `performance/noAwaitInLoops` | 143 | Many are intentionally sequential (journal ordering). Needs case-by-case review, not a sweep. | +| `suspicious/useAwait` | 84 | `async` with no `await`; largely test helpers and interface conformance. | +| `style/noNonNullAssertion` | 241 | 184 in tests, where `!` is reasonable. A `src`-only override would cost 46 fixes and pair well with `noUncheckedIndexedAccess`, which is already on. | +| `performance/noBarrelFile` / `noNamespaceImport` / `noDelete` | 33 / 6 / 15 | Bundle-size and shape rules; low value for a Node library. | + +### Rejected on taste, not soundness + +House-style rules the codebase consistently violates by choice, with counts for the record: +`style/noTernary` (1519), `style/useBlockStatements` (1573), `style/noMagicNumbers` (799), +`style/noNegationElse` (405), `style/noIncrementDecrement` (169), `style/noContinue` (113), +`style/useNamingConvention` (125), `style/useExportsLast` (104), `style/useDestructuring` (90), +`nursery/useExplicitType` (617), `complexity/noExcessiveCognitiveComplexity` (139). + +Framework rules for stacks this repo does not use (Vue, Qwik, Solid, Next, Astro, Svelte, GraphQL, +Drizzle, Playwright, React Native, Tailwind) were excluded outright — note that Biome runs them +regardless of what is installed, so `suspicious/noReactSpecificProps` (604) and +`correctness/noSolidDestructuredProps` (213) are noise, not findings. + +## Maintenance + +Eighteen of the 96 new rules are in Biome's `nursery` group, which is explicitly unstable. +`@biomejs/biome` is pinned to an exact `2.5.10` in `package.json`, so they cannot shift underneath +CI — but **a Biome upgrade should re-run this measurement**, both to re-check the nursery rules and +to see whether the false-positive list above has shrunk. diff --git a/examples/03-write-steps-and-patches/main.ts b/examples/03-write-steps-and-patches/main.ts index 69b2a317..8f0412c0 100644 --- a/examples/03-write-steps-and-patches/main.ts +++ b/examples/03-write-steps-and-patches/main.ts @@ -106,6 +106,6 @@ console.log( const report = await engine.report(handle.runId); const risk = report.split("## Remaining risk")[1]?.split("##")[0]?.trim(); -console.log("\nreport → remaining risk:\n" + (risk ?? "(none)")); +console.log(`\nreport → remaining risk:\n${risk ?? "(none)"}`); await rm(repo, { recursive: true, force: true }); diff --git a/package.json b/package.json index 9e7cfb49..cdc541b8 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit && pnpm --filter @techery/weft-ui typecheck", "test": "vitest run", "test:watch": "vitest", - "lint": "biome check .", + "lint": "biome check --error-on-warnings .", "lint:fix": "biome check --write .", "format": "biome format --write .", "sync:meta": "node scripts/sync-package-meta.mjs", diff --git a/packages/cli/src/commands/answer.ts b/packages/cli/src/commands/answer.ts index 72b93a01..931456d6 100644 --- a/packages/cli/src/commands/answer.ts +++ b/packages/cli/src/commands/answer.ts @@ -35,14 +35,15 @@ export function answerCommand(io: CliIo): Command { try { // `weft answer 7f3a '{"approved":true}'` — the id is optional, so a lone JSON // argument lands in its place. Read it back out rather than making people pad it. - if (json === undefined && req !== undefined && looksLikeJson(req)) { - json = req; - req = undefined; - } - const request = await pickRequest(io, weft, runId, req); + const loneJson = json === undefined && req !== undefined && looksLikeJson(req); + const answerJson = loneJson ? req : json; + const requestId = loneJson ? undefined : req; + + const request = await pickRequest(io, weft, runId, requestId); if (!request) return; - const value = json !== undefined ? parseJsonValue(json, "answer") : await promptForAnswer(request); + const value = + answerJson !== undefined ? parseJsonValue(answerJson, "answer") : await promptForAnswer(request); if (value === undefined) { io.out(pc.dim("cancelled — nothing was answered")); return; diff --git a/packages/core/src/jsonschema.ts b/packages/core/src/jsonschema.ts index fdb864f8..2a82b8d2 100644 --- a/packages/core/src/jsonschema.ts +++ b/packages/core/src/jsonschema.ts @@ -262,7 +262,7 @@ export function jsonUnsafeAt(value: unknown, path = "$", ancestors?: WeakSet { }); }); -describe("codex review findings, round 33 (PR #1)", () => { +describe("codex review findings, round 33 continued (PR #1)", () => { test("object enum/const equality ignores property entry order", async () => { const { structuralCheck } = await import("../src/jsonschema.ts"); const schema = { enum: [{ a: 1, b: { c: [1, 2] } }] }; diff --git a/packages/daemon/test/api.test.ts b/packages/daemon/test/api.test.ts index f4d5d54b..b1a86a45 100644 --- a/packages/daemon/test/api.test.ts +++ b/packages/daemon/test/api.test.ts @@ -805,7 +805,7 @@ describe("the whole app", () => { }); /** Referenced so the RunState import is load-bearing for the shapes asserted above. */ -export type _State = RunState; +type _State = RunState; // --------------------------------------------------------------------------- // Regressions diff --git a/packages/gate/src/load.ts b/packages/gate/src/load.ts index 80e7b1fd..ee4ac521 100644 --- a/packages/gate/src/load.ts +++ b/packages/gate/src/load.ts @@ -95,8 +95,7 @@ export async function instantiateBundle( // reads it immediately (and a hand-rolled object would otherwise fail much later). const def = mod.exports.default ?? mod.exports; if (!isWorkflowDefinition(def) || typeof def.meta?.description !== "string") { - const message = - `no workflow definition exported from ${where}` + ' - use "export default defineWorkflow({ … }, run)"'; + const message = `no workflow definition exported from ${where} - use "export default defineWorkflow({ … }, run)"`; throw new GateError(message, [{ rule: "no-workflow-export", message, file: where, line: 0, column: 0 }]); } return def; diff --git a/packages/gate/test/gate.test.ts b/packages/gate/test/gate.test.ts index 46303afd..989a8be6 100644 --- a/packages/gate/test/gate.test.ts +++ b/packages/gate/test/gate.test.ts @@ -292,7 +292,7 @@ describe("checkSource", () => { `export * from "chalk";`, ]; const diagnostics = checkSource(lines.join("\n"), "imports.ts"); - expect(diagnostics.map((d) => d.rule)).toEqual(Array(4).fill("no-bare-import")); + expect(diagnostics.map((d) => d.rule)).toEqual(new Array(4).fill("no-bare-import")); expect(diagnostics.map((d) => d.line)).toEqual([6, 7, 8, 10]); expect(diagnostics[0]?.message).toContain('node builtin "node:fs"'); expect(diagnostics[1]?.message).toContain('node builtin "path"'); diff --git a/packages/host/test/task-removal-durability.test.ts b/packages/host/test/task-removal-durability.test.ts index dcc0b3fd..7a5685e6 100644 --- a/packages/host/test/task-removal-durability.test.ts +++ b/packages/host/test/task-removal-durability.test.ts @@ -34,8 +34,8 @@ vi.mock("node:fs/promises", async (importOriginal) => { const { TaskStore } = await import("../src/tasks.ts"); -afterAll(cleanupRoots); beforeEach(() => events.splice(0)); +afterAll(cleanupRoots); describe("TaskStore removal durability", () => { it("syncs the workflow directory after unlinking a task", async () => { diff --git a/packages/isolation/test/tmp/nested.mts b/packages/isolation/test/tmp/nested.mts index 993275e2..ccd6c280 100644 --- a/packages/isolation/test/tmp/nested.mts +++ b/packages/isolation/test/tmp/nested.mts @@ -29,6 +29,6 @@ const out = await applyPatchToTree({ repoRoot: repo, patch: cap.patch }); console.log("APPLY:", JSON.stringify(out)); console.log( "CONTENT AFTER:", - await readFile(join(repo, "subproj/index.ts"), "utf8").catch((e) => "*** LOST: " + e.code), + await readFile(join(repo, "subproj/index.ts"), "utf8").catch((e) => `*** LOST: ${e.code}`), ); await rm(repo, { recursive: true, force: true }); diff --git a/packages/store-fs/src/journal.ts b/packages/store-fs/src/journal.ts index c5feb1f7..34586ec6 100644 --- a/packages/store-fs/src/journal.ts +++ b/packages/store-fs/src/journal.ts @@ -156,7 +156,7 @@ export class FsJournalStore implements JournalStore { const rec: JournalRecord = { i: base + offset, at, ev }; return rec; }); - const payload = Buffer.from(records.map((r) => JSON.stringify(r)).join("\n") + "\n"); + const payload = Buffer.from(`${records.map((r) => JSON.stringify(r)).join("\n")}\n`); const fd = openSync(this.journalPath(runId), "a"); try { // writeSync may return SHORT without throwing; treating a partial write