fix: take an unsafe entry's rule id from the entry, not its path - #242
Conversation
|
@claude /review |
|
Claude finished @thecodedrift's task in 2m 27s —— View job Review completeReview mode: incremental — read 0 prior review thread(s) before reviewing.
SummaryThis is a clean, well-scoped fix. Test coverage is solid: Deliberately-untouched behavior ( One finding posted inline ( No other correctness, security, or performance issues found in the diff. |
`repairTargets` parsed the rule id out of a reported `check.ts` path because reconcile's `unsafe` entries did not carry `ruleId`. They do: the deployed `GET /cli/api/__schema` requires `[expected, file, got, ruleId]` on `unsafe`. Our hand-written `UnsafeEntry` never declared the field, so it arrived on the wire and stayed invisible. The parse tied repair to a filesystem layout that has moved twice. A further move breaks it with nothing anyone sees: a wrong id, a 404 from restore, and a rule left unrepaired and unexecuted, which reads exactly like a pass. Deletes `ruleIdFromCheckPath`, the `unidentifiable` bucket it fed, and the notice that reported it. `repairTargets` now returns a plain array, since an entry can no longer fail to identify itself. `unknown` still routes nowhere, and `verifyRestoredCheck` is untouched. Fixes #240
`repairTargets` read `ruleId` off the entry unconditionally, and `reconcile` decodes the response with a cast rather than a schema. The deployed schema requires the field on both repairable verdicts, so the `string` type is the service's promise and not a fact about the bytes that arrived. A rollback, a canary, or a schema regression put `undefined` into it and `restoreRule` interpolated it straight into `/cli/api/request/undefined/restore` — a call that cannot succeed, made for a rule nobody named, failing where nobody reads it. Entries without a usable id are now partitioned out and reported as a notice, the way the deleted `unidentifiable` path did. Covers `missing` as well as `unsafe`, since the gap predates the entry-id change on that bucket. Whitespace-only ids are rejected for the same reason an absent one is: they encode into an equally meaningless request. Nothing is guessed. Path parsing stays deleted; the answer to a missing id is to report it. The rule stays withheld, which is already the safe state, and the notice reaches the `--json` envelope so a CI run sees it rather than reading a clean pass. This echoes `writeRuleFile`, which keeps a runtime check for a payload carrying both `files` and `content` even though the published union makes it unrepresentable.
794ed01 to
dd2a991
Compare
|
Addressed in dd2a991 (rebased onto The one finding was real. I reproduced it before fixing it: with the guard reverted, an integration test serving an — which is What changed.
Proven to bite. Reverting Checks, re-run after the rebase rather than assuming the pre-rebase run held: — AI Coding Agent |
The last open task was waiting on the generator sending `ruleId` on a reconcile entry, which landed in #242: `unsafe`, `unknown` and `missing` all carry it, and `ruleIdFromCheckPath` is deleted. N4 is confirmed both sides — the engine tier stays defined and unused, since G2 means a Vale rule cannot be delivered without its `.vale.ini` and so the middle rung has no future occupant. The directory move is the smaller half. Archiving promotes the change's spec deltas into the standing specs, which is what makes the contract official: `cli-generated-rule-delivery` and `cli-layout-export` become capabilities, and `cli-rule-reconciliation`, `cli-rules` and `cli-runtime-rule-execution` take the requirements the slices implemented. Eight requirements added, two modified. No code changes. The changeset was grown as the stack drained and already carries every slice.
Rule repair no longer reads a rule id out of a filesystem path.
The fallback
repairTargetsinpackages/cli/src/rules/runtime/repair.tsparsed the id out of the reportedcheck.tspath withruleIdFromCheckPath, matching.taskless/rules/runtime/<id>/check.tsfrom the right. It existed only because a reconcileunsafeentry did not carryruleId, and it tied repair to a filesystem layout that has already moved twice (0004,0005). Its own comment named the failure and the exit: a further move yields a wrong id, restore 404s, and the rule stays unrepaired and unexecuted — with nothing on stdout, nothing on stderr, and nothing on the--jsonenvelope. A rule that never fires looks exactly like a clean pass.The evidence that retires it
GET https://app.taskless.io/cli/api/__schema(public, no auth), fetched from the deployed service:Our hand-written
UnsafeEntryinpackages/cli/src/api/reconcile.tsdeclared only{ file, expected, got }, soruleIdwas on the wire and invisible to our code.What changed
UnsafeEntrygainsruleId.repairTargetsreads it off the entry for both repairable buckets, exactly asmissingalready did.ruleIdFromCheckPath, theTASKLESS_DIRECTORYconstant andRULES_DIRECTORYimport it needed, theunidentifiablebucket it fed, and thecheck.tsnotice that reported it. An entry can no longer fail to identify itself, sorepairTargetsreturns a plainRepairTarget[]instead of{ targets, unidentifiable }.test/repair.test.ts: thedescribe("reading a rule id out of a check path")block and the unidentifiable-entry case.Deliberately untouched:
verifyRestoredCheckstill compares restored bytes againstunsafe.expected, the signature reconcile sent, not the one the restore response carries. That is what makes "restore never returns newer bytes" testable, and the test asserting it still passes.unknownstill routes nowhere. There is nothing on the server to fetch for a file it never issued, which is why that entry alone carries no rule id.The disagreeing-id test
test/repair-integration.test.tsgains a case whose reconcile fixture reportsruleId: "logs-abc12345"on a file at.taskless/rules/runtime/demo/check.ts— the entry and the path name different rules. It asserts restore is called withlogs-abc12345and that the blessed bytes land under that id's directory. A path-parsing implementation asks fordemoand fails it.test/repair.test.tscovers the same disagreement as a pure function, over bothunsafeandmissing, plus a new case for an unsafe entry whose file sits outside the rules tree entirely — repairable now, dropped as unidentifiable before.Proven to bite. Reinstating the parse (
ruleId: entry.file.split("/").at(-2) ?? entry.ruleId) fails exactly the three new assertions and nothing else: 3 failed / 14 passed. Restored, all 17 pass.Checks
pnpm build,pnpm typecheck,pnpm lint,pnpm --filter @taskless/cli exec vitest run(1169 passed, 73 files), andpnpm cli checkagainst a fresh build — 4 warnings, the known pre-existingno-hedgingset (3 inroute.txt, 1 inonboard.txt).Fixes #240